baasix 0.1.0

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,1409 @@
1
+ import { existsSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import {
5
+ cancel,
6
+ confirm,
7
+ intro,
8
+ isCancel,
9
+ log,
10
+ outro,
11
+ select,
12
+ spinner,
13
+ text,
14
+ multiselect,
15
+ } from "@clack/prompts";
16
+ import chalk from "chalk";
17
+ import { Command } from "commander";
18
+ import crypto from "node:crypto";
19
+ import { detectPackageManager, installDependencies, type PackageManager } from "../utils/package-manager.js";
20
+
21
+ type ProjectTemplate = "api" | "nextjs" | "nextjs-app";
22
+
23
+ interface InitOptions {
24
+ cwd: string;
25
+ template?: ProjectTemplate;
26
+ name?: string;
27
+ yes?: boolean;
28
+ }
29
+
30
+ interface ProjectConfig {
31
+ projectName: string;
32
+ template: ProjectTemplate;
33
+ // Database
34
+ databaseUrl: string;
35
+ // Features
36
+ socketEnabled: boolean;
37
+ multiTenant: boolean;
38
+ publicRegistration: boolean;
39
+ // Storage
40
+ storageDriver: "LOCAL" | "S3";
41
+ s3Config?: {
42
+ endpoint: string;
43
+ bucket: string;
44
+ accessKey: string;
45
+ secretKey: string;
46
+ region: string;
47
+ };
48
+ // Cache
49
+ cacheAdapter: "memory" | "redis";
50
+ redisUrl?: string;
51
+ // Auth
52
+ authServices: string[];
53
+ // Mail
54
+ mailEnabled: boolean;
55
+ // OpenAPI
56
+ openApiEnabled: boolean;
57
+ }
58
+
59
+ function generateSecret(length: number = 64): string {
60
+ return crypto.randomBytes(length).toString("base64url").slice(0, length);
61
+ }
62
+
63
+ async function initAction(opts: InitOptions) {
64
+ const cwd = path.resolve(opts.cwd);
65
+
66
+ intro(chalk.bgCyan.black(" Baasix Project Setup "));
67
+
68
+ // Get project name
69
+ let projectName = opts.name;
70
+ if (!projectName) {
71
+ const result = await text({
72
+ message: "What is your project name?",
73
+ placeholder: "my-baasix-app",
74
+ defaultValue: "my-baasix-app",
75
+ validate: (value) => {
76
+ if (!value) return "Project name is required";
77
+ if (!/^[a-z0-9-_]+$/i.test(value)) return "Project name must be alphanumeric with dashes or underscores";
78
+ return undefined;
79
+ },
80
+ });
81
+
82
+ if (isCancel(result)) {
83
+ cancel("Operation cancelled");
84
+ process.exit(0);
85
+ }
86
+ projectName = result as string;
87
+ }
88
+
89
+ // Select template
90
+ let template = opts.template;
91
+ if (!template) {
92
+ const result = await select({
93
+ message: "Select a project template:",
94
+ options: [
95
+ {
96
+ value: "api",
97
+ label: "API Only",
98
+ hint: "Baasix server with basic configuration",
99
+ },
100
+ {
101
+ value: "nextjs-app",
102
+ label: "Next.js (App Router)",
103
+ hint: "Next.js 14+ with App Router and SDK integration",
104
+ },
105
+ {
106
+ value: "nextjs",
107
+ label: "Next.js (Pages Router)",
108
+ hint: "Next.js with Pages Router and SDK integration",
109
+ },
110
+ ],
111
+ });
112
+
113
+ if (isCancel(result)) {
114
+ cancel("Operation cancelled");
115
+ process.exit(0);
116
+ }
117
+ template = result as ProjectTemplate;
118
+ }
119
+
120
+ // Collect configuration options
121
+ const config = await collectProjectConfig(projectName, template, opts.yes);
122
+ if (!config) {
123
+ cancel("Operation cancelled");
124
+ process.exit(0);
125
+ }
126
+
127
+ const projectPath = path.join(cwd, projectName);
128
+
129
+ // Check if directory exists
130
+ if (existsSync(projectPath)) {
131
+ const overwrite = await confirm({
132
+ message: `Directory ${projectName} already exists. Overwrite?`,
133
+ initialValue: false,
134
+ });
135
+
136
+ if (isCancel(overwrite) || !overwrite) {
137
+ cancel("Operation cancelled");
138
+ process.exit(0);
139
+ }
140
+ }
141
+
142
+ const s = spinner();
143
+ s.start("Creating project structure...");
144
+
145
+ try {
146
+ // Create project directory
147
+ await fs.mkdir(projectPath, { recursive: true });
148
+
149
+ // Generate based on template
150
+ if (template === "api") {
151
+ await createApiProject(projectPath, config);
152
+ } else if (template === "nextjs-app" || template === "nextjs") {
153
+ await createNextJsProject(projectPath, config, template === "nextjs-app");
154
+ }
155
+
156
+ s.stop("Project structure created");
157
+
158
+ // Detect package manager
159
+ const packageManager = detectPackageManager(cwd);
160
+
161
+ // Install dependencies
162
+ const shouldInstall = opts.yes || await confirm({
163
+ message: `Install dependencies with ${packageManager}?`,
164
+ initialValue: true,
165
+ });
166
+
167
+ if (shouldInstall && !isCancel(shouldInstall)) {
168
+ s.start("Installing dependencies...");
169
+ try {
170
+ await installDependencies({
171
+ dependencies: [],
172
+ packageManager,
173
+ cwd: projectPath,
174
+ });
175
+ s.stop("Dependencies installed");
176
+ } catch (error) {
177
+ s.stop("Failed to install dependencies");
178
+ log.warn(`Run ${chalk.cyan(`cd ${projectName} && ${packageManager} install`)} to install manually`);
179
+ }
180
+ }
181
+
182
+ outro(chalk.green("✨ Project created successfully!"));
183
+
184
+ // Print next steps
185
+ console.log();
186
+ console.log(chalk.bold("Next steps:"));
187
+ console.log(` ${chalk.cyan(`cd ${projectName}`)}`);
188
+ if (template === "api") {
189
+ console.log(` ${chalk.cyan("# Review and update your .env file")}`);
190
+ console.log(` ${chalk.cyan(`${packageManager} run dev`)}`);
191
+ } else {
192
+ console.log(` ${chalk.cyan(`${packageManager} run dev`)} ${chalk.dim("# Start Next.js frontend")}`);
193
+ console.log();
194
+ console.log(chalk.dim(" Note: This is a frontend-only project. You need a separate Baasix API."));
195
+ console.log(chalk.dim(` To create an API: ${chalk.cyan("npx @tspvivek/baasix-cli init --template api")}`));
196
+ }
197
+ console.log();
198
+
199
+ } catch (error) {
200
+ s.stop("Failed to create project");
201
+ log.error(error instanceof Error ? error.message : "Unknown error");
202
+ process.exit(1);
203
+ }
204
+ }
205
+
206
+ async function collectProjectConfig(
207
+ projectName: string,
208
+ template: ProjectTemplate,
209
+ skipPrompts?: boolean
210
+ ): Promise<ProjectConfig | null> {
211
+ // If skipPrompts is true, return default configuration
212
+ if (skipPrompts) {
213
+ return {
214
+ projectName,
215
+ template,
216
+ databaseUrl: "postgresql://postgres:password@localhost:5432/baasix",
217
+ socketEnabled: false,
218
+ multiTenant: false,
219
+ publicRegistration: true,
220
+ storageDriver: "LOCAL",
221
+ s3Config: undefined,
222
+ cacheAdapter: "memory",
223
+ redisUrl: undefined,
224
+ authServices: ["LOCAL"],
225
+ mailEnabled: false,
226
+ openApiEnabled: true,
227
+ };
228
+ }
229
+
230
+ // Database URL
231
+ const dbUrl = await text({
232
+ message: "PostgreSQL connection URL:",
233
+ placeholder: "postgresql://postgres:password@localhost:5432/baasix",
234
+ defaultValue: "postgresql://postgres:password@localhost:5432/baasix",
235
+ });
236
+
237
+ if (isCancel(dbUrl)) return null;
238
+
239
+ // Multi-tenant
240
+ const multiTenant = await confirm({
241
+ message: "Enable multi-tenancy?",
242
+ initialValue: false,
243
+ });
244
+
245
+ if (isCancel(multiTenant)) return null;
246
+
247
+ // Public registration
248
+ const publicRegistration = await confirm({
249
+ message: "Allow public user registration?",
250
+ initialValue: true,
251
+ });
252
+
253
+ if (isCancel(publicRegistration)) return null;
254
+
255
+ // Real-time / Socket
256
+ const socketEnabled = await confirm({
257
+ message: "Enable real-time features (WebSocket)?",
258
+ initialValue: false,
259
+ });
260
+
261
+ if (isCancel(socketEnabled)) return null;
262
+
263
+ // Storage driver
264
+ const storageDriver = await select({
265
+ message: "Select storage driver:",
266
+ options: [
267
+ { value: "LOCAL", label: "Local Storage", hint: "Store files locally in uploads folder" },
268
+ { value: "S3", label: "S3 Compatible", hint: "AWS S3, DigitalOcean Spaces, MinIO, etc." },
269
+ ],
270
+ });
271
+
272
+ if (isCancel(storageDriver)) return null;
273
+
274
+ let s3Config: ProjectConfig["s3Config"];
275
+ if (storageDriver === "S3") {
276
+ const endpoint = await text({
277
+ message: "S3 endpoint:",
278
+ placeholder: "s3.amazonaws.com",
279
+ defaultValue: "s3.amazonaws.com",
280
+ });
281
+ if (isCancel(endpoint)) return null;
282
+
283
+ const bucket = await text({
284
+ message: "S3 bucket name:",
285
+ placeholder: "my-bucket",
286
+ });
287
+ if (isCancel(bucket)) return null;
288
+
289
+ const accessKey = await text({
290
+ message: "S3 Access Key ID:",
291
+ placeholder: "AKIAIOSFODNN7EXAMPLE",
292
+ });
293
+ if (isCancel(accessKey)) return null;
294
+
295
+ const secretKey = await text({
296
+ message: "S3 Secret Access Key:",
297
+ placeholder: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
298
+ });
299
+ if (isCancel(secretKey)) return null;
300
+
301
+ const region = await text({
302
+ message: "S3 Region:",
303
+ placeholder: "us-east-1",
304
+ defaultValue: "us-east-1",
305
+ });
306
+ if (isCancel(region)) return null;
307
+
308
+ s3Config = {
309
+ endpoint: endpoint as string,
310
+ bucket: bucket as string,
311
+ accessKey: accessKey as string,
312
+ secretKey: secretKey as string,
313
+ region: region as string,
314
+ };
315
+ }
316
+
317
+ // Cache adapter
318
+ const cacheAdapter = await select({
319
+ message: "Select cache adapter:",
320
+ options: [
321
+ { value: "memory", label: "In-Memory", hint: "Simple, good for development" },
322
+ { value: "redis", label: "Redis/Valkey", hint: "Recommended for production" },
323
+ ],
324
+ });
325
+
326
+ if (isCancel(cacheAdapter)) return null;
327
+
328
+ let redisUrl: string | undefined;
329
+ if (cacheAdapter === "redis") {
330
+ const url = await text({
331
+ message: "Redis connection URL:",
332
+ placeholder: "redis://localhost:6379",
333
+ defaultValue: "redis://localhost:6379",
334
+ });
335
+ if (isCancel(url)) return null;
336
+ redisUrl = url as string;
337
+ }
338
+
339
+ // Auth services
340
+ const authServices = await multiselect({
341
+ message: "Select authentication methods:",
342
+ options: [
343
+ { value: "LOCAL", label: "Email/Password", hint: "Built-in authentication" },
344
+ { value: "GOOGLE", label: "Google OAuth" },
345
+ { value: "FACEBOOK", label: "Facebook OAuth" },
346
+ { value: "GITHUB", label: "GitHub OAuth" },
347
+ { value: "APPLE", label: "Apple Sign In" },
348
+ ],
349
+ initialValues: ["LOCAL"],
350
+ required: true,
351
+ });
352
+
353
+ if (isCancel(authServices)) return null;
354
+
355
+ // OpenAPI
356
+ const openApiEnabled = await confirm({
357
+ message: "Enable OpenAPI documentation (Swagger)?",
358
+ initialValue: true,
359
+ });
360
+
361
+ if (isCancel(openApiEnabled)) return null;
362
+
363
+ // Mail (optional)
364
+ const mailEnabled = await confirm({
365
+ message: "Configure email sending?",
366
+ initialValue: false,
367
+ });
368
+
369
+ if (isCancel(mailEnabled)) return null;
370
+
371
+ return {
372
+ projectName,
373
+ template,
374
+ databaseUrl: dbUrl as string,
375
+ socketEnabled: socketEnabled as boolean,
376
+ multiTenant: multiTenant as boolean,
377
+ publicRegistration: publicRegistration as boolean,
378
+ storageDriver: storageDriver as "LOCAL" | "S3",
379
+ s3Config,
380
+ cacheAdapter: cacheAdapter as "memory" | "redis",
381
+ redisUrl,
382
+ authServices: authServices as string[],
383
+ mailEnabled: mailEnabled as boolean,
384
+ openApiEnabled: openApiEnabled as boolean,
385
+ };
386
+ }
387
+
388
+ async function createApiProject(projectPath: string, config: ProjectConfig) {
389
+ const secretKey = generateSecret(64);
390
+
391
+ // package.json
392
+ const packageJson = {
393
+ name: config.projectName,
394
+ version: "0.1.0",
395
+ type: "module",
396
+ scripts: {
397
+ dev: "node --watch server.js",
398
+ start: "node server.js",
399
+ },
400
+ dependencies: {
401
+ "@tspvivek/baasix": "latest",
402
+ "dotenv": "^16.3.1",
403
+ },
404
+ };
405
+
406
+ await fs.writeFile(
407
+ path.join(projectPath, "package.json"),
408
+ JSON.stringify(packageJson, null, 2)
409
+ );
410
+
411
+ // server.js
412
+ const serverJs = `import { startServer } from "@tspvivek/baasix";
413
+
414
+ startServer({
415
+ port: process.env.PORT || 8056,
416
+ logger: {
417
+ level: process.env.LOG_LEVEL || "info",
418
+ pretty: process.env.NODE_ENV !== "production",
419
+ },
420
+ }).catch((error) => {
421
+ console.error("Failed to start server:", error);
422
+ process.exit(1);
423
+ });
424
+ `;
425
+
426
+ await fs.writeFile(path.join(projectPath, "server.js"), serverJs);
427
+
428
+ // Generate .env file based on config
429
+ const envContent = generateEnvContent(config, secretKey);
430
+ await fs.writeFile(path.join(projectPath, ".env"), envContent);
431
+
432
+ // .env.example (sanitized version)
433
+ const envExample = generateEnvExample(config);
434
+ await fs.writeFile(path.join(projectPath, ".env.example"), envExample);
435
+
436
+ // .gitignore
437
+ const gitignore = `node_modules/
438
+ .env
439
+ uploads/
440
+ logs/
441
+ dist/
442
+ .cache/
443
+ .temp/
444
+ `;
445
+
446
+ await fs.writeFile(path.join(projectPath, ".gitignore"), gitignore);
447
+
448
+ // Create extensions directory
449
+ await fs.mkdir(path.join(projectPath, "extensions"), { recursive: true });
450
+ await fs.writeFile(
451
+ path.join(projectPath, "extensions", ".gitkeep"),
452
+ "# Place your Baasix extensions here\n"
453
+ );
454
+
455
+ // Create uploads directory (for local storage)
456
+ if (config.storageDriver === "LOCAL") {
457
+ await fs.mkdir(path.join(projectPath, "uploads"), { recursive: true });
458
+ await fs.writeFile(path.join(projectPath, "uploads", ".gitkeep"), "");
459
+ }
460
+
461
+ // Create migrations directory
462
+ await fs.mkdir(path.join(projectPath, "migrations"), { recursive: true });
463
+ await fs.writeFile(path.join(projectPath, "migrations", ".gitkeep"), "");
464
+
465
+ // README.md
466
+ const readme = generateReadme(config);
467
+ await fs.writeFile(path.join(projectPath, "README.md"), readme);
468
+ }
469
+
470
+ function generateEnvContent(config: ProjectConfig, secretKey: string): string {
471
+ const lines: string[] = [];
472
+
473
+ // Server section
474
+ lines.push("#-----------------------------------");
475
+ lines.push("# Server");
476
+ lines.push("#-----------------------------------");
477
+ lines.push("PORT=8056");
478
+ lines.push("HOST=localhost");
479
+ lines.push("NODE_ENV=development");
480
+ lines.push("DEBUGGING=false");
481
+ lines.push("");
482
+
483
+ // Database section
484
+ lines.push("#-----------------------------------");
485
+ lines.push("# Database");
486
+ lines.push("#-----------------------------------");
487
+ lines.push(`DATABASE_URL="${config.databaseUrl}"`);
488
+ lines.push("DATABASE_LOGGING=false");
489
+ lines.push("DATABASE_POOL_MAX=20");
490
+ lines.push("DATABASE_POOL_MIN=0");
491
+ lines.push("");
492
+
493
+ // Security section
494
+ lines.push("#-----------------------------------");
495
+ lines.push("# Security");
496
+ lines.push("#-----------------------------------");
497
+ lines.push(`SECRET_KEY=${secretKey}`);
498
+ lines.push("ACCESS_TOKEN_EXPIRES_IN=31536000");
499
+ lines.push("");
500
+
501
+ // Multi-tenancy section
502
+ lines.push("#-----------------------------------");
503
+ lines.push("# Multi-tenancy");
504
+ lines.push("#-----------------------------------");
505
+ lines.push(`MULTI_TENANT=${config.multiTenant}`);
506
+ lines.push(`PUBLIC_REGISTRATION=${config.publicRegistration}`);
507
+ if (!config.multiTenant) {
508
+ lines.push("DEFAULT_ROLE_REGISTERED=user");
509
+ }
510
+ lines.push("");
511
+
512
+ // Socket section
513
+ lines.push("#-----------------------------------");
514
+ lines.push("# Real-time (WebSocket)");
515
+ lines.push("#-----------------------------------");
516
+ lines.push(`SOCKET_ENABLED=${config.socketEnabled}`);
517
+ if (config.socketEnabled) {
518
+ lines.push('SOCKET_CORS_ENABLED_ORIGINS="http://localhost:3000,http://localhost:8056"');
519
+ lines.push("SOCKET_PATH=/realtime");
520
+ if (config.cacheAdapter === "redis" && config.redisUrl) {
521
+ lines.push("SOCKET_REDIS_ENABLED=true");
522
+ lines.push(`SOCKET_REDIS_URL=${config.redisUrl}`);
523
+ }
524
+ }
525
+ lines.push("");
526
+
527
+ // Cache section
528
+ lines.push("#-----------------------------------");
529
+ lines.push("# Cache");
530
+ lines.push("#-----------------------------------");
531
+ lines.push("CACHE_ENABLED=true");
532
+ lines.push(`CACHE_ADAPTER=${config.cacheAdapter}`);
533
+ lines.push("CACHE_TTL=300");
534
+ lines.push("CACHE_STRATEGY=explicit");
535
+ if (config.cacheAdapter === "memory") {
536
+ lines.push("CACHE_SIZE_GB=1");
537
+ } else if (config.cacheAdapter === "redis" && config.redisUrl) {
538
+ lines.push(`CACHE_REDIS_URL=${config.redisUrl}`);
539
+ }
540
+ lines.push("");
541
+
542
+ // Storage section
543
+ lines.push("#-----------------------------------");
544
+ lines.push("# Storage");
545
+ lines.push("#-----------------------------------");
546
+ if (config.storageDriver === "LOCAL") {
547
+ lines.push('STORAGE_SERVICES_ENABLED="LOCAL"');
548
+ lines.push('STORAGE_DEFAULT_SERVICE="LOCAL"');
549
+ lines.push("STORAGE_TEMP_PATH=./.temp");
550
+ lines.push("");
551
+ lines.push("# Local Storage");
552
+ lines.push("LOCAL_STORAGE_DRIVER=LOCAL");
553
+ lines.push('LOCAL_STORAGE_PATH="./uploads"');
554
+ } else if (config.storageDriver === "S3" && config.s3Config) {
555
+ lines.push('STORAGE_SERVICES_ENABLED="S3"');
556
+ lines.push('STORAGE_DEFAULT_SERVICE="S3"');
557
+ lines.push("STORAGE_TEMP_PATH=./.temp");
558
+ lines.push("");
559
+ lines.push("# S3 Compatible Storage");
560
+ lines.push("S3_STORAGE_DRIVER=S3");
561
+ lines.push(`S3_STORAGE_ENDPOINT=${config.s3Config.endpoint}`);
562
+ lines.push(`S3_STORAGE_BUCKET=${config.s3Config.bucket}`);
563
+ lines.push(`S3_STORAGE_ACCESS_KEY_ID=${config.s3Config.accessKey}`);
564
+ lines.push(`S3_STORAGE_SECRET_ACCESS_KEY=${config.s3Config.secretKey}`);
565
+ lines.push(`S3_STORAGE_REGION=${config.s3Config.region}`);
566
+ }
567
+ lines.push("");
568
+
569
+ // Auth section
570
+ lines.push("#-----------------------------------");
571
+ lines.push("# Authentication");
572
+ lines.push("#-----------------------------------");
573
+ lines.push(`AUTH_SERVICES_ENABLED=${config.authServices.join(",")}`);
574
+ lines.push('AUTH_APP_URL="http://localhost:3000,http://localhost:8056"');
575
+ lines.push("");
576
+
577
+ if (config.authServices.includes("GOOGLE")) {
578
+ lines.push("# Google OAuth");
579
+ lines.push("GOOGLE_CLIENT_ID=your_google_client_id");
580
+ lines.push("GOOGLE_CLIENT_SECRET=your_google_client_secret");
581
+ lines.push("");
582
+ }
583
+
584
+ if (config.authServices.includes("FACEBOOK")) {
585
+ lines.push("# Facebook OAuth");
586
+ lines.push("FACEBOOK_CLIENT_ID=your_facebook_client_id");
587
+ lines.push("FACEBOOK_CLIENT_SECRET=your_facebook_client_secret");
588
+ lines.push("");
589
+ }
590
+
591
+ if (config.authServices.includes("GITHUB")) {
592
+ lines.push("# GitHub OAuth");
593
+ lines.push("GITHUB_CLIENT_ID=your_github_client_id");
594
+ lines.push("GITHUB_CLIENT_SECRET=your_github_client_secret");
595
+ lines.push("");
596
+ }
597
+
598
+ if (config.authServices.includes("APPLE")) {
599
+ lines.push("# Apple Sign In");
600
+ lines.push("APPLE_CLIENT_ID=your_apple_client_id");
601
+ lines.push("APPLE_CLIENT_SECRET=your_apple_client_secret");
602
+ lines.push("APPLE_TEAM_ID=your_apple_team_id");
603
+ lines.push("APPLE_KEY_ID=your_apple_key_id");
604
+ lines.push("");
605
+ }
606
+
607
+ // CORS section
608
+ lines.push("#-----------------------------------");
609
+ lines.push("# CORS");
610
+ lines.push("#-----------------------------------");
611
+ lines.push('AUTH_CORS_ALLOWED_ORIGINS="http://localhost:3000,http://localhost:8056"');
612
+ lines.push("AUTH_CORS_ALLOW_ANY_PORT=true");
613
+ lines.push("AUTH_CORS_CREDENTIALS=true");
614
+ lines.push("");
615
+
616
+ // Cookies section
617
+ lines.push("#-----------------------------------");
618
+ lines.push("# Cookies");
619
+ lines.push("#-----------------------------------");
620
+ lines.push("AUTH_COOKIE_HTTP_ONLY=true");
621
+ lines.push("AUTH_COOKIE_SECURE=false");
622
+ lines.push("AUTH_COOKIE_SAME_SITE=lax");
623
+ lines.push("AUTH_COOKIE_PATH=/");
624
+ lines.push("");
625
+
626
+ // Mail section
627
+ if (config.mailEnabled) {
628
+ lines.push("#-----------------------------------");
629
+ lines.push("# Mail");
630
+ lines.push("#-----------------------------------");
631
+ lines.push('MAIL_SENDERS_ENABLED="SMTP"');
632
+ lines.push('MAIL_DEFAULT_SENDER="SMTP"');
633
+ lines.push("SEND_WELCOME_EMAIL=true");
634
+ lines.push("");
635
+ lines.push("# SMTP Configuration");
636
+ lines.push("SMTP_SMTP_HOST=smtp.example.com");
637
+ lines.push("SMTP_SMTP_PORT=587");
638
+ lines.push("SMTP_SMTP_SECURE=false");
639
+ lines.push("SMTP_SMTP_USER=your_smtp_user");
640
+ lines.push("SMTP_SMTP_PASS=your_smtp_password");
641
+ lines.push('SMTP_FROM_ADDRESS="Your App" <noreply@example.com>');
642
+ lines.push("");
643
+ }
644
+
645
+ // OpenAPI section
646
+ lines.push("#-----------------------------------");
647
+ lines.push("# OpenAPI Documentation");
648
+ lines.push("#-----------------------------------");
649
+ lines.push(`OPENAPI_ENABLED=${config.openApiEnabled}`);
650
+ if (config.openApiEnabled) {
651
+ lines.push("OPENAPI_INCLUDE_AUTH=true");
652
+ lines.push("OPENAPI_INCLUDE_SCHEMA=true");
653
+ lines.push("OPENAPI_INCLUDE_PERMISSIONS=true");
654
+ }
655
+ lines.push("");
656
+
657
+ return lines.join("\n");
658
+ }
659
+
660
+ function generateEnvExample(config: ProjectConfig): string {
661
+ const lines: string[] = [];
662
+
663
+ lines.push("# Database (PostgreSQL 14+ required)");
664
+ lines.push('DATABASE_URL="postgresql://username:password@localhost:5432/baasix"');
665
+ lines.push("");
666
+ lines.push("# Server");
667
+ lines.push("PORT=8056");
668
+ lines.push("NODE_ENV=development");
669
+ lines.push("");
670
+ lines.push("# Security (REQUIRED - generate unique keys)");
671
+ lines.push("SECRET_KEY=your-secret-key-minimum-32-characters-long");
672
+ lines.push("");
673
+ lines.push("# Features");
674
+ lines.push(`MULTI_TENANT=${config.multiTenant}`);
675
+ lines.push(`PUBLIC_REGISTRATION=${config.publicRegistration}`);
676
+ lines.push(`SOCKET_ENABLED=${config.socketEnabled}`);
677
+ lines.push("");
678
+ lines.push("# Storage");
679
+ lines.push(`STORAGE_DEFAULT_SERVICE="${config.storageDriver}"`);
680
+ if (config.storageDriver === "LOCAL") {
681
+ lines.push('LOCAL_STORAGE_PATH="./uploads"');
682
+ } else {
683
+ lines.push("S3_STORAGE_ENDPOINT=your-s3-endpoint");
684
+ lines.push("S3_STORAGE_BUCKET=your-bucket-name");
685
+ lines.push("S3_STORAGE_ACCESS_KEY_ID=your-access-key");
686
+ lines.push("S3_STORAGE_SECRET_ACCESS_KEY=your-secret-key");
687
+ }
688
+ lines.push("");
689
+ lines.push("# Cache");
690
+ lines.push(`CACHE_ADAPTER=${config.cacheAdapter}`);
691
+ if (config.cacheAdapter === "redis") {
692
+ lines.push("CACHE_REDIS_URL=redis://localhost:6379");
693
+ }
694
+ lines.push("");
695
+ lines.push("# Auth");
696
+ lines.push(`AUTH_SERVICES_ENABLED=${config.authServices.join(",")}`);
697
+ lines.push("");
698
+
699
+ return lines.join("\n");
700
+ }
701
+
702
+ function generateReadme(config: ProjectConfig): string {
703
+ return `# ${config.projectName}
704
+
705
+ A Baasix Backend-as-a-Service project.
706
+
707
+ ## Configuration
708
+
709
+ | Feature | Status |
710
+ |---------|--------|
711
+ | Multi-tenancy | ${config.multiTenant ? "✅ Enabled" : "❌ Disabled"} |
712
+ | Public Registration | ${config.publicRegistration ? "✅ Enabled" : "❌ Disabled"} |
713
+ | Real-time (WebSocket) | ${config.socketEnabled ? "✅ Enabled" : "❌ Disabled"} |
714
+ | Storage | ${config.storageDriver} |
715
+ | Cache | ${config.cacheAdapter} |
716
+ | Auth Methods | ${config.authServices.join(", ")} |
717
+ | OpenAPI Docs | ${config.openApiEnabled ? "✅ Enabled" : "❌ Disabled"} |
718
+
719
+ ## Getting Started
720
+
721
+ 1. **Configure your database**
722
+
723
+ Edit \`.env\` and verify your PostgreSQL connection:
724
+ \`\`\`
725
+ DATABASE_URL="postgresql://username:password@localhost:5432/baasix"
726
+ \`\`\`
727
+
728
+ 2. **Start the server**
729
+
730
+ \`\`\`bash
731
+ npm run dev
732
+ \`\`\`
733
+
734
+ 3. **Access the API**
735
+
736
+ - API: http://localhost:8056
737
+ - ${config.openApiEnabled ? "Swagger UI: http://localhost:8056/documentation" : "OpenAPI: Disabled"}
738
+ - Default admin: admin@baasix.com / admin@123
739
+
740
+ ## Project Structure
741
+
742
+ \`\`\`
743
+ ${config.projectName}/
744
+ ├── .env # Environment configuration
745
+ ├── .env.example # Example configuration
746
+ ├── package.json
747
+ ├── server.js # Server entry point
748
+ ├── extensions/ # Custom hooks and endpoints
749
+ ├── migrations/ # Database migrations
750
+ ${config.storageDriver === "LOCAL" ? "└── uploads/ # Local file storage" : ""}
751
+ \`\`\`
752
+
753
+ ## Extensions
754
+
755
+ Place your custom hooks and endpoints in the \`extensions/\` directory:
756
+
757
+ - **Endpoint extensions**: Add custom API routes
758
+ - **Hook extensions**: Add lifecycle hooks (before/after CRUD)
759
+
760
+ See [Extensions Documentation](https://baasix.com/docs/extensions) for details.
761
+
762
+ ## Migrations
763
+
764
+ \`\`\`bash
765
+ # Create a migration
766
+ npx baasix migrate create -n create_products_table
767
+
768
+ # Run migrations
769
+ npx baasix migrate run
770
+
771
+ # Check status
772
+ npx baasix migrate status
773
+ \`\`\`
774
+
775
+ ## Documentation
776
+
777
+ - [Baasix Documentation](https://baasix.com/docs)
778
+ - [SDK Guide](https://baasix.com/docs/sdk-guide)
779
+ - [API Reference](https://baasix.com/docs/api-reference)
780
+ `;
781
+ }
782
+
783
+ function generateNextJsEnvContent(config: ProjectConfig): string {
784
+ const lines: string[] = [];
785
+
786
+ // Next.js environment variables (frontend only)
787
+ lines.push("#-----------------------------------");
788
+ lines.push("# Baasix API Connection");
789
+ lines.push("#-----------------------------------");
790
+ lines.push("# URL of your Baasix API server");
791
+ lines.push("NEXT_PUBLIC_BAASIX_URL=http://localhost:8056");
792
+ lines.push("");
793
+ lines.push("# Note: Create a separate Baasix API project using:");
794
+ lines.push("# npx @tspvivek/baasix-cli init --template api");
795
+ lines.push("");
796
+
797
+ return lines.join("\n");
798
+ }
799
+
800
+ async function createNextJsProject(projectPath: string, config: ProjectConfig, useAppRouter: boolean) {
801
+ // package.json - Frontend only, no API dependencies
802
+ const packageJson = {
803
+ name: config.projectName,
804
+ version: "0.1.0",
805
+ private: true,
806
+ scripts: {
807
+ dev: "next dev",
808
+ build: "next build",
809
+ start: "next start",
810
+ lint: "next lint",
811
+ },
812
+ dependencies: {
813
+ "@tspvivek/baasix-sdk": "latest",
814
+ next: "^14.0.0",
815
+ react: "^18.2.0",
816
+ "react-dom": "^18.2.0",
817
+ },
818
+ devDependencies: {
819
+ "@types/node": "^20.0.0",
820
+ "@types/react": "^18.2.0",
821
+ "@types/react-dom": "^18.2.0",
822
+ typescript: "^5.0.0",
823
+ },
824
+ };
825
+
826
+ await fs.writeFile(
827
+ path.join(projectPath, "package.json"),
828
+ JSON.stringify(packageJson, null, 2)
829
+ );
830
+
831
+ // .env.local - Only frontend environment variables
832
+ const envContent = generateNextJsEnvContent(config);
833
+ await fs.writeFile(path.join(projectPath, ".env.local"), envContent);
834
+
835
+ // tsconfig.json
836
+ const tsconfig = {
837
+ compilerOptions: {
838
+ target: "es5",
839
+ lib: ["dom", "dom.iterable", "esnext"],
840
+ allowJs: true,
841
+ skipLibCheck: true,
842
+ strict: true,
843
+ noEmit: true,
844
+ esModuleInterop: true,
845
+ module: "esnext",
846
+ moduleResolution: "bundler",
847
+ resolveJsonModule: true,
848
+ isolatedModules: true,
849
+ jsx: "preserve",
850
+ incremental: true,
851
+ plugins: [{ name: "next" }],
852
+ paths: {
853
+ "@/*": [useAppRouter ? "./src/*" : "./*"],
854
+ },
855
+ },
856
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
857
+ exclude: ["node_modules"],
858
+ };
859
+
860
+ await fs.writeFile(
861
+ path.join(projectPath, "tsconfig.json"),
862
+ JSON.stringify(tsconfig, null, 2)
863
+ );
864
+
865
+ // next.config.mjs
866
+ const nextConfig = `/** @type {import('next').NextConfig} */
867
+ const nextConfig = {
868
+ reactStrictMode: true,
869
+ };
870
+
871
+ export default nextConfig;
872
+ `;
873
+
874
+ await fs.writeFile(path.join(projectPath, "next.config.mjs"), nextConfig);
875
+
876
+ if (useAppRouter) {
877
+ // App Router structure
878
+ await fs.mkdir(path.join(projectPath, "src", "app"), { recursive: true });
879
+ await fs.mkdir(path.join(projectPath, "src", "lib"), { recursive: true });
880
+
881
+ // src/lib/baasix.ts - SDK client
882
+ const baasixClient = `import { createBaasix } from "@tspvivek/baasix-sdk";
883
+
884
+ export const baasix = createBaasix({
885
+ url: process.env.NEXT_PUBLIC_BAASIX_URL || "http://localhost:8056",
886
+ authMode: "jwt",
887
+ autoRefresh: true,
888
+ });
889
+
890
+ // Re-export for convenience
891
+ export type { User, Role, QueryParams, Filter } from "@tspvivek/baasix-sdk";
892
+ `;
893
+
894
+ await fs.writeFile(path.join(projectPath, "src", "lib", "baasix.ts"), baasixClient);
895
+
896
+ // src/app/layout.tsx
897
+ const layout = `import type { Metadata } from "next";
898
+ import "./globals.css";
899
+
900
+ export const metadata: Metadata = {
901
+ title: "${config.projectName}",
902
+ description: "Built with Baasix",
903
+ };
904
+
905
+ export default function RootLayout({
906
+ children,
907
+ }: {
908
+ children: React.ReactNode;
909
+ }) {
910
+ return (
911
+ <html lang="en">
912
+ <body>{children}</body>
913
+ </html>
914
+ );
915
+ }
916
+ `;
917
+
918
+ await fs.writeFile(path.join(projectPath, "src", "app", "layout.tsx"), layout);
919
+
920
+ // src/app/globals.css
921
+ const globalsCss = `* {
922
+ box-sizing: border-box;
923
+ padding: 0;
924
+ margin: 0;
925
+ }
926
+
927
+ html,
928
+ body {
929
+ max-width: 100vw;
930
+ overflow-x: hidden;
931
+ font-family: system-ui, -apple-system, sans-serif;
932
+ }
933
+
934
+ body {
935
+ background: #0a0a0a;
936
+ color: #ededed;
937
+ }
938
+
939
+ a {
940
+ color: #0070f3;
941
+ text-decoration: none;
942
+ }
943
+
944
+ a:hover {
945
+ text-decoration: underline;
946
+ }
947
+ `;
948
+
949
+ await fs.writeFile(path.join(projectPath, "src", "app", "globals.css"), globalsCss);
950
+
951
+ // src/app/page.tsx
952
+ const page = `"use client";
953
+
954
+ import { useState, useEffect } from "react";
955
+ import { baasix, type User } from "@/lib/baasix";
956
+
957
+ export default function Home() {
958
+ const [user, setUser] = useState<User | null>(null);
959
+ const [loading, setLoading] = useState(true);
960
+ const [error, setError] = useState<string | null>(null);
961
+
962
+ useEffect(() => {
963
+ baasix.auth.getCachedUser().then((u) => {
964
+ setUser(u);
965
+ setLoading(false);
966
+ }).catch(() => {
967
+ setLoading(false);
968
+ });
969
+ }, []);
970
+
971
+ const handleLogin = async () => {
972
+ setError(null);
973
+ try {
974
+ const { user } = await baasix.auth.login({
975
+ email: "admin@baasix.com",
976
+ password: "admin@123",
977
+ });
978
+ setUser(user);
979
+ } catch (err) {
980
+ setError("Login failed. Make sure your Baasix API server is running.");
981
+ console.error("Login failed:", err);
982
+ }
983
+ };
984
+
985
+ const handleLogout = async () => {
986
+ await baasix.auth.logout();
987
+ setUser(null);
988
+ };
989
+
990
+ if (loading) {
991
+ return (
992
+ <main style={{ padding: "2rem", textAlign: "center" }}>
993
+ <p>Loading...</p>
994
+ </main>
995
+ );
996
+ }
997
+
998
+ return (
999
+ <main style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}>
1000
+ <h1 style={{ marginBottom: "1rem" }}>🚀 ${config.projectName}</h1>
1001
+ <p style={{ marginBottom: "2rem", color: "#888" }}>
1002
+ Next.js Frontend with Baasix SDK
1003
+ </p>
1004
+
1005
+ {error && (
1006
+ <div style={{ padding: "1rem", background: "#3a1a1a", borderRadius: "8px", marginBottom: "1rem", color: "#ff6b6b" }}>
1007
+ {error}
1008
+ </div>
1009
+ )}
1010
+
1011
+ {user ? (
1012
+ <div>
1013
+ <p style={{ marginBottom: "1rem" }}>
1014
+ Welcome, <strong>{user.email}</strong>!
1015
+ </p>
1016
+ <button
1017
+ onClick={handleLogout}
1018
+ style={{
1019
+ padding: "0.5rem 1rem",
1020
+ background: "#333",
1021
+ color: "#fff",
1022
+ border: "none",
1023
+ borderRadius: "4px",
1024
+ cursor: "pointer",
1025
+ }}
1026
+ >
1027
+ Logout
1028
+ </button>
1029
+ </div>
1030
+ ) : (
1031
+ <div>
1032
+ <p style={{ marginBottom: "1rem" }}>Not logged in</p>
1033
+ <button
1034
+ onClick={handleLogin}
1035
+ style={{
1036
+ padding: "0.5rem 1rem",
1037
+ background: "#0070f3",
1038
+ color: "#fff",
1039
+ border: "none",
1040
+ borderRadius: "4px",
1041
+ cursor: "pointer",
1042
+ }}
1043
+ >
1044
+ Login as Admin
1045
+ </button>
1046
+ </div>
1047
+ )}
1048
+
1049
+ <div style={{ marginTop: "3rem", padding: "1rem", background: "#111", borderRadius: "8px" }}>
1050
+ <h2 style={{ marginBottom: "0.5rem", fontSize: "1.2rem" }}>Getting Started</h2>
1051
+ <p style={{ marginBottom: "1rem", color: "#888", fontSize: "0.9rem" }}>
1052
+ This is a frontend-only Next.js app. You need a separate Baasix API server.
1053
+ </p>
1054
+ <ol style={{ paddingLeft: "1.5rem", lineHeight: "1.8" }}>
1055
+ <li>Create a Baasix API project: <code>npx @tspvivek/baasix-cli init --template api</code></li>
1056
+ <li>Start the API server: <code>cd your-api && npm run dev</code></li>
1057
+ <li>Update <code>.env.local</code> with your API URL if needed</li>
1058
+ <li>Start this Next.js app: <code>npm run dev</code></li>
1059
+ </ol>
1060
+ </div>
1061
+
1062
+ <div style={{ marginTop: "1.5rem", padding: "1rem", background: "#111", borderRadius: "8px" }}>
1063
+ <h2 style={{ marginBottom: "0.5rem", fontSize: "1.2rem" }}>API Connection</h2>
1064
+ <p style={{ color: "#888", fontSize: "0.9rem" }}>
1065
+ Currently configured to connect to: <code>{process.env.NEXT_PUBLIC_BAASIX_URL || "http://localhost:8056"}</code>
1066
+ </p>
1067
+ </div>
1068
+ </main>
1069
+ );
1070
+ }
1071
+ `;
1072
+
1073
+ await fs.writeFile(path.join(projectPath, "src", "app", "page.tsx"), page);
1074
+
1075
+ } else {
1076
+ // Pages Router structure
1077
+ await fs.mkdir(path.join(projectPath, "pages"), { recursive: true });
1078
+ await fs.mkdir(path.join(projectPath, "lib"), { recursive: true });
1079
+ await fs.mkdir(path.join(projectPath, "styles"), { recursive: true });
1080
+
1081
+ // lib/baasix.ts
1082
+ const baasixClient = `import { createBaasix } from "@tspvivek/baasix-sdk";
1083
+
1084
+ export const baasix = createBaasix({
1085
+ url: process.env.NEXT_PUBLIC_BAASIX_URL || "http://localhost:8056",
1086
+ authMode: "jwt",
1087
+ autoRefresh: true,
1088
+ });
1089
+
1090
+ export type { User, Role, QueryParams, Filter } from "@tspvivek/baasix-sdk";
1091
+ `;
1092
+
1093
+ await fs.writeFile(path.join(projectPath, "lib", "baasix.ts"), baasixClient);
1094
+
1095
+ // pages/_app.tsx
1096
+ const app = `import type { AppProps } from "next/app";
1097
+ import "@/styles/globals.css";
1098
+
1099
+ export default function App({ Component, pageProps }: AppProps) {
1100
+ return <Component {...pageProps} />;
1101
+ }
1102
+ `;
1103
+
1104
+ await fs.writeFile(path.join(projectPath, "pages", "_app.tsx"), app);
1105
+
1106
+ // styles/globals.css
1107
+ const globalsCss = `* {
1108
+ box-sizing: border-box;
1109
+ padding: 0;
1110
+ margin: 0;
1111
+ }
1112
+
1113
+ html,
1114
+ body {
1115
+ max-width: 100vw;
1116
+ overflow-x: hidden;
1117
+ font-family: system-ui, -apple-system, sans-serif;
1118
+ }
1119
+
1120
+ body {
1121
+ background: #0a0a0a;
1122
+ color: #ededed;
1123
+ }
1124
+
1125
+ a {
1126
+ color: #0070f3;
1127
+ text-decoration: none;
1128
+ }
1129
+
1130
+ a:hover {
1131
+ text-decoration: underline;
1132
+ }
1133
+ `;
1134
+
1135
+ await fs.writeFile(path.join(projectPath, "styles", "globals.css"), globalsCss);
1136
+
1137
+ // pages/index.tsx - Frontend only with SDK
1138
+ const index = `import { useState, useEffect } from "react";
1139
+ import { baasix, type User } from "@/lib/baasix";
1140
+
1141
+ export default function Home() {
1142
+ const [user, setUser] = useState<User | null>(null);
1143
+ const [loading, setLoading] = useState(true);
1144
+ const [error, setError] = useState<string | null>(null);
1145
+
1146
+ useEffect(() => {
1147
+ baasix.auth.getCachedUser().then((u) => {
1148
+ setUser(u);
1149
+ setLoading(false);
1150
+ }).catch(() => {
1151
+ setLoading(false);
1152
+ });
1153
+ }, []);
1154
+
1155
+ const handleLogin = async () => {
1156
+ setError(null);
1157
+ try {
1158
+ const { user } = await baasix.auth.login({
1159
+ email: "admin@baasix.com",
1160
+ password: "admin@123",
1161
+ });
1162
+ setUser(user);
1163
+ } catch (err) {
1164
+ setError("Login failed. Make sure your Baasix API server is running.");
1165
+ console.error("Login failed:", err);
1166
+ }
1167
+ };
1168
+
1169
+ const handleLogout = async () => {
1170
+ await baasix.auth.logout();
1171
+ setUser(null);
1172
+ };
1173
+
1174
+ if (loading) {
1175
+ return (
1176
+ <main style={{ padding: "2rem", textAlign: "center" }}>
1177
+ <p>Loading...</p>
1178
+ </main>
1179
+ );
1180
+ }
1181
+
1182
+ return (
1183
+ <main style={{ padding: "2rem", maxWidth: "800px", margin: "0 auto" }}>
1184
+ <h1 style={{ marginBottom: "1rem" }}>🚀 ${config.projectName}</h1>
1185
+ <p style={{ marginBottom: "2rem", color: "#888" }}>
1186
+ Next.js Frontend with Baasix SDK
1187
+ </p>
1188
+
1189
+ {error && (
1190
+ <div style={{ padding: "1rem", background: "#3a1a1a", borderRadius: "8px", marginBottom: "1rem", color: "#ff6b6b" }}>
1191
+ {error}
1192
+ </div>
1193
+ )}
1194
+
1195
+ {user ? (
1196
+ <div>
1197
+ <p style={{ marginBottom: "1rem" }}>
1198
+ Welcome, <strong>{user.email}</strong>!
1199
+ </p>
1200
+ <button
1201
+ onClick={handleLogout}
1202
+ style={{
1203
+ padding: "0.5rem 1rem",
1204
+ background: "#333",
1205
+ color: "#fff",
1206
+ border: "none",
1207
+ borderRadius: "4px",
1208
+ cursor: "pointer",
1209
+ }}
1210
+ >
1211
+ Logout
1212
+ </button>
1213
+ </div>
1214
+ ) : (
1215
+ <div>
1216
+ <p style={{ marginBottom: "1rem" }}>Not logged in</p>
1217
+ <button
1218
+ onClick={handleLogin}
1219
+ style={{
1220
+ padding: "0.5rem 1rem",
1221
+ background: "#0070f3",
1222
+ color: "#fff",
1223
+ border: "none",
1224
+ borderRadius: "4px",
1225
+ cursor: "pointer",
1226
+ }}
1227
+ >
1228
+ Login as Admin
1229
+ </button>
1230
+ </div>
1231
+ )}
1232
+
1233
+ <div style={{ marginTop: "3rem", padding: "1rem", background: "#111", borderRadius: "8px" }}>
1234
+ <h2 style={{ marginBottom: "0.5rem", fontSize: "1.2rem" }}>Getting Started</h2>
1235
+ <p style={{ marginBottom: "1rem", color: "#888", fontSize: "0.9rem" }}>
1236
+ This is a frontend-only Next.js app. You need a separate Baasix API server.
1237
+ </p>
1238
+ <ol style={{ paddingLeft: "1.5rem", lineHeight: "1.8" }}>
1239
+ <li>Create a Baasix API project: <code>npx @tspvivek/baasix-cli init --template api</code></li>
1240
+ <li>Start the API server: <code>cd your-api && npm run dev</code></li>
1241
+ <li>Update <code>.env.local</code> with your API URL if needed</li>
1242
+ <li>Start this Next.js app: <code>npm run dev</code></li>
1243
+ </ol>
1244
+ </div>
1245
+
1246
+ <div style={{ marginTop: "1.5rem", padding: "1rem", background: "#111", borderRadius: "8px" }}>
1247
+ <h2 style={{ marginBottom: "0.5rem", fontSize: "1.2rem" }}>API Connection</h2>
1248
+ <p style={{ color: "#888", fontSize: "0.9rem" }}>
1249
+ Currently configured to connect to: <code>{process.env.NEXT_PUBLIC_BAASIX_URL || "http://localhost:8056"}</code>
1250
+ </p>
1251
+ </div>
1252
+ </main>
1253
+ );
1254
+ }
1255
+ `;
1256
+
1257
+ await fs.writeFile(path.join(projectPath, "pages", "index.tsx"), index);
1258
+ }
1259
+
1260
+ // .gitignore - No api/ folder references since this is frontend-only
1261
+ const gitignore = `# Dependencies
1262
+ node_modules/
1263
+ .pnp
1264
+ .pnp.js
1265
+
1266
+ # Testing
1267
+ coverage/
1268
+
1269
+ # Next.js
1270
+ .next/
1271
+ out/
1272
+ build/
1273
+
1274
+ # Environment
1275
+ .env
1276
+ .env.local
1277
+ .env.development.local
1278
+ .env.test.local
1279
+ .env.production.local
1280
+
1281
+ # Misc
1282
+ .DS_Store
1283
+ *.pem
1284
+ npm-debug.log*
1285
+ yarn-debug.log*
1286
+ yarn-error.log*
1287
+
1288
+ # Vercel
1289
+ .vercel
1290
+
1291
+ # TypeScript
1292
+ *.tsbuildinfo
1293
+ next-env.d.ts
1294
+ `;
1295
+
1296
+ await fs.writeFile(path.join(projectPath, ".gitignore"), gitignore);
1297
+
1298
+ // README.md - Frontend-only documentation
1299
+ const readme = `# ${config.projectName}
1300
+
1301
+ A Next.js frontend project that connects to a Baasix API server using the SDK.
1302
+
1303
+ ## Architecture
1304
+
1305
+ This is a **frontend-only** project. You need a separate Baasix API server running.
1306
+
1307
+ \`\`\`
1308
+ ┌─────────────────┐ HTTP/WS ┌─────────────────┐
1309
+ │ Next.js App │ ◄──────────────► │ Baasix API │
1310
+ │ (Frontend) │ via SDK │ (Backend) │
1311
+ └─────────────────┘ └─────────────────┘
1312
+ Port 3000 Port 8056
1313
+ \`\`\`
1314
+
1315
+ ## Getting Started
1316
+
1317
+ ### 1. Start your Baasix API Server
1318
+
1319
+ If you don't have a Baasix API project yet, create one:
1320
+
1321
+ \`\`\`bash
1322
+ npx @tspvivek/baasix-cli init --template api my-api
1323
+ cd my-api
1324
+ npm install
1325
+ npm run dev
1326
+ \`\`\`
1327
+
1328
+ ### 2. Configure this Frontend
1329
+
1330
+ Update \`.env.local\` if your API is running on a different URL:
1331
+
1332
+ \`\`\`
1333
+ NEXT_PUBLIC_BAASIX_URL=http://localhost:8056
1334
+ \`\`\`
1335
+
1336
+ ### 3. Start the Frontend
1337
+
1338
+ \`\`\`bash
1339
+ npm install
1340
+ npm run dev
1341
+ \`\`\`
1342
+
1343
+ ### 4. Open your browser
1344
+
1345
+ - Frontend: http://localhost:3000
1346
+
1347
+ ## Default Admin Credentials
1348
+
1349
+ Use these credentials to login (configured in your API server):
1350
+
1351
+ - Email: admin@baasix.com
1352
+ - Password: admin@123
1353
+
1354
+ ## Project Structure
1355
+
1356
+ \`\`\`
1357
+ ${config.projectName}/
1358
+ ├── .env.local # API URL configuration
1359
+ ├── package.json
1360
+ ${useAppRouter ? `├── src/
1361
+ │ ├── app/ # Next.js App Router pages
1362
+ │ │ ├── layout.tsx
1363
+ │ │ ├── page.tsx
1364
+ │ │ └── globals.css
1365
+ │ └── lib/
1366
+ │ └── baasix.ts # SDK client` : `├── pages/ # Next.js Pages Router
1367
+ │ ├── _app.tsx
1368
+ │ └── index.tsx
1369
+ ├── lib/
1370
+ │ └── baasix.ts # SDK client
1371
+ └── styles/
1372
+ └── globals.css`}
1373
+ \`\`\`
1374
+
1375
+ ## SDK Usage
1376
+
1377
+ The SDK is pre-configured in \`${useAppRouter ? 'src/lib/baasix.ts' : 'lib/baasix.ts'}\`:
1378
+
1379
+ \`\`\`typescript
1380
+ import { baasix } from "${useAppRouter ? '@/lib/baasix' : '@/lib/baasix'}";
1381
+
1382
+ // Authentication
1383
+ const { user } = await baasix.auth.login({ email, password });
1384
+ await baasix.auth.logout();
1385
+
1386
+ // CRUD operations
1387
+ const items = await baasix.items("posts").list();
1388
+ const item = await baasix.items("posts").create({ title: "Hello" });
1389
+ await baasix.items("posts").update(id, { title: "Updated" });
1390
+ await baasix.items("posts").delete(id);
1391
+ \`\`\`
1392
+
1393
+ ## Documentation
1394
+
1395
+ - [Baasix Documentation](https://baasix.com/docs)
1396
+ - [SDK Guide](https://baasix.com/docs/sdk-guide)
1397
+ - [Next.js Documentation](https://nextjs.org/docs)
1398
+ `;
1399
+
1400
+ await fs.writeFile(path.join(projectPath, "README.md"), readme);
1401
+ }
1402
+
1403
+ export const init = new Command("init")
1404
+ .description("Initialize a new Baasix project")
1405
+ .option("-c, --cwd <path>", "Working directory", process.cwd())
1406
+ .option("-t, --template <template>", "Project template (api, nextjs, nextjs-app)")
1407
+ .option("-n, --name <name>", "Project name")
1408
+ .option("-y, --yes", "Skip confirmation prompts")
1409
+ .action(initAction);