forgestack-os-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,16 +31,26 @@ CMD ["nginx", "-g", "daemon off;"]
31
31
  `;
32
32
  await fs_extra_1.default.writeFile(path_1.default.join(dockerDir, 'frontend.Dockerfile'), frontendDockerfile);
33
33
  // Backend Dockerfile
34
- const backendDockerfile = `FROM node:20-alpine
34
+ const backendDockerfile = `FROM node:20-alpine AS builder
35
35
 
36
36
  WORKDIR /app
37
37
 
38
38
  COPY package*.json ./
39
- RUN npm install --only=production
39
+ RUN npm install
40
40
 
41
41
  COPY . .
42
42
  RUN npm run build
43
43
 
44
+ FROM node:20-alpine AS runner
45
+
46
+ WORKDIR /app
47
+
48
+ COPY --from=builder /app/package*.json ./
49
+ RUN npm install --only=production
50
+
51
+ COPY --from=builder /app/dist ./dist
52
+ ${config.database !== 'mongodb' ? 'COPY --from=builder /app/prisma ./prisma\nRUN npx prisma generate' : ''}
53
+
44
54
  EXPOSE 3000
45
55
 
46
56
  CMD ["node", "dist/index.js"]
@@ -111,8 +121,8 @@ services:
111
121
  environment:
112
122
  - NODE_ENV=production
113
123
  - PORT=3000
114
- - DATABASE_URL=${config.database === 'postgresql' ? `postgresql://postgres:postgres@postgres:5432/${config.projectName}` : config.database === 'mongodb' ? `mongodb://mongo:mongo@mongodb:27017/${config.projectName}` : config.database === 'mysql' ? `mysql://root:mysql@mysql:3306/${config.projectName}` : '${DATABASE_URL}'}
115
- - JWT_SECRET=\${JWT_SECRET}
124
+ - DATABASE_URL=${config.database === 'postgresql' ? `postgresql://postgres:postgres@postgres:5432/${config.projectName}` : config.database === 'mongodb' ? `mongodb://mongo:mongo@mongodb:27017/${config.projectName}` : config.database === 'mysql' ? `mysql://root:mysql@mysql:3306/${config.projectName}` : 'sqlite:./dev.db'}
125
+ - JWT_SECRET=\${JWT_SECRET:-your-secret-key}
116
126
  depends_on:
