create-tigra 2.0.1 → 2.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.
@@ -124,11 +124,11 @@ async function main() {
124
124
  program
125
125
  .name('create-tigra')
126
126
  .description('Create a production-ready full-stack app with Next.js + Fastify + Prisma + Redis')
127
- .version('2.0.1')
127
+ .version('2.0.2')
128
128
  .argument('[project-name]', 'Name for your new project')
129
129
  .action(async (projectNameArg) => {
130
130
  console.log();
131
- console.log(chalk.bold(' Create Tigra') + chalk.dim(' v2.0.1'));
131
+ console.log(chalk.bold(' Create Tigra') + chalk.dim(' v2.0.2'));
132
132
  console.log();
133
133
 
134
134
  let projectName = projectNameArg;
@@ -171,6 +171,9 @@ async function main() {
171
171
  }
172
172
  }
173
173
 
174
+ // Generate random port offset (1-200) so multiple projects don't conflict
175
+ const portOffset = crypto.randomInt(1, 201);
176
+
174
177
  // Derive all variables
175
178
  const variables = {
176
179
  PROJECT_NAME: projectName,
@@ -178,6 +181,10 @@ async function main() {
178
181
  PROJECT_DISPLAY_NAME: toTitleCase(projectName),
179
182
  DATABASE_NAME: `${toSnakeCase(projectName)}_db`,
180
183
  JWT_SECRET: crypto.randomBytes(48).toString('hex'),
184
+ MYSQL_PORT: String(3306 + portOffset),
185
+ PHPMYADMIN_PORT: String(8080 + portOffset),
186
+ REDIS_PORT: String(6379 + portOffset),
187
+ REDIS_COMMANDER_PORT: String(8081 + portOffset),
181
188
  };
182
189
 
183
190
  // Copy template
@@ -197,6 +204,15 @@ async function main() {
197
204
  }
198
205
  }
199
206
 
207
+ // Generate .env from .env.example (so users don't have to copy manually)
208
+ for (const envExample of ['server/.env.example', 'client/.env.example']) {
209
+ const examplePath = path.join(targetDir, envExample);
210
+ const envPath = path.join(targetDir, envExample.replace('.env.example', '.env'));
211
+ if (await fs.pathExists(examplePath)) {
212
+ await fs.copy(examplePath, envPath);
213
+ }
214
+ }
215
+
200
216
  spinner.succeed('Project scaffolded successfully!');
201
217
  } catch (error) {
202
218
  spinner.fail('Failed to scaffold project');
@@ -222,7 +238,6 @@ async function main() {
222
238
  console.log();
223
239
  console.log(cyan(' 1 ') + 'Install & start infrastructure');
224
240
  console.log(dim(' npm install'));
225
- console.log(dim(' cp .env.example .env'));
226
241
  console.log(dim(' npm run docker:up'));
227
242
  console.log();
228
243
  console.log(cyan(' 2 ') + 'Set up database');
@@ -236,15 +251,14 @@ async function main() {
236
251
  console.log();
237
252
  console.log(cyan(' 4 ') + 'Start the client');
238
253
  console.log(dim(' npm install'));
239
- console.log(dim(' cp .env.example .env'));
240
254
  console.log(dim(' npm run dev'));
241
255
  console.log();
242
256
  console.log(line);
243
257
  console.log();
244
258
  console.log(dim(' App ') + cyan('http://localhost:3000'));
245
259
  console.log(dim(' API ') + cyan('http://localhost:8000'));
246
- console.log(dim(' phpMyAdmin ') + cyan('http://localhost:8080'));
247
- console.log(dim(' Redis CMD ') + cyan('http://localhost:8081'));
260
+ console.log(dim(' phpMyAdmin ') + cyan(`http://localhost:${variables.PHPMYADMIN_PORT}`));
261
+ console.log(dim(' Redis CMD ') + cyan(`http://localhost:${variables.REDIS_COMMANDER_PORT}`));
248
262
  console.log();
249
263
  console.log(line);
250
264
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-tigra",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "type": "module",
5
5
  "description": "Create a production-ready full-stack app with Next.js 16 + Fastify 5 + Prisma + Redis",
6
6
  "bin": {
@@ -28,6 +28,9 @@
28
28
  "tailwindcss-animate": "^1.0.7",
29
29
  "zod": "^4.3.6"
30
30
  },
31
+ "overrides": {
32
+ "minimatch": ">=10.2.1"
33
+ },
31
34
  "devDependencies": {
32
35
  "@tailwindcss/postcss": "^4",
33
36
  "@tanstack/react-query-devtools": "^5.91.3",
@@ -17,7 +17,7 @@ HOST=0.0.0.0
17
17
 
18
18
  # Database connection string
19
19
  # Format: mysql://username:password@host:port/database
20
- DATABASE_URL="mysql://root:rootpassword@localhost:3306/{{DATABASE_NAME}}"
20
+ DATABASE_URL="mysql://root:rootpassword@localhost:{{MYSQL_PORT}}/{{DATABASE_NAME}}"
21
21
 
22
22
  # Connection pool settings (for high-traffic production)
23
23
  # Min connections: 2-5 for low traffic, 5-10 for medium, 10-20 for high
@@ -31,7 +31,7 @@ DATABASE_POOL_MAX=10
31
31
 
32
32
  # Redis connection URL
33
33
  # Format: redis://[:password@]host:port[/database]
34
- REDIS_URL="redis://localhost:6379"
34
+ REDIS_URL="redis://localhost:{{REDIS_PORT}}"
35
35
 
36
36
  # Max retry attempts for failed Redis operations
37
37
  REDIS_MAX_RETRIES=3
@@ -39,6 +39,16 @@ REDIS_MAX_RETRIES=3
39
39
  # Connection timeout in milliseconds
40
40
  REDIS_CONNECT_TIMEOUT=10000
41
41
 
42
+ # ===================================================================
43
+ # DOCKER PORTS (auto-generated, unique per project)
44
+ # ===================================================================
45
+
46
+ # Change these if they conflict with other services on your machine
47
+ MYSQL_PORT={{MYSQL_PORT}}
48
+ PHPMYADMIN_PORT={{PHPMYADMIN_PORT}}
49
+ REDIS_PORT={{REDIS_PORT}}
50
+ REDIS_COMMANDER_PORT={{REDIS_COMMANDER_PORT}}
51
+
42
52
  # ===================================================================
43
53
  # JWT AUTHENTICATION
44
54
  # ===================================================================
@@ -9,7 +9,7 @@ services:
9
9
  container_name: {{PROJECT_NAME}}-mysql
10
10
  restart: unless-stopped
11
11
  ports:
12
- - '3306:3306'
12
+ - '${MYSQL_PORT:-{{MYSQL_PORT}}}:3306'
13
13
  environment:
14
14
  MYSQL_ROOT_PASSWORD: rootpassword
15
15
  MYSQL_DATABASE: {{DATABASE_NAME}}
@@ -28,7 +28,7 @@ services:
28
28
  container_name: {{PROJECT_NAME}}-phpmyadmin
29
29
  restart: unless-stopped
30
30
  ports:
31
- - '8080:80'
31
+ - '${PHPMYADMIN_PORT:-{{PHPMYADMIN_PORT}}}:80'
32
32
  environment:
33
33
  PMA_HOST: mysql
34
34
  PMA_PORT: 3306
@@ -48,7 +48,7 @@ services:
48
48
  container_name: {{PROJECT_NAME}}-redis
49
49
  restart: unless-stopped
50
50
  ports:
51
- - '6379:6379'
51
+ - '${REDIS_PORT:-{{REDIS_PORT}}}:6379'
52
52
  volumes:
53
53
  - redis_data:/data
54
54
  networks:
@@ -64,7 +64,7 @@ services:
64
64
  container_name: {{PROJECT_NAME}}-redis-commander
65
65
  restart: unless-stopped
66
66
  ports:
67
- - '8081:8081'
67
+ - '${REDIS_COMMANDER_PORT:-{{REDIS_COMMANDER_PORT}}}:8081'
68
68
  environment:
69
69
  REDIS_HOSTS: local:redis:6379
70
70
  depends_on:
@@ -35,10 +35,9 @@
35
35
  "@fastify/jwt": "^10.0.0",
36
36
  "@fastify/multipart": "^9.0.2",
37
37
  "@fastify/rate-limit": "^10.3.0",
38
- "@fastify/static": "^8.0.4",
38
+ "@fastify/static": "^9.0.0",
39
39
  "@prisma/client": "^6.19.2",
40
40
  "argon2": "^0.44.0",
41
- "bcryptjs": "^2.4.3",
42
41
  "dotenv": "^16.4.7",
43
42
  "fastify": "^5.7.4",
44
43
  "fastify-type-provider-zod": "^6.1.0",
@@ -49,15 +48,19 @@
49
48
  "uuid": "^10.0.0",
50
49
  "zod": "^4.3.6"
51
50
  },
51
+ "overrides": {
52
+ "bn.js": ">=5.2.3",
53
+ "@typescript-eslint/typescript-estree": {
54
+ "minimatch": ">=10.2.1"
55
+ }
56
+ },
52
57
  "devDependencies": {
53
- "@eslint/js": "^9.39.2",
54
- "@types/bcryptjs": "^2.4.6",
58
+ "@eslint/js": "^10.0.1",
55
59
  "@types/node": "^20.17.10",
56
- "@types/sharp": "^0.32.0",
57
60
  "@types/uuid": "^10.0.0",
58
61
  "@vitest/coverage-v8": "^4.0.18",
59
62
  "@vitest/ui": "^4.0.18",
60
- "eslint": "^9.39.2",
63
+ "eslint": "^10.0.1",
61
64
  "prisma": "^6.19.2",
62
65
  "tsc-alias": "^1.8.16",
63
66
  "tsx": "^4.21.0",
@@ -16,23 +16,8 @@ export async function hashPassword(password: string): Promise<string> {
16
16
  }
17
17
 
18
18
  /**
19
- * Verify a password against a hash.
20
- * Supports both argon2 and legacy bcrypt hashes.
21
- * Returns { valid, needsRehash } so the caller can transparently upgrade bcrypt hashes.
19
+ * Verify a password against an argon2 hash
22
20
  */
23
- export async function verifyPassword(
24
- password: string,
25
- hash: string,
26
- ): Promise<{ valid: boolean; needsRehash: boolean }> {
27
- const isBcryptHash = hash.startsWith('$2a$') || hash.startsWith('$2b$') || hash.startsWith('$2y$');
28
-
29
- if (isBcryptHash) {
30
- // Lazy-import bcryptjs only for legacy hash verification
31
- const bcrypt = await import('bcryptjs');
32
- const valid = await bcrypt.default.compare(password, hash);
33
- return { valid, needsRehash: valid }; // rehash only if password is correct
34
- }
35
-
36
- const valid = await argon2.verify(hash, password);
37
- return { valid, needsRehash: false };
21
+ export async function verifyPassword(password: string, hash: string): Promise<boolean> {
22
+ return argon2.verify(hash, password);
38
23
  }
@@ -108,7 +108,7 @@ describe('Auth Service', () => {
108
108
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
109
109
 
110
110
  vi.mocked(authRepo.findUserByEmail).mockResolvedValue(testUsers.validUser);
111
- vi.mocked(verifyPassword).mockResolvedValue({ valid: true, needsRehash: false });
111
+ vi.mocked(verifyPassword).mockResolvedValue(true);
112
112
  vi.mocked(authLib.signAccessToken).mockReturnValue(accessToken);
113
113
  vi.mocked(authLib.generateRefreshToken).mockReturnValue(refreshToken);
114
114
  vi.mocked(authLib.getRefreshTokenExpiresAt).mockReturnValue(expiresAt);
@@ -154,7 +154,7 @@ describe('Auth Service', () => {
154
154
  it('should throw UnauthorizedError if password is invalid', async () => {
155
155
  // Arrange
156
156
  vi.mocked(authRepo.findUserByEmail).mockResolvedValue(testUsers.validUser);
157
- vi.mocked(verifyPassword).mockResolvedValue({ valid: false, needsRehash: false });
157
+ vi.mocked(verifyPassword).mockResolvedValue(false);
158
158
 
159
159
  // Act & Assert
160
160
  await expect(authService.login(validLoginInput)).rejects.toThrow(UnauthorizedError);
@@ -132,7 +132,7 @@ export async function login(
132
132
  throw new UnauthorizedError('Invalid email or password', 'INVALID_CREDENTIALS');
133
133
  }
134
134
 
135
- const { valid, needsRehash } = await verifyPassword(input.password, user.password);
135
+ const valid = await verifyPassword(input.password, user.password);
136
136
 
137
137
  if (!valid) {
138
138
  // Increment failed attempts
@@ -153,12 +153,6 @@ export async function login(
153
153
  await authRepo.resetFailedAttempts(user.id);
154
154
  }
155
155
 
156
- // Transparent rehash: upgrade bcrypt → argon2id
157
- if (needsRehash) {
158
- const newHash = await hashPassword(input.password);
159
- await authRepo.updateUserPassword(user.id, newHash);
160
- }
161
-
162
156
  const accessToken = signAccessToken({
163
157
  userId: user.id,
164
158
  role: user.role,