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.
- package/bin/launchbase.js +363 -18
- 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.
|
|
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('
|
|
245
|
+
.description('�� Generate production-ready NestJS backends with authentication, multi-tenancy, billing, and deployment')
|
|
58
246
|
.version(VERSION);
|
|
59
247
|
|
|
60
|
-
//
|
|
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 -
|
|
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('
|
|
220
|
-
|
|
221
|
-
// Generate Prisma client
|
|
222
|
-
runCommand('npx prisma generate', { cwd: projectDir, stdio: 'pipe' });
|
|
562
|
+
console.log('ļæ½ Setting up database...\n');
|
|
223
563
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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