launchbase 1.0.2 → 1.0.4

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/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.4';
10
10
  const program = new Command();
11
11
 
12
12
  function replaceInFile(filePath, replacements) {
@@ -52,12 +52,431 @@ 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
+ const platform = getPlatform();
171
+
172
+ console.log('\nāš ļø Docker is installed but not running.');
173
+
174
+ // Try to start Docker Desktop automatically on Windows/macOS
175
+ if (platform === 'win32') {
176
+ console.log('šŸ”„ Attempting to start Docker Desktop...\n');
177
+ try {
178
+ // Try to start Docker Desktop via PowerShell
179
+ execSync('Start-Process "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"', {
180
+ shell: 'powershell.exe',
181
+ stdio: 'pipe'
182
+ });
183
+
184
+ console.log('ā³ Waiting for Docker to start');
185
+
186
+ // Wait for Docker to be ready (up to 60 seconds)
187
+ let retries = 60;
188
+ while (retries > 0) {
189
+ await new Promise(r => setTimeout(r, 1000));
190
+ try {
191
+ execSync('docker info', { stdio: 'pipe' });
192
+ console.log('\nāœ… Docker is now running!\n');
193
+ return true;
194
+ } catch {
195
+ process.stdout.write('.');
196
+ retries--;
197
+ }
198
+ }
199
+
200
+ console.log('\n\nāš ļø Docker did not start in time. Please start Docker Desktop manually.\n');
201
+ process.exit(1);
202
+ } catch (error) {
203
+ console.log('āŒ Could not start Docker Desktop automatically.\n');
204
+ console.log('šŸ’” Please start Docker Desktop from your Start menu and run the command again.\n');
205
+ process.exit(1);
206
+ }
207
+ } else if (platform === 'darwin') {
208
+ console.log('šŸ”„ Attempting to start Docker Desktop...\n');
209
+ try {
210
+ execSync('open -a Docker', { stdio: 'pipe' });
211
+
212
+ console.log('ā³ Waiting for Docker to start');
213
+
214
+ // Wait for Docker to be ready (up to 60 seconds)
215
+ let retries = 60;
216
+ while (retries > 0) {
217
+ await new Promise(r => setTimeout(r, 1000));
218
+ try {
219
+ execSync('docker info', { stdio: 'pipe' });
220
+ console.log('\nāœ… Docker is now running!\n');
221
+ return true;
222
+ } catch {
223
+ process.stdout.write('.');
224
+ retries--;
225
+ }
226
+ }
227
+
228
+ console.log('\n\nāš ļø Docker did not start in time. Please start Docker Desktop manually.\n');
229
+ process.exit(1);
230
+ } catch (error) {
231
+ console.log('āŒ Could not start Docker Desktop automatically.\n');
232
+ console.log('šŸ’” Please start Docker Desktop from your Applications and run the command again.\n');
233
+ process.exit(1);
234
+ }
235
+ } else {
236
+ // Linux - try systemctl
237
+ console.log('šŸ”„ Attempting to start Docker daemon...\n');
238
+ try {
239
+ execSync('sudo systemctl start docker', { stdio: 'inherit' });
240
+
241
+ // Wait briefly and check
242
+ await new Promise(r => setTimeout(r, 2000));
243
+ if (checkDockerRunning()) {
244
+ console.log('āœ… Docker is now running!\n');
245
+ return true;
246
+ }
247
+ } catch {}
248
+
249
+ console.log('šŸ’” Please start Docker manually and run the command again.\n');
250
+ process.exit(1);
251
+ }
252
+ }
253
+
254
+ return true;
255
+ }
256
+
257
+ async function startDatabase(projectDir) {
258
+ const dockerComposePath = path.join(projectDir, 'docker-compose.yml');
259
+
260
+ if (!await fs.pathExists(dockerComposePath)) {
261
+ console.log('āš ļø No docker-compose.yml found, skipping database setup.\n');
262
+ return false;
263
+ }
264
+
265
+ console.log('🐳 Starting database with Docker Compose...\n');
266
+
267
+ try {
268
+ // Check if containers are already running
269
+ const psOutput = execSync('docker compose ps -q', {
270
+ cwd: projectDir,
271
+ stdio: 'pipe',
272
+ encoding: 'utf8'
273
+ }).trim();
274
+
275
+ if (psOutput) {
276
+ console.log('āœ… Database containers already running\n');
277
+ return true;
278
+ }
279
+ } catch {
280
+ // Containers not running, proceed to start
281
+ }
282
+
283
+ try {
284
+ execSync('docker compose up -d', {
285
+ cwd: projectDir,
286
+ stdio: 'inherit'
287
+ });
288
+
289
+ console.log('\nā³ Waiting for database to be ready...');
290
+
291
+ // Wait for database to be ready
292
+ let retries = 30;
293
+ while (retries > 0) {
294
+ try {
295
+ execSync('docker compose exec -T db pg_isready -U postgres', {
296
+ cwd: projectDir,
297
+ stdio: 'pipe'
298
+ });
299
+ console.log('āœ… Database is ready!\n');
300
+ return true;
301
+ } catch {
302
+ process.stdout.write('.');
303
+ await new Promise(r => setTimeout(r, 1000));
304
+ retries--;
305
+ }
306
+ }
307
+
308
+ console.log('\nāš ļø Database may not be ready yet. Check logs: docker compose logs\n');
309
+ return true;
310
+ } catch (error) {
311
+ console.log('\nāŒ Failed to start database. Check Docker logs.\n');
312
+ return false;
313
+ }
314
+ }
315
+
55
316
  program
