@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.
@@ -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 };