117
127
  `;
118
128
  // Add database service
@@ -1 +1 @@
1
- {"version":3,"file":"docker.js","sourceRoot":"","sources":["../../src/generators/docker.ts"],"names":[],"mappings":";;;;;AAIA,wCAyFC;AA7FD,gDAAwB;AACxB,wDAA0B;AAGnB,KAAK,UAAU,cAAc,CAAC,MAAmB,EAAE,SAAiB;IACzE,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACjD,MAAM,kBAAE,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAE9B,sBAAsB;IACtB,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;CAkB5B,CAAC;IAEA,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEpF,qBAAqB;IACrB,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;CAa3B,CAAC;IAEA,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAElF,4BAA4B;IAC5B,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;CAmBnB,CAAC;IAEA,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,SAAS,CAAC,CAAC;IAElE,iBAAiB;IACjB,MAAM,aAAa,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,aAAa,CAAC,CAAC;IAE9E,gBAAgB;IAChB,MAAM,YAAY,GAAG;;;;;;;;;;;;CAYtB,CAAC;IAEA,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,EAAE,YAAY,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAmB;IAC3C,IAAI,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;uBAuBM,MAAM,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,gDAAgD,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,uCAAuC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,iCAAiC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,iBAAiB;;;CAGrU,CAAC;IAEA,uBAAuB;IACvB,IAAI,MAAM,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;QACrC,QAAQ,IAAI;;;;;;;;;sBASM,MAAM,CAAC,WAAW;;;;;;CAMvC,CAAC;IACA,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACzC,QAAQ,IAAI;;;;;;;;;gCASgB,MAAM,CAAC,WAAW;;;;;;CAMjD,CAAC;IACA,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACvC,QAAQ,IAAI;;;;;;;;yBAQS,MAAM,CAAC,WAAW;;;;;;CAM1C,CAAC;IACA,CAAC;SAAM,CAAC;QACN,QAAQ,IAAI;;;;CAIf,CAAC;IACA,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
1
+ {"version":3,"file":"docker.js","sourceRoot":"","sources":["../../src/generators/docker.ts"],"names":[],"mappings":";;;;;AAIA,wCAmGC;AAvGD,gDAAwB;AACxB,wDAA0B;AAGnB,KAAK,UAAU,cAAc,CAAC,MAAmB,EAAE,SAAiB;IACzE,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACjD,MAAM,kBAAE,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAE9B,sBAAsB;IACtB,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;CAkB5B,CAAC;IAEA,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEpF,qBAAqB;IACrB,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;EAkB1B,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,mEAAmE,CAAC,CAAC,CAAC,EAAE;;;;;CAKzG,CAAC;IAEA,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAElF,4BAA4B;IAC5B,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;CAmBnB,CAAC;IAEA,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,SAAS,CAAC,CAAC;IAElE,iBAAiB;IACjB,MAAM,aAAa,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC/C,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,aAAa,CAAC,CAAC;IAE9E,gBAAgB;IAChB,MAAM,YAAY,GAAG;;;;;;;;;;;;CAYtB,CAAC;IAEA,MAAM,kBAAE,CAAC,SAAS,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,EAAE,YAAY,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAmB;IAC3C,IAAI,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;uBAuBM,MAAM,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,gDAAgD,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,uCAAuC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,iCAAiC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,iBAAiB;;;CAGrU,CAAC;IAEA,uBAAuB;IACvB,IAAI,MAAM,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;QACrC,QAAQ,IAAI;;;;;;;;;sBASM,MAAM,CAAC,WAAW;;;;;;CAMvC,CAAC;IACA,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACzC,QAAQ,IAAI;;;;;;;;;gCASgB,MAAM,CAAC,WAAW;;;;;;CAMjD,CAAC;IACA,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACvC,QAAQ,IAAI;;;;;;;;yBAQS,MAAM,CAAC,WAAW;;;;;;CAM1C,CAAC;IACA,CAAC;SAAM,CAAC;QACN,QAAQ,IAAI;;;;CAIf,CAAC;IACA,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -5,17 +5,72 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.promptForStack = promptForStack;
7
7
  const inquirer_1 = __importDefault(require("inquirer"));
8
+ /**
9
+ * Normalizes input strings for consistency (e.g., 'Next.js 14' -> 'nextjs')
10
+ */
11
+ function normalizeInput(category, value) {
12
+ const v = value.toLowerCase().trim();
13
+ const mappings = {
14
+ frontend: {
15
+ 'react-vite': 'react-vite',
16
+ 'react + vite': 'react-vite',
17
+ 'react': 'react-vite',
18
+ 'vite': 'react-vite',
19
+ 'nextjs': 'nextjs',
20
+ 'next.js': 'nextjs',
21
+ 'next.js-14': 'nextjs',
22
+ 'next': 'nextjs',
23
+ 'vue-vite': 'vue-vite',
24
+ 'vue': 'vue-vite',
25
+ 'sveltekit': 'sveltekit',
26
+ 'svelte': 'sveltekit'
27
+ },
28
+ backend: {
29
+ 'express': 'express',
30
+ 'node.js + express': 'express',
31
+ 'fastify': 'fastify',
32
+ 'node.js + fastify': 'fastify',
33
+ 'nestjs': 'nestjs',
34
+ 'nest': 'nestjs',
35
+ 'bun-elysia': 'bun-elysia',
36
+ 'bun': 'bun-elysia',
37
+ 'elysia': 'bun-elysia',
38
+ 'go-fiber': 'go-fiber',
39
+ 'go': 'go-fiber'
40
+ },
41
+ auth: {
42
+ 'jwt': 'jwt',
43
+ 'clerk': 'clerk',
44
+ 'supabase': 'supabase',
45
+ 'supabase-auth': 'supabase',
46
+ 'authjs': 'authjs',
47
+ 'auth.js': 'authjs',
48
+ 'nextauth': 'authjs',
49
+ 'firebase': 'firebase',
50
+ 'firebase-auth': 'firebase'
51
+ },
52
+ database: {
53
+ 'postgresql': 'postgresql',
54
+ 'postgres': 'postgresql',
55
+ 'mongodb': 'mongodb',
56
+ 'mongo': 'mongodb',
57
+ 'mysql': 'mysql',
58
+ 'sqlite': 'sqlite',
59
+ 'supabase-db': 'supabase-db'
60
+ }
61
+ };
62
+ return mappings[category][v] || v;
63
+ }
8
64
  async function promptForStack(projectName, options = {}) {
9
65
  console.log('\n');
10
- // If options are provided, merge them with defaults and return
11
- // This allows non-interactive mode
66
+ // If flags are provided, normalize and return immediately for non-interactive mode
12
67
  if (options.frontend || options.backend || options.auth || options.database) {
13
68
  return {
14
69
  projectName,
15
- frontend: options.frontend || 'react-vite',
16
- backend: options.backend || 'express',
17
- auth: options.auth || 'jwt',
18
- database: options.database || 'postgresql',
70
+ frontend: normalizeInput('frontend', options.frontend || 'react-vite'),
71
+ backend: normalizeInput('backend', options.backend || 'express'),
72
+ auth: normalizeInput('auth', options.auth || 'jwt'),
73
+ database: normalizeInput('database', options.database || 'postgresql'),
19
74
  apiStyle: options.api // 'api' option maps to 'apiStyle' in config
20
75
  ? (options.api === 'trpc' ? 'trpc' : options.api === 'graphql' ? 'graphql' : 'rest')
21
76
  : 'rest',
@@ -30,7 +85,7 @@ async function promptForStack(projectName, options = {}) {
30
85
  message: 'Choose your frontend framework:',
31
86
  choices: [
32
87
  { name: 'React + Vite (Recommended)', value: 'react-vite' },
33
- { name: 'Next.js (App Router)', value: 'nextjs' },
88
+ { name: 'Next.js 14 (App Router)', value: 'nextjs' },
34
89
  { name: 'Vue + Vite', value: 'vue-vite' },
35
90
  { name: 'SvelteKit', value: 'sveltekit' },
36
91
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/utils/prompts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/utils/prompts.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "forgestack-os-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "ForgeStack OS CLI - Generate production-ready full-stack SaaS applications",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "forgestack": "./dist/index.js"
8
8
  },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
9
14
  "scripts": {
10
15
  "dev": "tsx watch src/index.ts",
11
16
  "build": "tsc",
@@ -46,4 +51,4 @@
46
51
  "typescript": "^5.3.3",
47
52
  "vitest": "^1.6.1"
48
53
  }
49
- }
54
+ }
@@ -1,82 +0,0 @@
1
- import path from 'path';
2
- import fs from 'fs-extra';
3
- import chalk from 'chalk';
4
- import { logger } from '../utils/logger';
5
- import { promptForStack } from '../utils/prompts';
6
- import { validateStackConfig } from '../utils/validators';
7
- import { generateProject } from '../generators';
8
-
9
- export async function createCommand(projectName: string, options: any) {
10
- try {
11
- // Display welcome banner
12
- console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════╗'));
13
- console.log(chalk.bold.cyan('║ ║'));
14
- console.log(chalk.bold.cyan('║ 🚀 ForgeStack OS v0.1.0 ║'));
15
- console.log(chalk.bold.cyan('║ ║'));
16
- console.log(chalk.bold.cyan('║ One platform. Any stack. Production. ║'));
17
- console.log(chalk.bold.cyan('║ ║'));
18
- console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
19
-
20
- // Check if directory already exists
21
- const targetDir = path.resolve(process.cwd(), projectName);
22
- if (await fs.pathExists(targetDir)) {
23
- logger.error(`Directory "${projectName}" already exists!`);
24
- process.exit(1);
25
- }
26
-
27
- // Prompt for stack configuration
28
- logger.title('📋 Configure Your Stack');
29
- const config = await promptForStack(projectName, options);
30
-
31
- // Validate configuration
32
- const validation = validateStackConfig(config);
33
-
34
- if (validation.warnings.length > 0) {
35
- console.log('');
36
- validation.warnings.forEach(warning => logger.warning(warning));
37
- }
38
-
39
- if (!validation.valid) {
40
- console.log('');
41
- validation.errors.forEach(error => logger.error(error));
42
- process.exit(1);
43
- }
44
-
45
- // Display selected stack
46
- console.log('');
47
- logger.title('✨ Your Stack Configuration');
48
- console.log(chalk.gray('─'.repeat(50)));
49
- console.log(`${chalk.bold('Project:')} ${chalk.cyan(config.projectName)}`);
50
- console.log(`${chalk.bold('Frontend:')} ${chalk.cyan(config.frontend)}`);
51
- console.log(`${chalk.bold('Backend:')} ${chalk.cyan(config.backend)}`);
52
- console.log(`${chalk.bold('Auth:')} ${chalk.cyan(config.auth)}`);
53
- console.log(`${chalk.bold('Database:')} ${chalk.cyan(config.database)}`);
54
- console.log(`${chalk.bold('API Style:')} ${chalk.cyan(config.apiStyle)}`);
55
- console.log(`${chalk.bold('Docker:')} ${chalk.cyan(config.docker ? 'Yes' : 'No')}`);
56
- console.log(`${chalk.bold('Multi-Tenant:')} ${chalk.cyan(config.multiTenant ? 'Yes' : 'No')}`);
57
- console.log(chalk.gray('─'.repeat(50)));
58
- console.log('');
59
-
60
- // Generate project
61
- await generateProject(config, targetDir);
62
-
63
- // Success message
64
- console.log('');
65
- logger.success(chalk.bold('🎉 Project created successfully!\n'));
66
-
67
- console.log(chalk.bold('Next steps:\n'));
68
- console.log(chalk.cyan(` cd ${projectName}`));
69
- console.log(chalk.cyan(' npm install'));
70
- console.log(chalk.cyan(' npm run dev\n'));
71
-
72
- console.log(chalk.gray('For more information, check out the README.md in your project.\n'));
73
- console.log(chalk.bold('Built by Sumit Chauhan'));
74
- console.log(chalk.gray('GitHub: https://github.com/halloffame12'));
75
- console.log(chalk.gray('LinkedIn: https://www.linkedin.com/in/sumit-chauhan-a4ba98325/\n'));
76
-
77
- } catch (error) {
78
- logger.error('Failed to create project');
79
- console.error(error);
80
- process.exit(1);
81
- }
82
- }
@@ -1,353 +0,0 @@
1
- import path from 'path';
2
- import fs from 'fs-extra';
3
- import { StackConfig } from '../types';
4
-
5
- export async function generateGraphQL(config: StackConfig, backendDir: string) {
6
- if (config.backend === 'express') {
7
- await generateExpressGraphQL(config, backendDir);
8
- } else if (config.backend === 'nestjs') {
9
- await generateNestJSGraphQL(config, backendDir);
10
- }
11
- }
12
-
13
- async function generateExpressGraphQL(config: StackConfig, backendDir: string) {
14
- // Update package.json
15
- const packageJson = await fs.readJSON(path.join(backendDir, 'package.json'));
16
- packageJson.dependencies['graphql'] = '^16.8.1';
17
- packageJson.dependencies['@apollo/server'] = '^4.10.0';
18
- packageJson.dependencies['@graphql-tools/schema'] = '^10.0.2';
19
- await fs.writeJSON(path.join(backendDir, 'package.json'), packageJson, { spaces: 2 });
20
-
21
- // Create GraphQL directory
22
- const graphqlDir = path.join(backendDir, 'src', 'graphql');
23
- await fs.ensureDir(graphqlDir);
24
- await fs.ensureDir(path.join(graphqlDir, 'resolvers'));
25
- await fs.ensureDir(path.join(graphqlDir, 'types'));
26
-
27
- // Schema
28
- const schema = `import { gql } from 'graphql-tag';
29
-
30
- export const typeDefs = gql\`
31
- type User {
32
- id: ID!
33
- email: String!
34
- name: String
35
- ${config.multiTenant ? 'tenantId: String' : ''}
36
- }
37
-
38
- type AuthPayload {
39
- token: String!
40
- user: User!
41
- }
42
-
43
- type Query {
44
- me: User
45
- users: [User!]!
46
- }
47
-
48
- type Mutation {
49
- register(email: String!, password: String!, name: String): AuthPayload!
50
- login(email: String!, password: String!): AuthPayload!
51
- }
52
- \`;
53
- `;
54
- await fs.writeFile(path.join(graphqlDir, 'schema.ts'), schema);
55
-
56
- // Resolvers
57
- const resolvers = `import bcrypt from 'bcrypt';
58
- import jwt from 'jsonwebtoken';
59
- ${config.database === 'mongodb' ? "import User from '../models/User';" : "import prisma from '../lib/prisma';"}
60
-
61
- export const resolvers = {
62
- Query: {
63
- me: async (_: any, __: any, context: any) => {
64
- if (!context.user) throw new Error('Not authenticated');
65
-
66
- ${config.database === 'mongodb' ? `
67
- return await User.findById(context.user.userId);
68
- ` : `
69
- return await prisma.user.findUnique({
70
- where: { id: context.user.userId },
71
- });
72
- `}
73
- },
74
- users: async (_: any, __: any, context: any) => {
75
- if (!context.user) throw new Error('Not authenticated');
76
-
77
- ${config.database === 'mongodb' ? `
78
- return await User.find(${config.multiTenant ? '{ tenantId: context.user.tenantId }' : '{}'});
79
- ` : `
80
- return await prisma.user.findMany(${config.multiTenant ? '{ where: { tenantId: context.user.tenantId } }' : '{}'});
81
- `}
82
- },
83
- },
84
- Mutation: {
85
- register: async (_: any, { email, password, name }: any) => {
86
- const hashedPassword = await bcrypt.hash(password, 10);
87
-
88
- ${config.database === 'mongodb' ? `
89
- const user = await User.create({
90
- email,
91
- password: hashedPassword,
92
- name,
93
- });
94
- ` : `
95
- const user = await prisma.user.create({
96
- data: {
97
- email,
98
- password: hashedPassword,
99
- name,
100
- },
101
- });
102
- `}
103
-
104
- const token = jwt.sign(
105
- { userId: user.id, email: user.email },
106
- process.env.JWT_SECRET!,
107
- { expiresIn: '7d' }
108
- );
109
-
110
- return { token, user };
111
- },
112
- login: async (_: any, { email, password }: any) => {
113
- ${config.database === 'mongodb' ? `
114
- const user = await User.findOne({ email });
115
- ` : `
116
- const user = await prisma.user.findUnique({
117
- where: { email },
118
- });
119
- `}
120
-
121
- if (!user) throw new Error('Invalid credentials');
122
-
123
- const valid = await bcrypt.compare(password, user.password);
124
- if (!valid) throw new Error('Invalid credentials');
125
-
126
- const token = jwt.sign(
127
- { userId: user.id, email: user.email },
128
- process.env.JWT_SECRET!,
129
- { expiresIn: '7d' }
130
- );
131
-
132
- return { token, user };
133
- },
134
- },
135
- };
136
- `;
137
- await fs.writeFile(path.join(graphqlDir, 'resolvers', 'index.ts'), resolvers);
138
-
139
- // Apollo Server setup
140
- const apolloSetup = `import { ApolloServer } from '@apollo/server';
141
- import { expressMiddleware } from '@apollo/server/express4';
142
- import { typeDefs } from './schema';
143
- import { resolvers } from './resolvers';
144
- import jwt from 'jsonwebtoken';
145
-
146
- export async function createApolloServer() {
147
- const server = new ApolloServer({
148
- typeDefs,
149
- resolvers,
150
- });
151
-
152
- await server.start();
153
-
154
- return expressMiddleware(server, {
155
- context: async ({ req }) => {
156
- const token = req.headers.authorization?.replace('Bearer ', '');
157
-
158
- if (token) {
159
- try {
160
- const user = jwt.verify(token, process.env.JWT_SECRET!);
161
- return { user };
162
- } catch (err) {
163
- return {};
164
- }
165
- }
166
-
167
- return {};
168
- },
169
- });
170
- }
171
- `;
172
- await fs.writeFile(path.join(graphqlDir, 'server.ts'), apolloSetup);
173
- }
174
-
175
- async function generateNestJSGraphQL(config: StackConfig, backendDir: string) {
176
- // Update package.json
177
- const packageJson = await fs.readJSON(path.join(backendDir, 'package.json'));
178
- packageJson.dependencies['@nestjs/graphql'] = '^12.0.11';
179
- packageJson.dependencies['@nestjs/apollo'] = '^12.0.11';
180
- packageJson.dependencies['@apollo/server'] = '^4.10.0';
181
- packageJson.dependencies['graphql'] = '^16.8.1';
182
- await fs.writeJSON(path.join(backendDir, 'package.json'), packageJson, { spaces: 2 });
183
-
184
- // GraphQL module
185
- const graphqlModule = `import { Module } from '@nestjs/common';
186
- import { GraphQLModule } from '@nestjs/graphql';
187
- import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
188
- import { join } from 'path';
189
-
190
- @Module({
191
- imports: [
192
- GraphQLModule.forRoot<ApolloDriverConfig>({
193
- driver: ApolloDriver,
194
- autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
195
- sortSchema: true,
196
- playground: true,
197
- context: ({ req }) => ({ req }),
198
- }),
199
- ],
200
- })
201
- export class GraphqlModule {}
202
- `;
203
- await fs.writeFile(path.join(backendDir, 'src', 'graphql.module.ts'), graphqlModule);
204
-
205
- // User resolver
206
- const userResolver = `import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
207
- import { UseGuards } from '@nestjs/common';
208
- import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
209
- import { UsersService } from './users/users.service';
210
- import { AuthService } from './auth/auth.service';
211
-
212
- @Resolver('User')
213
- export class UserResolver {
214
- constructor(
215
- private usersService: UsersService,
216
- private authService: AuthService,
217
- ) {}
218
-
219
- @Query()
220
- @UseGuards(JwtAuthGuard)
221
- async me(@Args('id') id: string) {
222
- return this.usersService.findById(id);
223
- }
224
-
225
- @Mutation()
226
- async register(
227
- @Args('email') email: string,
228
- @Args('password') password: string,
229
- @Args('name') name?: string,
230
- ) {
231
- return this.authService.register({ email, password, name });
232
- }
233
-
234
- @Mutation()
235
- async login(
236
- @Args('email') email: string,
237
- @Args('password') password: string,
238
- ) {
239
- return this.authService.login({ email, password });
240
- }
241
- }
242
- `;
243
- await fs.writeFile(path.join(backendDir, 'src', 'users', 'users.resolver.ts'), userResolver);
244
- }
245
-
246
- export async function generateTRPC(config: StackConfig, backendDir: string, frontendDir: string) {
247
- // Backend setup
248
- const packageJson = await fs.readJSON(path.join(backendDir, 'package.json'));
249
- packageJson.dependencies['@trpc/server'] = '^10.45.0';
250
- packageJson.dependencies['zod'] = '^3.22.4';
251
- await fs.writeJSON(path.join(backendDir, 'package.json'), packageJson, { spaces: 2 });
252
-
253
- // tRPC router
254
- const trpcDir = path.join(backendDir, 'src', 'trpc');
255
- await fs.ensureDir(trpcDir);
256
- await fs.ensureDir(path.join(trpcDir, 'routers'));
257
-
258
- const trpcSetup = `import { initTRPC } from '@trpc/server';
259
- import { z } from 'zod';
260
-
261
- const t = initTRPC.create();
262
-
263
- export const router = t.router;
264
- export const publicProcedure = t.procedure;
265
- `;
266
- await fs.writeFile(path.join(trpcDir, 'trpc.ts'), trpcSetup);
267
-
268
- const authRouter = `import { router, publicProcedure } from '../trpc';
269
- import { z } from 'zod';
270
- import bcrypt from 'bcrypt';
271
- import jwt from 'jsonwebtoken';
272
- ${config.database === 'mongodb' ? "import User from '../../models/User';" : "import prisma from '../../lib/prisma';"}
273
-
274
- export const authRouter = router({
275
- register: publicProcedure
276
- .input(z.object({
277
- email: z.string().email(),
278
- password: z.string().min(8),
279
- name: z.string().optional(),
280
- }))
281
- .mutation(async ({ input }) => {
282
- const hashedPassword = await bcrypt.hash(input.password, 10);
283
-
284
- ${config.database === 'mongodb' ? `
285
- const user = await User.create({
286
- email: input.email,
287
- password: hashedPassword,
288
- name: input.name,
289
- });
290
- ` : `
291
- const user = await prisma.user.create({
292
- data: {
293
- email: input.email,
294
- password: hashedPassword,
295
- name: input.name,
296
- },
297
- });
298
- `}
299
-
300
- const token = jwt.sign(
301
- { userId: user.id, email: user.email },
302
- process.env.JWT_SECRET!,
303
- { expiresIn: '7d' }
304
- );
305
-
306
- return { token, user: { id: user.id, email: user.email, name: user.name } };
307
- }),
308
-
309
- login: publicProcedure
310
- .input(z.object({
311
- email: z.string().email(),
312
- password: z.string(),
313
- }))
314
- .mutation(async ({ input }) => {
315
- ${config.database === 'mongodb' ? `
316
- const user = await User.findOne({ email: input.email });
317
- ` : `
318
- const user = await prisma.user.findUnique({
319
- where: { email: input.email },
320
- });
321
- `}
322
-
323
- if (!user) throw new Error('Invalid credentials');
324
-
325
- const valid = await bcrypt.compare(input.password, user.password);
326
- if (!valid) throw new Error('Invalid credentials');
327
-
328
- const token = jwt.sign(
329
- { userId: user.id, email: user.email },
330
- process.env.JWT_SECRET!,
331
- { expiresIn: '7d' }
332
- );
333
-
334
- return { token, user: { id: user.id, email: user.email, name: user.name } };
335
- }),
336
- });
337
- `;
338
- await fs.writeFile(path.join(trpcDir, 'routers', 'auth.ts'), authRouter);
339
-
340
- // Frontend tRPC client
341
- const frontendPackageJson = await fs.readJSON(path.join(frontendDir, 'package.json'));
342
- frontendPackageJson.dependencies['@trpc/client'] = '^10.45.0';
343
- frontendPackageJson.dependencies['@trpc/react-query'] = '^10.45.0';
344
- frontendPackageJson.dependencies['@tanstack/react-query'] = '^5.17.19';
345
- await fs.writeJSON(path.join(frontendDir, 'package.json'), frontendPackageJson, { spaces: 2 });
346
-
347
- const trpcClient = `import { createTRPCReact } from '@trpc/react-query';
348
- import type { AppRouter } from '../../../backend/src/trpc/routers';
349
-
350
- export const trpc = createTRPCReact<AppRouter>();
351
- `;
352
- await fs.writeFile(path.join(frontendDir, 'src', 'lib', 'trpc.ts'), trpcClient);
353
- }