56
317
  .name('launchbase')
57
- .description('šŸš€ Generate production-ready NestJS backends with authentication, multi-tenancy, billing, and deployment')
318
+ .description('�� Generate production-ready NestJS backends with authentication, multi-tenancy, billing, and deployment')
58
319
  .version(VERSION);
59
320
 
60
- // Default command: create
321
+ // New command - one command to create and start everything
322
+ program
323
+ .command('new')
324
+ .description('šŸš€ Create new project and start development (one command experience)')
325
+ .argument('<appName>', 'Project name')
326
+ .option('-t, --template', 'Include frontend React template')
327
+ .option('-s, --sdk', 'Include TypeScript SDK')
328
+ .option('--no-docker', 'Skip Docker/database setup')
329
+ .option('--no-cicd', 'Skip CI/CD workflow')
330
+ .action(async (appName, options) => {
331
+ console.log('\nšŸš€ LaunchBase CLI v' + VERSION + '\n');
332
+ console.log('šŸ“ Creating project:', appName);
333
+
334
+ const templateDir = path.resolve(__dirname, '..', 'template');
335
+ const targetDir = path.resolve(process.cwd(), appName);
336
+
337
+ // Check if directory exists
338
+ if (await fs.pathExists(targetDir)) {
339
+ console.error(`āŒ Target directory already exists: ${targetDir}`);
340
+ console.log(' Use a different name or remove the existing directory.');
341
+ process.exit(1);
342
+ }
343
+
344
+ // Copy template files with filtering
345
+ await fs.copy(templateDir, targetDir, {
346
+ filter: (src) => {
347
+ const relativePath = path.relative(templateDir, src);
348
+
349
+ // Skip node_modules, dist, etc.
350
+ if (relativePath.includes('node_modules') || relativePath.includes('dist') || relativePath.includes('.next')) {
351
+ return false;
352
+ }
353
+
354
+ // Skip frontend if not requested
355
+ if (relativePath.startsWith('frontend') && !options.template) {
356
+ return false;
357
+ }
358
+
359
+ // Skip SDK if not requested
360
+ if (relativePath.startsWith('sdk') && !options.sdk) {
361
+ return false;
362
+ }
363
+
364
+ // Skip Docker files if not requested
365
+ if (options.noDocker) {
366
+ if (relativePath.includes('Dockerfile') ||
367
+ relativePath.includes('docker-compose') ||
368
+ relativePath.includes('nginx.conf') ||
369
+ relativePath.includes('certbot')) {
370
+ return false;
371
+ }
372
+ }
373
+
374
+ // Skip CI/CD if not requested
375
+ if (options.noCicd && relativePath.startsWith('.github')) {
376
+ return false;
377
+ }
378
+
379
+ return true;
380
+ }
381
+ });
382
+
383
+ // Replace placeholders
384
+ const replacements = {
385
+ '__APP_NAME__': appName,
386
+ '"name": "launchbase-template"': `"name": "${appName}"`,
387
+ };
388
+
389
+ const filesToReplace = ['package.json', '.env.example', 'README.md'];
390
+ for (const rel of filesToReplace) {
391
+ const fp = path.join(targetDir, rel);
392
+ if (await fs.pathExists(fp)) {
393
+ replaceInFile(fp, replacements);
394
+ }
395
+ }
396
+
397
+ // Generate .env with secrets
398
+ const envExamplePath = path.join(targetDir, '.env.example');
399
+ const envPath = path.join(targetDir, '.env');
400
+
401
+ if (await fs.pathExists(envExamplePath)) {
402
+ let env = await fs.readFile(envExamplePath, 'utf8');
403
+ env = env.replace('JWT_ACCESS_SECRET=__CHANGE_ME__', `JWT_ACCESS_SECRET=${randomSecret(32)}`);
404
+ env = env.replace('JWT_REFRESH_SECRET=__CHANGE_ME__', `JWT_REFRESH_SECRET=${randomSecret(32)}`);
405
+ await fs.writeFile(envPath, env, 'utf8');
406
+ }
407
+
408
+ console.log('āœ… Project files created\n');
409
+
410
+ // Install dependencies
411
+ console.log('šŸ“¦ Installing dependencies...\n');
412
+ runCommand('npm install', { cwd: targetDir });
413
+
414
+ if (options.template) {
415
+ const frontendPath = path.join(targetDir, 'frontend');
416
+ if (await fs.pathExists(frontendPath)) {
417
+ console.log('\nšŸ“¦ Installing frontend dependencies...\n');
418
+ runCommand('npm install', { cwd: frontendPath });
419
+ }
420
+ }
421
+
422
+ // Setup Docker and database
423
+ if (!options.noDocker) {
424
+ console.log('\n🐳 Setting up database...\n');
425
+
426
+ if (await ensureDocker()) {
427
+ if (await startDatabase(targetDir)) {
428
+ // Wait a moment for DB to be fully ready
429
+ await new Promise(r => setTimeout(r, 2000));
430
+
431
+ // Run migrations
432
+ console.log('šŸ“„ Running database migrations...\n');
433
+ runCommand('npx prisma migrate dev --name init', { cwd: targetDir });
434
+ }
435
+ }
436
+ }
437
+
438
+ // Start dev server
439
+ console.log('\nšŸš€ Starting development server...\n');
440
+ console.log('━'.repeat(50));
441
+ console.log(' API: http://localhost:3000');
442
+ console.log(' Docs: http://localhost:3000/docs');
443
+ console.log(' Health: http://localhost:3000/health');
444
+ if (options.template) {
445
+ console.log(' Frontend: http://localhost:5173');
446
+ }
447
+ console.log('━'.repeat(50));
448
+ console.log('\nPress Ctrl+C to stop\n');
449
+
450
+ // Start backend
451
+ const backend = spawn('npm', ['run', 'start:dev'], {
452
+ cwd: targetDir,
453
+ stdio: 'inherit',
454
+ shell: true
455
+ });
456
+
457
+ // Start frontend after delay if included
458
+ if (options.template) {
459
+ const frontendPath = path.join(targetDir, 'frontend');
460
+ if (await fs.pathExists(frontendPath)) {
461
+ setTimeout(() => {
462
+ const frontend = spawn('npm', ['run', 'dev'], {
463
+ cwd: frontendPath,
464
+ stdio: 'inherit',
465
+ shell: true
466
+ });
467
+ }, 5000);
468
+ }
469
+ }
470
+
471
+ // Handle shutdown
472
+ process.on('SIGINT', () => {
473
+ console.log('\n\nšŸ‘‹ Shutting down...');
474
+ backend.kill();
475
+ process.exit(0);
476
+ });
477
+ });
478
+
479
+ // Default command: create (scaffold only)
61
480
  program
