create-tigra 2.0.0 → 2.0.2

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.0')
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.0'));
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');
@@ -205,34 +221,50 @@ async function main() {
205
221
  }
206
222
 
207
223
  // Print next steps
224
+ const dim = chalk.dim;
225
+ const bold = chalk.bold;
226
+ const cyan = chalk.cyan;
227
+ const green = chalk.green;
228
+ const line = dim(' ─────────────────────────────────────────');
229
+
230
+ console.log();
231
+ console.log(green.bold(' ✓ Created ') + cyan.bold(projectName) + dim(` at ${targetDir}`));
232
+ console.log();
233
+ console.log(' ┌─────────────────────────────────────────┐');
234
+ console.log(' │' + bold(' Getting Started ') + '│');
235
+ console.log(' └─────────────────────────────────────────┘');
236
+ console.log();
237
+ console.log(bold(' SERVER') + dim(' cd ') + cyan(`${projectName}/server`));
238
+ console.log();
239
+ console.log(cyan(' 1 ') + 'Install & start infrastructure');
240
+ console.log(dim(' npm install'));
241
+ console.log(dim(' npm run docker:up'));
242
+ console.log();
243
+ console.log(cyan(' 2 ') + 'Set up database');
244
+ console.log(dim(' npm run prisma:generate'));
245
+ console.log(dim(' npm run prisma:migrate:dev -- --name init'));
208
246
  console.log();
209
- console.log(chalk.green.bold(` Success!`) + ` Created ${chalk.cyan(projectName)} at ${chalk.dim(targetDir)}`);
247
+ console.log(cyan(' 3 ') + 'Start the server');
248
+ console.log(dim(' npm run dev'));
210
249
  console.log();
211
- console.log(chalk.bold(' Next steps:'));
250
+ console.log(bold(' CLIENT') + dim(' (new terminal) cd ') + cyan(`${projectName}/client`));
212
251
  console.log();
213
- console.log(chalk.cyan(' 1.') + ' Start infrastructure:');
214
- console.log(chalk.dim(` cd ${projectName}/server`));
215
- console.log(chalk.dim(' docker compose up -d'));
252
+ console.log(cyan(' 4 ') + 'Start the client');
253
+ console.log(dim(' npm install'));
254
+ console.log(dim(' npm run dev'));
216
255
  console.log();
217
- console.log(chalk.cyan(' 2.') + ' Install server dependencies & set up database:');
218
- console.log(chalk.dim(' npm install'));
219
- console.log(chalk.dim(' cp .env.example .env'));
220
- console.log(chalk.dim(' npm run prisma:generate'));
221
- console.log(chalk.dim(' npm run prisma:migrate:dev -- --name init'));
256
+ console.log(line);
222
257
  console.log();
223
- console.log(chalk.cyan(' 3.') + ' Start the server:');
224
- console.log(chalk.dim(' npm run dev'));
258
+ console.log(dim(' App ') + cyan('http://localhost:3000'));
259
+ console.log(dim(' API ') + cyan('http://localhost:8000'));
260
+ console.log(dim(' phpMyAdmin ') + cyan(`http://localhost:${variables.PHPMYADMIN_PORT}`));
261
+ console.log(dim(' Redis CMD ') + cyan(`http://localhost:${variables.REDIS_COMMANDER_PORT}`));
225
262
  console.log();
226
- console.log(chalk.cyan(' 4.') + ` In a ${chalk.bold('new terminal')}, set up the client:`);
227
- console.log(chalk.dim(` cd ${projectName}/client`));
228
- console.log(chalk.dim(' npm install'));
229
- console.log(chalk.dim(' cp .env.example .env'));
230
- console.log(chalk.dim(' npm run dev'));
263
+ console.log(line);
231
264
  console.log();
232
- console.log(chalk.dim(' Server running at: ') + chalk.cyan('http://localhost:8000'));
233
- console.log(chalk.dim(' Client running at: ') + chalk.cyan('http://localhost:3000'));
265
+ console.log(dim(' Tip: ') + 'npm run docker:down' + dim(' to stop infrastructure'));
234
266
  console.log();
235
- console.log(chalk.dim(' Happy coding!'));
267
+ console.log(dim(' Happy coding! 🚀'));
236
268
  console.log();
237
269
  });
238
270
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-tigra",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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": {
@@ -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
  # ===================================================================
@@ -1,13 +1,15 @@
1
1
  # WARNING: These credentials are for LOCAL DEVELOPMENT ONLY.
2
2
  # Change all passwords and secrets before using in any shared or production environment.
3
3
 
4
+ name: {{PROJECT_NAME}}
5
+
4
6
  services:
5
7
  mysql:
6
8
  image: mysql:8.0
7
9
  container_name: {{PROJECT_NAME}}-mysql
8
10
  restart: unless-stopped
9
11
  ports:
10
- - '3306:3306'
12
+ - '${MYSQL_PORT:-{{MYSQL_PORT}}}:3306'
11
13
  environment:
12
14
  MYSQL_ROOT_PASSWORD: rootpassword
13
15
  MYSQL_DATABASE: {{DATABASE_NAME}}
@@ -26,7 +28,7 @@ services:
26
28
  container_name: {{PROJECT_NAME}}-phpmyadmin
27
29
  restart: unless-stopped
28
30
  ports:
29
- - '8080:80'
31
+ - '${PHPMYADMIN_PORT:-{{PHPMYADMIN_PORT}}}:80'
30
32
  environment:
31
33
  PMA_HOST: mysql
32
34
  PMA_PORT: 3306
@@ -46,7 +48,7 @@ services:
46
48
  container_name: {{PROJECT_NAME}}-redis
47
49
  restart: unless-stopped
48
50
  ports:
49
- - '6379:6379'
51
+ - '${REDIS_PORT:-{{REDIS_PORT}}}:6379'
50
52
  volumes:
51
53
  - redis_data:/data
52
54
  networks:
@@ -62,7 +64,7 @@ services:
62
64
  container_name: {{PROJECT_NAME}}-redis-commander
63
65
  restart: unless-stopped
64
66
  ports:
65
- - '8081:8081'
67
+ - '${REDIS_COMMANDER_PORT:-{{REDIS_COMMANDER_PORT}}}:8081'
66
68
  environment:
67
69
  REDIS_HOSTS: local:redis:6379
68
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,