@launchframe/cli 0.1.10 → 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 CHANGED
@@ -4,14 +4,7 @@
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
- ![LaunchFrame CLI Preview](cli.png)
7
+ ![LaunchFrame CLI Preview](https://unpkg.com/@launchframe/cli@latest/cli.png)
15
8
 
16
9
  ## What You Get
17
10
 
@@ -30,14 +23,39 @@ Here's a sneak peek of the CLI experience:
30
23
  ## Installation
31
24
 
32
25
  ```bash
33
- npx @launchframe/cli init
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
- ## Early Access
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
- LaunchFrame is currently in **private beta**.
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
- Join the waitlist at **[launchframe.dev](https://launchframe.dev)** to get early access, exclusive updates, and founding member perks.
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.10",
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": "./src/index.js"
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 - clone repo and configure environment
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
- await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath);
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: Check repository privacy
141
- console.log(chalk.yellow('šŸ” Step 4: Checking repository access...\n'));
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 repoCheck = await checkRepoPrivacy(githubOrg, projectName);
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
- cloneSpinner.text = 'Cloning repository...';
161
-
162
- // Check if directory is empty or has .git
163
- const { stdout: lsOutput } = await executeSSH(vpsUser, vpsHost, `ls -A ${vpsAppFolder}`);
164
-
165
- if (lsOutput.trim()) {
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
- cloneSpinner.succeed('Repository setup complete');
154
+ setupSpinner.succeed('Infrastructure files copied to VPS');
189
155
  } catch (error) {
190
- cloneSpinner.fail('Failed to setup repository');
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 6: Copy .env.prod to VPS
227
- console.log(chalk.yellow('\nšŸ“„ Step 6: Copying production environment...\n'));
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 7: Pull Docker images
242
- console.log(chalk.yellow('\n🐳 Step 7: Pulling Docker images...\n'));
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(` - Repository cloned to: ${vpsAppFolder}`));
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
- 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
- ]);
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
- 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
- }
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 and remove all containers
51
- console.log(chalk.gray('Stopping and removing containers...'));
50
+ // Step 1: Stop all running containers first
51
+ console.log(chalk.gray('Stopping running containers...'));
52
52
  try {
53
- execSync(`docker ps -a --filter "name=${projectName}" -q | xargs -r docker rm -f`, { stdio: 'inherit' });
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 2: Remove all volumes
59
- console.log(chalk.gray('Removing volumes...'));
77
+ // Step 3: Remove network (must be after containers are removed)
78
+ console.log(chalk.gray('Removing network...'));
60
79
  try {
61
- execSync(`docker volume ls --filter "name=${projectName}" -q | xargs -r docker volume rm`, { stdio: 'inherit' });
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 no volumes found
83
+ // Ignore errors if network doesn't exist or has active endpoints
64
84
  }
65
85
 
66
- // Step 3: Remove all images
67
- console.log(chalk.gray('Removing images...'));
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 images --filter "reference=${projectName}*" -q | xargs -r docker rmi -f`, { stdio: 'inherit' });
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 images found
96
+ // Ignore errors if no volumes found
72
97
  }
73
98
 
74
- // Step 4: Remove network
75
- console.log(chalk.gray('Removing network...'));
99
+ // Step 5: Remove all images (do this last)
100
+ console.log(chalk.gray('Removing images...'));
76
101
  try {
77
- execSync(`docker network rm ${projectName}-network`, { stdio: 'inherit' });
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 network doesn't exist
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'));