62
481
  .argument('[appName]', 'Destination folder name', 'my-app')
63
482
  .option('-t, --template', 'Include frontend React template')
@@ -173,7 +592,7 @@ program
173
592
  console.log('\n');
174
593
  });
175
594
 
176
- // Dev command - one command to run everything
595
+ // Dev command - start development environment (for existing projects)
177
596
  program
178
597
  .command('dev')
179
598
  .description('šŸš€ Start development environment (backend + frontend + db)')
@@ -184,16 +603,13 @@ program
184
603
  console.log('\nšŸš€ LaunchBase Dev\n');
185
604
 
186
605
  const projectDir = process.cwd();
187
- const { spawn } = require('child_process');
188
606
 
189
607
  // Check if this is a LaunchBase project
190
608
  const packageJsonPath = path.join(projectDir, 'package.json');
191
609
  if (!await fs.pathExists(packageJsonPath)) {
192
610
  console.error('āŒ No package.json found. Run this command from your project directory.');
193
611
  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');
612
+ console.log(' npx launchbase new my-app\n');
197
613
  process.exit(1);
198
614
  }
199
615
 
@@ -216,16 +632,18 @@ program
216
632
 
217
633
  // Setup database if not skipped
218
634
  if (!options.skipDb && !options.frontendOnly) {
219
- console.log('šŸ—„ļø Setting up database...\n');
635
+ console.log('ļæ½ Setting up database...\n');
220
636
 
221
- // Generate Prisma client
222
- runCommand('npx prisma generate', { cwd: projectDir, stdio: 'pipe' });
223
-
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');
637
+ // Ensure Docker is available
638
+ if (await ensureDocker()) {
639
+ if (await startDatabase(projectDir)) {
640
+ // Generate Prisma client
641
+ runCommand('npx prisma generate', { cwd: projectDir });
642
+
643
+ // Run migrations
644
+ console.log('\nšŸ“„ Running migrations...\n');
645
+ runCommand('npx prisma migrate dev', { cwd: projectDir });
646
+ }
229
647
  }
230
648
  }
