@launchframe/cli 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/package.json +45 -0
- package/src/commands/deploy-configure.js +219 -0
- package/src/commands/deploy-init.js +277 -0
- package/src/commands/deploy-set-env.js +232 -0
- package/src/commands/deploy-up.js +144 -0
- package/src/commands/docker-build.js +44 -0
- package/src/commands/docker-destroy.js +93 -0
- package/src/commands/docker-down.js +44 -0
- package/src/commands/docker-logs.js +69 -0
- package/src/commands/docker-up.js +73 -0
- package/src/commands/doctor.js +20 -0
- package/src/commands/help.js +79 -0
- package/src/commands/init.js +126 -0
- package/src/commands/service.js +569 -0
- package/src/commands/waitlist-deploy.js +231 -0
- package/src/commands/waitlist-down.js +50 -0
- package/src/commands/waitlist-logs.js +55 -0
- package/src/commands/waitlist-up.js +95 -0
- package/src/generator.js +190 -0
- package/src/index.js +158 -0
- package/src/prompts.js +200 -0
- package/src/services/registry.js +48 -0
- package/src/services/variant-config.js +349 -0
- package/src/utils/docker-helper.js +237 -0
- package/src/utils/env-generator.js +88 -0
- package/src/utils/env-validator.js +75 -0
- package/src/utils/file-ops.js +87 -0
- package/src/utils/project-helpers.js +104 -0
- package/src/utils/section-replacer.js +71 -0
- package/src/utils/ssh-helper.js +220 -0
- package/src/utils/variable-replacer.js +95 -0
- package/src/utils/variant-processor.js +313 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const { requireProject, getProjectConfig } = require('../utils/project-helpers');
|
|
6
|
+
const { generateSecret, checkForPlaceholders } = require('../utils/env-validator');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configure production environment variables
|
|
10
|
+
*/
|
|
11
|
+
async function deploySetEnv() {
|
|
12
|
+
requireProject();
|
|
13
|
+
|
|
14
|
+
console.log(chalk.blue.bold('\nš LaunchFrame Production Environment Configuration\n'));
|
|
15
|
+
|
|
16
|
+
const config = getProjectConfig();
|
|
17
|
+
if (!config.deployConfigured) {
|
|
18
|
+
console.log(chalk.red('ā Error: Deployment not configured yet\n'));
|
|
19
|
+
console.log(chalk.gray('Run this command first:'));
|
|
20
|
+
console.log(chalk.white(' launchframe deploy:configure\n'));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const projectRoot = process.cwd();
|
|
25
|
+
const envPath = path.join(projectRoot, 'infrastructure', '.env');
|
|
26
|
+
const envProdPath = path.join(projectRoot, 'infrastructure', '.env.prod');
|
|
27
|
+
|
|
28
|
+
// Check if .env.prod already exists
|
|
29
|
+
const envProdExists = await fs.pathExists(envProdPath);
|
|
30
|
+
|
|
31
|
+
if (envProdExists) {
|
|
32
|
+
console.log(chalk.yellow('ā ļø .env.prod already exists.\n'));
|
|
33
|
+
|
|
34
|
+
const { action } = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'list',
|
|
37
|
+
name: 'action',
|
|
38
|
+
message: 'What would you like to do?',
|
|
39
|
+
choices: [
|
|
40
|
+
{ name: 'Update existing values', value: 'update' },
|
|
41
|
+
{ name: 'Start fresh (overwrite)', value: 'overwrite' },
|
|
42
|
+
{ name: 'Cancel', value: 'cancel' }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
if (action === 'cancel') {
|
|
48
|
+
console.log(chalk.yellow('\nā Configuration cancelled\n'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (action === 'overwrite') {
|
|
53
|
+
await fs.remove(envProdPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Copy .env as template if .env.prod doesn't exist
|
|
58
|
+
if (!await fs.pathExists(envProdPath)) {
|
|
59
|
+
if (!await fs.pathExists(envPath)) {
|
|
60
|
+
console.log(chalk.red('ā Error: infrastructure/.env file not found\n'));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await fs.copy(envPath, envProdPath);
|
|
65
|
+
console.log(chalk.green('ā Created .env.prod from .env template\n'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(chalk.yellow('Configure production environment variables.\n'));
|
|
69
|
+
console.log(chalk.gray('Leave blank to keep existing values or generate secure defaults.\n'));
|
|
70
|
+
|
|
71
|
+
// Generate secure defaults
|
|
72
|
+
const dbPassword = generateSecret(24);
|
|
73
|
+
const redisPassword = generateSecret(24);
|
|
74
|
+
const jwtSecret = generateSecret(32);
|
|
75
|
+
const bullAdminToken = generateSecret(32);
|
|
76
|
+
|
|
77
|
+
const answers = await inquirer.prompt([
|
|
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: 'openaiApiKey',
|
|
115
|
+
message: 'OpenAI API key:',
|
|
116
|
+
default: ''
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'input',
|
|
120
|
+
name: 'claudeApiKey',
|
|
121
|
+
message: 'Claude API key:',
|
|
122
|
+
default: ''
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'input',
|
|
126
|
+
name: 'polarAccessToken',
|
|
127
|
+
message: 'Polar Access Token:',
|
|
128
|
+
default: ''
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'input',
|
|
132
|
+
name: 'polarWebhookSecret',
|
|
133
|
+
message: 'Polar Webhook Secret:',
|
|
134
|
+
default: ''
|
|
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
|
+
]);
|
|
149
|
+
|
|
150
|
+
// Read current .env.prod content
|
|
151
|
+
let envContent = await fs.readFile(envProdPath, 'utf8');
|
|
152
|
+
|
|
153
|
+
// Replace values based on deployment mode
|
|
154
|
+
const replacements = {};
|
|
155
|
+
|
|
156
|
+
if (isWaitlistMode) {
|
|
157
|
+
// Waitlist mode: only update Resend API key if provided
|
|
158
|
+
// Airtable credentials should already be in .env from component:add
|
|
159
|
+
if (answers.resendApiKey) {
|
|
160
|
+
replacements['RESEND_API_KEY'] = answers.resendApiKey;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// Full-app mode: update all configuration
|
|
164
|
+
replacements['DB_PASSWORD'] = answers.dbPassword;
|
|
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
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update environment variables
|
|
178
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
179
|
+
// Match KEY=old_value and replace with KEY=new_value
|
|
180
|
+
const regex = new RegExp(`^${key}=.*$`, 'gm');
|
|
181
|
+
if (regex.test(envContent)) {
|
|
182
|
+
envContent = envContent.replace(regex, `${key}=${value}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Update production URLs based on deployment config
|
|
187
|
+
if (config.deployment?.primaryDomain) {
|
|
188
|
+
const domain = config.deployment.primaryDomain;
|
|
189
|
+
const urlReplacements = {
|
|
190
|
+
'API_BASE_URL': `https://api.${domain}`,
|
|
191
|
+
'ADMIN_BASE_URL': `https://admin.${domain}`,
|
|
192
|
+
'FRONTEND_BASE_URL': `https://app.${domain}`,
|
|
193
|
+
'WEBSITE_BASE_URL': `https://${domain}`
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
for (const [key, value] of Object.entries(urlReplacements)) {
|
|
197
|
+
const regex = new RegExp(`^${key}=.*$`, 'gm');
|
|
198
|
+
if (regex.test(envContent)) {
|
|
199
|
+
envContent = envContent.replace(regex, `${key}=${value}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Write updated content
|
|
205
|
+
await fs.writeFile(envProdPath, envContent, 'utf8');
|
|
206
|
+
|
|
207
|
+
console.log(chalk.yellow('\nāļø Validating configuration...\n'));
|
|
208
|
+
|
|
209
|
+
// Check for remaining placeholders
|
|
210
|
+
const { hasPlaceholders, placeholders } = await checkForPlaceholders(envProdPath);
|
|
211
|
+
|
|
212
|
+
if (hasPlaceholders) {
|
|
213
|
+
console.log(chalk.yellow('ā ļø Warning: Some placeholder variables remain:\n'));
|
|
214
|
+
placeholders.forEach(p => console.log(chalk.gray(` - ${p}`)));
|
|
215
|
+
console.log(chalk.gray('\nYou can edit .env.prod manually to set these values.\n'));
|
|
216
|
+
} else {
|
|
217
|
+
console.log(chalk.green('ā No placeholder variables found\n'));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(chalk.green.bold('ā
Production environment configured!\n'));
|
|
221
|
+
console.log(chalk.white('Configuration saved to:'));
|
|
222
|
+
console.log(chalk.gray(' - infrastructure/.env.prod\n'));
|
|
223
|
+
|
|
224
|
+
console.log(chalk.yellow('ā ļø Important:'));
|
|
225
|
+
console.log(chalk.gray(' - .env.prod is gitignored (will not be committed)'));
|
|
226
|
+
console.log(chalk.gray(' - Keep this file secure - it contains production secrets\n'));
|
|
227
|
+
|
|
228
|
+
console.log(chalk.white('Next step:'));
|
|
229
|
+
console.log(chalk.gray(' Run: launchframe deploy:init\n'));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = { deploySetEnv };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { exec } = require('child_process');
|
|
3
|
+
const { promisify } = require('util');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const { requireProject, getProjectConfig } = require('../utils/project-helpers');
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Start services on VPS using Docker context
|
|
11
|
+
*/
|
|
12
|
+
async function deployUp() {
|
|
13
|
+
requireProject();
|
|
14
|
+
|
|
15
|
+
console.log(chalk.blue.bold('\nš LaunchFrame Service Deployment\n'));
|
|
16
|
+
|
|
17
|
+
const config = getProjectConfig();
|
|
18
|
+
|
|
19
|
+
// Validate deployment is configured
|
|
20
|
+
if (!config.deployConfigured || !config.deployment) {
|
|
21
|
+
console.log(chalk.red('ā Error: Deployment not configured yet\n'));
|
|
22
|
+
console.log(chalk.gray('Run this command first:'));
|
|
23
|
+
console.log(chalk.white(' launchframe deploy:configure\n'));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { vpsHost, vpsUser, vpsAppFolder, primaryDomain } = config.deployment;
|
|
28
|
+
const { projectName } = config;
|
|
29
|
+
|
|
30
|
+
// Step 1: Check Docker version
|
|
31
|
+
console.log(chalk.yellow('š³ Step 1: Checking Docker version...\n'));
|
|
32
|
+
|
|
33
|
+
const dockerSpinner = ora('Checking Docker installation...').start();
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const { stdout: versionOutput } = await execAsync('docker version --format "{{.Client.Version}}"');
|
|
37
|
+
const version = versionOutput.trim();
|
|
38
|
+
const [major, minor] = version.split('.').map(Number);
|
|
39
|
+
|
|
40
|
+
if (major < 19 || (major === 19 && minor < 3)) {
|
|
41
|
+
dockerSpinner.fail(`Docker version ${version} is too old`);
|
|
42
|
+
console.log(chalk.red('\nā Docker 19.03+ is required for context support\n'));
|
|
43
|
+
console.log(chalk.gray('Please upgrade Docker:'));
|
|
44
|
+
console.log(chalk.white(' https://docs.docker.com/engine/install/\n'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dockerSpinner.succeed(`Docker ${version} (compatible)`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
dockerSpinner.fail('Docker not found');
|
|
51
|
+
console.log(chalk.red('\nā Docker is not installed or not in PATH\n'));
|
|
52
|
+
console.log(chalk.gray('Please install Docker:'));
|
|
53
|
+
console.log(chalk.white(' https://docs.docker.com/engine/install/\n'));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Step 2: Verify SSH connection
|
|
58
|
+
console.log(chalk.yellow('\nš Step 2: Verifying SSH connection...\n'));
|
|
59
|
+
|
|
60
|
+
const sshSpinner = ora('Testing SSH connection...').start();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await execAsync(`ssh -o ConnectTimeout=10 ${vpsUser}@${vpsHost} "echo 'Connected'"`, {
|
|
64
|
+
timeout: 15000
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
sshSpinner.succeed('SSH connection verified');
|
|
68
|
+
} catch (error) {
|
|
69
|
+
sshSpinner.fail('Failed to connect via SSH');
|
|
70
|
+
console.log(chalk.red(`\nā Error: ${error.message}\n`));
|
|
71
|
+
console.log(chalk.gray('Make sure you can SSH to the VPS without password (key-based auth).\n'));
|
|
72
|
+
console.log(chalk.gray(`Test manually: ssh ${vpsUser}@${vpsHost}\n`));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 3: Start services on VPS via SSH
|
|
77
|
+
console.log(chalk.yellow('\nš Step 3: Starting services on VPS...\n'));
|
|
78
|
+
console.log(chalk.gray('This will start all Docker containers in production mode.\n'));
|
|
79
|
+
|
|
80
|
+
const deploySpinner = ora('Connecting to VPS...').start();
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
deploySpinner.text = 'Starting full application stack...';
|
|
84
|
+
|
|
85
|
+
await execAsync(
|
|
86
|
+
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d"`,
|
|
87
|
+
{ timeout: 300000 } // 5 minutes
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
deploySpinner.succeed('Full application started successfully');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
deploySpinner.fail('Failed to start services');
|
|
93
|
+
|
|
94
|
+
console.log(chalk.red(`\nā Error: ${error.message}\n`));
|
|
95
|
+
console.log(chalk.gray('Common issues:'));
|
|
96
|
+
console.log(chalk.gray(' - Docker not running on VPS'));
|
|
97
|
+
console.log(chalk.gray(' - Docker Compose not installed on VPS'));
|
|
98
|
+
console.log(chalk.gray(' - Insufficient permissions\n'));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Step 4: Verify services are running
|
|
103
|
+
console.log(chalk.yellow('\nš Step 4: Verifying deployment...\n'));
|
|
104
|
+
|
|
105
|
+
const verifySpinner = ora('Checking service status...').start();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const { stdout: psOutput} = await execAsync(
|
|
109
|
+
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml ps"`,
|
|
110
|
+
{ timeout: 30000 }
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
verifySpinner.succeed('Services verified');
|
|
114
|
+
|
|
115
|
+
console.log(chalk.gray('\n' + psOutput));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
verifySpinner.warn('Could not verify services');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Success!
|
|
121
|
+
console.log(chalk.green.bold('\nā
Deployment complete!\n'));
|
|
122
|
+
|
|
123
|
+
console.log(chalk.white('Your application is now running at:\n'));
|
|
124
|
+
console.log(chalk.cyan(` š User Frontend: https://${primaryDomain}`));
|
|
125
|
+
console.log(chalk.cyan(` āļø Admin Panel: https://admin.${primaryDomain}`));
|
|
126
|
+
console.log(chalk.cyan(` š API: https://api.${primaryDomain}`));
|
|
127
|
+
console.log(chalk.cyan(` š Website: https://www.${primaryDomain || primaryDomain}`));
|
|
128
|
+
|
|
129
|
+
console.log(chalk.white('\nā° Note: SSL certificates from Let\'s Encrypt may take a few minutes to provision.\n'));
|
|
130
|
+
|
|
131
|
+
console.log(chalk.white('Next steps:\n'));
|
|
132
|
+
console.log(chalk.white('1. Configure GitHub Secrets for automated deployments:'));
|
|
133
|
+
console.log(chalk.gray(` Repository: https://github.com/${config.deployment.githubOrg}/${projectName}/settings/secrets/actions`));
|
|
134
|
+
console.log(chalk.gray(' Required secrets:'));
|
|
135
|
+
console.log(chalk.gray(' - VPS_HOST, VPS_USER, VPS_SSH_KEY, GHCR_TOKEN\n'));
|
|
136
|
+
|
|
137
|
+
console.log(chalk.white('2. Future deployments:'));
|
|
138
|
+
console.log(chalk.gray(' Just push to GitHub - CI/CD will handle deployment automatically!\n'));
|
|
139
|
+
|
|
140
|
+
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
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { deployUp };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { requireProject } = require('../utils/project-helpers');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build Docker images for the project
|
|
9
|
+
*/
|
|
10
|
+
async function dockerBuild() {
|
|
11
|
+
requireProject();
|
|
12
|
+
|
|
13
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(infrastructurePath)) {
|
|
16
|
+
console.error(chalk.red('\nā Error: infrastructure/ directory not found'));
|
|
17
|
+
console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log(chalk.blue.bold('\nšØ Building Docker Images\n'));
|
|
22
|
+
console.log(chalk.gray('This will build all service images for local development...\n'));
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const buildCommand = 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml build';
|
|
26
|
+
|
|
27
|
+
console.log(chalk.gray(`Running: ${buildCommand}\n`));
|
|
28
|
+
|
|
29
|
+
execSync(buildCommand, {
|
|
30
|
+
cwd: infrastructurePath,
|
|
31
|
+
stdio: 'inherit'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log(chalk.green.bold('\nā
Docker images built successfully!\n'));
|
|
35
|
+
console.log(chalk.white('Next steps:'));
|
|
36
|
+
console.log(chalk.gray(' launchframe docker:up # Start all services\n'));
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(chalk.red('\nā Error during build:'), error.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { dockerBuild };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const { requireProject } = require('../utils/project-helpers');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Destroy all Docker resources for this project
|
|
10
|
+
* @param {Object} options - Command options
|
|
11
|
+
* @param {boolean} options.force - Skip confirmation prompt
|
|
12
|
+
*/
|
|
13
|
+
async function dockerDestroy(options = {}) {
|
|
14
|
+
requireProject();
|
|
15
|
+
|
|
16
|
+
console.log(chalk.yellow.bold('\nā ļø WARNING: Docker Resource Destruction\n'));
|
|
17
|
+
console.log(chalk.white('This will remove ALL Docker resources for this project:'));
|
|
18
|
+
console.log(chalk.gray(' ⢠All containers (running and stopped)'));
|
|
19
|
+
console.log(chalk.gray(' ⢠All volumes (including databases)'));
|
|
20
|
+
console.log(chalk.gray(' ⢠All images'));
|
|
21
|
+
console.log(chalk.gray(' ⢠Project network\n'));
|
|
22
|
+
console.log(chalk.red('ā ļø This action is IRREVERSIBLE. All data will be lost.\n'));
|
|
23
|
+
|
|
24
|
+
// Skip confirmation if --force flag is provided
|
|
25
|
+
if (!options.force) {
|
|
26
|
+
const { confirmed } = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: 'confirm',
|
|
29
|
+
name: 'confirmed',
|
|
30
|
+
message: 'Are you sure you want to destroy all Docker resources?',
|
|
31
|
+
default: false
|
|
32
|
+
}
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
if (!confirmed) {
|
|
36
|
+
console.log(chalk.gray('\nā Cancelled. No changes made.\n'));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
console.log(chalk.yellow('--force flag detected, skipping confirmation...\n'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const markerPath = path.join(process.cwd(), '.launchframe');
|
|
45
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
46
|
+
const projectName = marker.projectName;
|
|
47
|
+
|
|
48
|
+
console.log(chalk.yellow('\nšļø Destroying Docker resources...\n'));
|
|
49
|
+
|
|
50
|
+
// Step 1: Stop and remove all containers
|
|
51
|
+
console.log(chalk.gray('Stopping and removing containers...'));
|
|
52
|
+
try {
|
|
53
|
+
execSync(`docker ps -a --filter "name=${projectName}" -q | xargs -r docker rm -f`, { stdio: 'inherit' });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Ignore errors if no containers found
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Step 2: Remove all volumes
|
|
59
|
+
console.log(chalk.gray('Removing volumes...'));
|
|
60
|
+
try {
|
|
61
|
+
execSync(`docker volume ls --filter "name=${projectName}" -q | xargs -r docker volume rm`, { stdio: 'inherit' });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Ignore errors if no volumes found
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Step 3: Remove all images
|
|
67
|
+
console.log(chalk.gray('Removing images...'));
|
|
68
|
+
try {
|
|
69
|
+
execSync(`docker images --filter "reference=${projectName}*" -q | xargs -r docker rmi -f`, { stdio: 'inherit' });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Ignore errors if no images found
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step 4: Remove network
|
|
75
|
+
console.log(chalk.gray('Removing network...'));
|
|
76
|
+
try {
|
|
77
|
+
execSync(`docker network rm ${projectName}-network`, { stdio: 'inherit' });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Ignore errors if network doesn't exist
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(chalk.green.bold('\nā
All Docker resources destroyed successfully!\n'));
|
|
83
|
+
console.log(chalk.white('To rebuild your project:'));
|
|
84
|
+
console.log(chalk.gray(' cd infrastructure'));
|
|
85
|
+
console.log(chalk.gray(' docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build\n'));
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(chalk.red('\nā Error during cleanup:'), error.message);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { dockerDestroy };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { requireProject } = require('../utils/project-helpers');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Stop all Docker services (keeps volumes/data)
|
|
9
|
+
*/
|
|
10
|
+
async function dockerDown() {
|
|
11
|
+
requireProject();
|
|
12
|
+
|
|
13
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(infrastructurePath)) {
|
|
16
|
+
console.error(chalk.red('\nā Error: infrastructure/ directory not found'));
|
|
17
|
+
console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log(chalk.blue.bold('\nš Stopping Docker Services\n'));
|
|
22
|
+
console.log(chalk.gray('Stopping all services (data will be preserved)...\n'));
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const downCommand = 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml down';
|
|
26
|
+
|
|
27
|
+
console.log(chalk.gray(`Running: ${downCommand}\n`));
|
|
28
|
+
|
|
29
|
+
execSync(downCommand, {
|
|
30
|
+
cwd: infrastructurePath,
|
|
31
|
+
stdio: 'inherit'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log(chalk.green.bold('\nā
All services stopped successfully!\n'));
|
|
35
|
+
console.log(chalk.white('Your data (database, volumes) has been preserved.'));
|
|
36
|
+
console.log(chalk.gray('Run `launchframe docker:up` to start services again.\n'));
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(chalk.red('\nā Error stopping services:'), error.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { dockerDown };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const { requireProject } = require('../utils/project-helpers');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* View logs from Docker services
|
|
9
|
+
*/
|
|
10
|
+
async function dockerLogs() {
|
|
11
|
+
requireProject();
|
|
12
|
+
|
|
13
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(infrastructurePath)) {
|
|
16
|
+
console.error(chalk.red('\nā Error: infrastructure/ directory not found'));
|
|
17
|
+
console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get optional service name from args (e.g., launchframe docker:logs backend)
|
|
22
|
+
const service = process.argv[3];
|
|
23
|
+
|
|
24
|
+
console.log(chalk.blue.bold('\nš Docker Service Logs\n'));
|
|
25
|
+
|
|
26
|
+
if (service) {
|
|
27
|
+
console.log(chalk.gray(`Streaming logs for: ${service}\n`));
|
|
28
|
+
} else {
|
|
29
|
+
console.log(chalk.gray('Streaming logs for all services\n'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(chalk.yellow('Press Ctrl+C to stop\n'));
|
|
33
|
+
console.log(chalk.gray('ā'.repeat(80) + '\n'));
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const logsCommand = 'docker-compose';
|
|
37
|
+
const args = ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'logs', '-f'];
|
|
38
|
+
|
|
39
|
+
if (service) {
|
|
40
|
+
args.push(service);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Use spawn to stream output in real-time
|
|
44
|
+
const child = spawn(logsCommand, args, {
|
|
45
|
+
cwd: infrastructurePath,
|
|
46
|
+
stdio: 'inherit',
|
|
47
|
+
shell: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Handle Ctrl+C gracefully
|
|
51
|
+
process.on('SIGINT', () => {
|
|
52
|
+
console.log(chalk.yellow('\n\nā Stopped viewing logs\n'));
|
|
53
|
+
child.kill('SIGINT');
|
|
54
|
+
process.exit(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
child.on('exit', (code) => {
|
|
58
|
+
if (code !== 0 && code !== null) {
|
|
59
|
+
console.log(chalk.yellow(`\nā ļø Process exited with code ${code}\n`));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(chalk.red('\nā Error viewing logs:'), error.message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { dockerLogs };
|