launchbase 1.0.2 → 1.0.3

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.
Files changed (2) hide show
  1. package/bin/launchbase.js +363 -18
  2. package/package.json +1 -1
package/bin/launchbase.js CHANGED
@@ -6,7 +6,7 @@ const crypto = require('crypto');
6
6
  const fs = require('fs-extra');
7
7
  const { execSync, spawn } = require('child_process');
8
8
 
9
- const VERSION = '1.0.0';
9
+ const VERSION = '1.0.3';
10
10
  const program = new Command();
11
11
 
12
12
  function replaceInFile(filePath, replacements) {
@@ -52,12 +52,358 @@ function checkRenderCLI() {
52
52
  }
53
53
  }
54
54
 
55
+ function checkDocker() {
56
+ try {
57
+ execSync('docker --version', { stdio: 'pipe' });
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ function checkDockerRunning() {
65
+ try {
66
+ execSync('docker info', { stdio: 'pipe' });
67
+ return true;
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function getPlatform() {
74
+ return process.platform; // 'win32', 'darwin', 'linux'
75
+ }
76
+
77
+ async function installDocker() {
78
+ const platform = getPlatform();
79
+ console.log('\nšŸ“¦ Docker not found. Installing...\n');
80
+
81
+ if (platform === 'win32') {
82
+ console.log('šŸ“‹ Docker Desktop for Windows\n');
83
+ console.log('Option 1: Install via winget (recommended)');
84
+ console.log(' winget install Docker.DockerDesktop\n');
85
+ console.log('Option 2: Download manually');
86
+ console.log(' https://www.docker.com/products/docker-desktop\n');
87
+ console.log('After installation, restart your terminal and run the command again.\n');
88
+
89
+ // Try winget install
90
+ const useWinget = await askYesNo('Install via winget now?');
91
+ if (useWinget) {
92
+ console.log('\nā³ Installing Docker Desktop via winget...');
93
+ console.log(' (This may take a few minutes and require UAC prompt)\n');
94
+ try {
95
+ execSync('winget install Docker.DockerDesktop --accept-package-agreements --accept-source-agreements', {
96
+ stdio: 'inherit'
97
+ });
98
+ console.log('\nāœ… Docker Desktop installed!');
99
+ console.log('āš ļø Please restart Docker Desktop and run the command again.\n');
100
+ process.exit(0);
101
+ } catch (error) {
102
+ console.log('\nāŒ winget install failed. Please install manually from:');
103
+ console.log(' https://www.docker.com/products/docker-desktop\n');
104
+ process.exit(1);
105
+ }
106
+ }
107
+ } else if (platform === 'darwin') {
108
+ console.log('šŸ“‹ Docker Desktop for macOS\n');
109
+ console.log('Option 1: Install via Homebrew (recommended)');
110
+ console.log(' brew install --cask docker\n');
111
+ console.log('Option 2: Download manually');
112
+ console.log(' https://www.docker.com/products/docker-desktop\n');
113
+
114
+ // Try brew install
115
+ try {
116
+ execSync('brew --version', { stdio: 'pipe' });
117
+ const useBrew = await askYesNo('Install via Homebrew now?');
118
+ if (useBrew) {
119
+ console.log('\nā³ Installing Docker Desktop via Homebrew...\n');
120
+ execSync('brew install --cask docker', { stdio: 'inherit' });
121
+ console.log('\nāœ… Docker Desktop installed!');
122
+ console.log('āš ļø Open Docker Desktop and run the command again.\n');
123
+ process.exit(0);
124
+ }
125
+ } catch {
126
+ console.log('Homebrew not found. Install manually from:');
127
+ console.log(' https://www.docker.com/products/docker-desktop\n');
128
+ }
129
+ } else if (platform === 'linux') {
130
+ console.log('ļæ½ Docker for Linux\n');
131
+ console.log('Running official Docker install script...\n');
132
+ try {
133
+ execSync('curl -fsSL https://get.docker.com | sh', { stdio: 'inherit' });
134
+ console.log('\nāœ… Docker installed!');
135
+ console.log('āš ļø Run: sudo usermod -aG docker $USER && newgrp docker\n');
136
+ process.exit(0);
137
+ } catch {
138
+ console.log('\nāŒ Install failed. See: https://docs.docker.com/engine/install/\n');
139
+ process.exit(1);
140
+ }
141
+ }
142
+
143
+ process.exit(1);
144
+ }
145
+
146
+ async function askYesNo(question) {
147
+ const readline = require('readline');
148
+ const rl = readline.createInterface({
149
+ input: process.stdin,
150
+ output: process.stdout
151
+ });
152
+
153
+ return new Promise((resolve) => {
154
+ rl.question(`${question} (y/n): `, (answer) => {
155
+ rl.close();
156
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
157
+ });
158
+ });
159
+ }
160
+
161
+ async function ensureDocker() {
162
+ // Check if Docker is installed
163
+ if (!checkDocker()) {
164
+ await installDocker();
165
+ return false;
166
+ }
167
+
168
+ // Check if Docker daemon is running
169
+ if (!checkDockerRunning()) {
170
+ console.log('\nāš ļø Docker is installed but not running.\n');
171
+ console.log('Please start Docker Desktop and run the command again.\n');
172
+
173
+ const platform = getPlatform();
174
+ if (platform === 'win32' || platform === 'darwin') {
175
+ console.log('šŸ’” Tip: Open Docker Desktop from your applications.\n');
176
+ }
177
+
178
+ process.exit(1);
179
+ }
180
+
181
+ return true;
182
+ }
183
+
184
+ async function startDatabase(projectDir) {
185
+ const dockerComposePath = path.join(projectDir, 'docker-compose.yml');
186
+
187
+ if (!await fs.pathExists(dockerComposePath)) {
188
+ console.log('āš ļø No docker-compose.yml found, skipping database setup.\n');
189
+ return false;
190
+ }
191
+
192
+ console.log('🐳 Starting database with Docker Compose...\n');
193
+
194
+ try {
195
+ // Check if containers are already running
196
+ const psOutput = execSync('docker compose ps -q', {
197
+ cwd: projectDir,
198
+ stdio: 'pipe',
199
+ encoding: 'utf8'
200
+ }).trim();
201
+
202
+ if (psOutput) {
203
+ console.log('āœ… Database containers already running\n');
204
+ return true;
205
+ }
206
+ } catch {
207
+ // Containers not running, proceed to start
208
+ }
209
+
210
+ try {
211
+ execSync('docker compose up -d', {
212
+ cwd: projectDir,
213
+ stdio: 'inherit'
214
+ });
215
+
216
+ console.log('\nā³ Waiting for database to be ready...');
217
+
218
+ // Wait for database to be ready
219
+ let retries = 30;
220
+ while (retries > 0) {
221
+ try {
222
+ execSync('docker compose exec -T db pg_isready -U postgres', {
223
+ cwd: projectDir,
224
+ stdio: 'pipe'
225
+ });
226
+ console.log('āœ… Database is ready!\n');
227
+ return true;
228
+ } catch {
229
+ process.stdout.write('.');
230
+ await new Promise(r => setTimeout(r, 1000));
231
+ retries--;
232
+ }
233
+ }
234
+
235
+ console.log('\nāš ļø Database may not be ready yet. Check logs: docker compose logs\n');
236
+ return true;
237
+ } catch (error) {
238
+ console.log('\nāŒ Failed to start database. Check Docker logs.\n');
239
+ return false;
240
+ }
241
+ }
242
+
55
243
  program
56
244
  .name('launchbase')
57
- .description('šŸš€ Generate production-ready NestJS backends with authentication, multi-tenancy, billing, and deployment')
245
+ .description('�� Generate production-ready NestJS backends with authentication, multi-tenancy, billing, and deployment')
58
246
  .version(VERSION);
59
247
 
60
- // Default command: create
248
+ // New command - one command to create and start everything
249
+ program
250
+ .command('new')
251
+ .description('šŸš€ Create new project and start development (one command experience)')
252
+ .argument('<appName>', 'Project name')
253
+ .option('-t, --template', 'Include frontend React template')
254
+ .option('-s, --sdk', 'Include TypeScript SDK')
255
+ .option('--no-docker', 'Skip Docker/database setup')
256
+ .option('--no-cicd', 'Skip CI/CD workflow')
257
+ .action(async (appName, options) => {
258
+ console.log('\nšŸš€ LaunchBase CLI v' + VERSION + '\n');
259
+ console.log('šŸ“ Creating project:', appName);
260
+
261
+ const templateDir = path.resolve(__dirname, '..', 'template');
262
+ const targetDir = path.resolve(process.cwd(), appName);
263
+
264
+ // Check if directory exists
265
+ if (await fs.pathExists(targetDir)) {
266
+ console.error(`āŒ Target directory already exists: ${targetDir}`);
267
+ console.log(' Use a different name or remove the existing directory.');
268
+ process.exit(1);
269
+ }
270
+
271
+ // Copy template files with filtering
272
+ await fs.copy(templateDir, targetDir, {
273
+ filter: (src) => {
274
+ const relativePath = path.relative(templateDir, src);
275
+
276
+ // Skip node_modules, dist, etc.
277
+ if (relativePath.includes('node_modules') || relativePath.includes('dist') || relativePath.includes('.next')) {
278
+ return false;
279
+ }
280
+
281
+ // Skip frontend if not requested
282
+ if (relativePath.startsWith('frontend') && !options.template) {
283
+ return false;
284
+ }
285
+
286
+ // Skip SDK if not requested
287
+ if (relativePath.startsWith('sdk') && !options.sdk) {
288
+ return false;
289
+ }
290
+
291
+ // Skip Docker files if not requested
292
+ if (options.noDocker) {
293
+ if (relativePath.includes('Dockerfile') ||
294
+ relativePath.includes('docker-compose') ||
295
+ relativePath.includes('nginx.conf') ||
296
+ relativePath.includes('certbot')) {
297
+ return false;
298
+ }
299
+ }
300
+
301
+ // Skip CI/CD if not requested
302
+ if (options.noCicd && relativePath.startsWith('.github')) {
303
+ return false;
304
+ }
305
+
306
+ return true;
307
+ }
308
+ });
309
+
310
+ // Replace placeholders
311
+ const replacements = {
312
+ '__APP_NAME__': appName,
313
+ '"name": "launchbase-template"': `"name": "${appName}"`,
314
+ };
315
+
316
+ const filesToReplace = ['package.json', '.env.example', 'README.md'];
317
+ for (const rel of filesToReplace) {
318
+ const fp = path.join(targetDir, rel);
319
+ if (await fs.pathExists(fp)) {
320
+ replaceInFile(fp, replacements);
321
+ }
322
+ }
323
+
324
+ // Generate .env with secrets
325
+ const envExamplePath = path.join(targetDir, '.env.example');
326
+ const envPath = path.join(targetDir, '.env');
327
+
328
+ if (await fs.pathExists(envExamplePath)) {
329
+ let env = await fs.readFile(envExamplePath, 'utf8');
330
+ env = env.replace('JWT_ACCESS_SECRET=__CHANGE_ME__', `JWT_ACCESS_SECRET=${randomSecret(32)}`);
331
+ env = env.replace('JWT_REFRESH_SECRET=__CHANGE_ME__', `JWT_REFRESH_SECRET=${randomSecret(32)}`);
332
+ await fs.writeFile(envPath, env, 'utf8');
333
+ }
334
+
335
+ console.log('āœ… Project files created\n');
336
+
337
+ // Install dependencies
338
+ console.log('šŸ“¦ Installing dependencies...\n');
339
+ runCommand('npm install', { cwd: targetDir });
340
+
341
+ if (options.template) {
342
+ const frontendPath = path.join(targetDir, 'frontend');
343
+ if (await fs.pathExists(frontendPath)) {
344
+ console.log('\nšŸ“¦ Installing frontend dependencies...\n');
345
+ runCommand('npm install', { cwd: frontendPath });
346
+ }
347
+ }
348
+
349
+ // Setup Docker and database
350
+ if (!options.noDocker) {
351
+ console.log('\n🐳 Setting up database...\n');
352
+
353
+ if (await ensureDocker()) {
354
+ if (await startDatabase(targetDir)) {
355
+ // Wait a moment for DB to be fully ready
356
+ await new Promise(r => setTimeout(r, 2000));
357
+
358
+ // Run migrations
359
+ console.log('šŸ“„ Running database migrations...\n');
360
+ runCommand('npx prisma migrate dev --name init', { cwd: targetDir });
361
+ }
362
+ }
363
+ }
364
+
365
+ // Start dev server
366
+ console.log('\nšŸš€ Starting development server...\n');
367
+ console.log('━'.repeat(50));
368
+ console.log(' API: http://localhost:3000');
369
+ console.log(' Docs: http://localhost:3000/docs');
370
+ console.log(' Health: http://localhost:3000/health');
371
+ if (options.template) {
372
+ console.log(' Frontend: http://localhost:5173');
373
+ }
374
+ console.log('━'.repeat(50));
375
+ console.log('\nPress Ctrl+C to stop\n');
376
+
377
+ // Start backend
378
+ const backend = spawn('npm', ['run', 'start:dev'], {
379
+ cwd: targetDir,
380
+ stdio: 'inherit',
381
+ shell: true
382
+ });
383
+
384
+ // Start frontend after delay if included
385
+ if (options.template) {
386
+ const frontendPath = path.join(targetDir, 'frontend');
387
+ if (await fs.pathExists(frontendPath)) {
388
+ setTimeout(() => {
389
+ const frontend = spawn('npm', ['run', 'dev'], {
390
+ cwd: frontendPath,
391
+ stdio: 'inherit',
392
+ shell: true
393
+ });
394
+ }, 5000);
395
+ }
396
+ }
397
+
398
+ // Handle shutdown
399
+ process.on('SIGINT', () => {
400
+ console.log('\n\nšŸ‘‹ Shutting down...');
401
+ backend.kill();
402
+ process.exit(0);
403
+ });
404
+ });
405
+
406
+ // Default command: create (scaffold only)
61
407
  program
62
408
  .argument('[appName]', 'Destination folder name', 'my-app')
63
409
  .option('-t, --template', 'Include frontend React template')
@@ -173,7 +519,7 @@ program
173
519
  console.log('\n');
174
520
  });
175
521
 
176
- // Dev command - one command to run everything
522
+ // Dev command - start development environment (for existing projects)
177
523
  program
178
524
  .command('dev')
179
525
  .description('šŸš€ Start development environment (backend + frontend + db)')
@@ -184,16 +530,13 @@ program
184
530
  console.log('\nšŸš€ LaunchBase Dev\n');
185
531
 
186
532
  const projectDir = process.cwd();
187
- const { spawn } = require('child_process');
188
533
 
189
534
  // Check if this is a LaunchBase project
190
535
  const packageJsonPath = path.join(projectDir, 'package.json');
191
536
  if (!await fs.pathExists(packageJsonPath)) {
192
537
  console.error('āŒ No package.json found. Run this command from your project directory.');
193
538
  console.log('\nšŸ’” Create a new project first:');
194
- console.log(' npx launchbase my-app');
195
- console.log(' cd my-app');
196
- console.log(' npx launchbase dev\n');
539
+ console.log(' npx launchbase new my-app\n');
197
540
  process.exit(1);
198
541
  }
199
542
 
@@ -216,16 +559,18 @@ program
216
559
 
217
560
  // Setup database if not skipped
218
561
  if (!options.skipDb && !options.frontendOnly) {
219
- console.log('šŸ—„ļø Setting up database...\n');
220
-
221
- // Generate Prisma client
222
- runCommand('npx prisma generate', { cwd: projectDir, stdio: 'pipe' });
562
+ console.log('ļæ½ Setting up database...\n');
223
563
 
224
- // Push schema to database
225
- const dbResult = runCommand('npx prisma db push --accept-data-loss', { cwd: projectDir, stdio: 'pipe' });
226
-
227
- if (dbResult !== null) {
228
- console.log('āœ… Database ready\n');
564
+ // Ensure Docker is available
565
+ if (await ensureDocker()) {
566
+ if (await startDatabase(projectDir)) {
567
+ // Generate Prisma client
568
+ runCommand('npx prisma generate', { cwd: projectDir });
569
+
570
+ // Run migrations
571
+ console.log('\nšŸ“„ Running migrations...\n');
572
+ runCommand('npx prisma migrate dev', { cwd: projectDir });
573
+ }
229
574
  }
230
575
  }
231
576
 
@@ -234,7 +579,7 @@ program
234
579
 
235
580
  if (!options.frontendOnly) {
236
581
  // Start backend
237
- console.log('šŸ”§ Starting backend on http://localhost:3000');
582
+ console.log('\nšŸ”§ Starting backend on http://localhost:3000');
238
583
  const backend = spawn('npm', ['run', 'start:dev'], {
239
584
  cwd: projectDir,
240
585
  stdio: 'inherit',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchbase",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Generate production-ready NestJS backends with authentication, multi-tenancy, billing, and deployment in minutes",
5
5
  "author": "LaunchBase",
6
6
  "keywords": [