231
649
 
@@ -234,7 +652,7 @@ program
234
652
 
235
653
  if (!options.frontendOnly) {
236
654
  // Start backend
237
- console.log('šŸ”§ Starting backend on http://localhost:3000');
655
+ console.log('\nšŸ”§ Starting backend on http://localhost:3000');
238
656
  const backend = spawn('npm', ['run', 'start:dev'], {
239
657
  cwd: projectDir,
240
658
  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.4",
4
4
  "description": "Generate production-ready NestJS backends with authentication, multi-tenancy, billing, and deployment in minutes",
5
5
  "author": "LaunchBase",
6
6
  "keywords": [
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "license": "UNLICENSED",
6
6
  "scripts": {
7
- "build": "tsc -p tsconfig.build.json",
7
+ "build": "prisma generate && tsc -p tsconfig.build.json",
8
8
  "start": "node dist/src/main.js",
9
9
  "start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
10
10
  "prisma:generate": "prisma generate",
@@ -4,7 +4,7 @@ import LaunchBase from '@launchbasex/sdk'
4
4
 
5
5
  @Injectable()
6
6
  export class LaunchBaseService implements OnModuleInit {
7
- private client: LaunchBase
7
+ private client!: LaunchBase // Definite assignment assertion - initialized in onModuleInit
8
8
 
9
9
  constructor(private configService: ConfigService) {}
10
10