create-projx 1.6.5 → 1.7.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.
@@ -4,6 +4,8 @@ import {
4
4
  DEFAULT_COMPONENT_SKIP_PATTERNS,
5
5
  DEFAULT_ROOT_SKIP_PATTERNS,
6
6
  EXCLUDE,
7
+ KNOWN_FEATURES,
8
+ ORM_PROVIDERS,
7
9
  PACKAGE_MANAGERS,
8
10
  REPO,
9
11
  REPO_URL,
@@ -33,13 +35,15 @@ import {
33
35
  upsertComponentMarker,
34
36
  writeComponentMarker,
35
37
  writeProjxConfig
36
- } from "./chunk-6YRBHJ2V.js";
38
+ } from "./chunk-OLPF7FAN.js";
37
39
  export {
38
40
  COMPONENTS,
39
41
  COMPONENT_MARKER,
40
42
  DEFAULT_COMPONENT_SKIP_PATTERNS,
41
43
  DEFAULT_ROOT_SKIP_PATTERNS,
42
44
  EXCLUDE,
45
+ KNOWN_FEATURES,
46
+ ORM_PROVIDERS,
43
47
  PACKAGE_MANAGERS,
44
48
  REPO,
45
49
  REPO_URL,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.6.5",
4
- "description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
3
+ "version": "1.7.1",
4
+ "description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, Express, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-projx": "dist/index.js"
@@ -20,6 +20,7 @@
20
20
  "fullstack",
21
21
  "fastapi",
22
22
  "fastify",
23
+ "express",
23
24
  "react",
24
25
  "flutter",
25
26
  "terraform",
@@ -44,21 +45,25 @@
44
45
  "node": ">=18"
45
46
  },
46
47
  "dependencies": {
47
- "@clack/prompts": "^0.9"
48
+ "@clack/prompts": "^1.2.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@eslint/js": "^10.0.1",
51
52
  "@types/node": "^25.5.2",
53
+ "@vitest/coverage-v8": "^4.1.5",
52
54
  "eslint": "^10.2.0",
55
+ "prettier": "^3.8.3",
53
56
  "tsup": "^8",
54
57
  "typescript": "^5",
55
- "typescript-eslint": "^8.58.0",
56
- "vitest": "^4.1.2"
58
+ "typescript-eslint": "^8.59.0",
59
+ "vitest": "^4.1.5"
57
60
  },
58
61
  "scripts": {
59
62
  "build": "tsup src/index.ts --format esm --target node18 --clean",
60
63
  "dev": "tsup src/index.ts --format esm --target node18 --watch",
61
- "test": "vitest run",
64
+ "format": "prettier --write .",
65
+ "format:check": "prettier --check .",
66
+ "test": "vitest run --coverage",
62
67
  "test:watch": "vitest"
63
68
  }
64
69
  }
@@ -10,7 +10,10 @@ Scaffolded with [Projx](https://github.com/ukanhaupa/projx).
10
10
  | **<%= paths.fastapi %>/** | FastAPI, SQLAlchemy, Alembic, uvicorn |
11
11
  <% } %>
12
12
  <% if (components.includes('fastify')) { %>
13
- | **<%= paths.fastify %>/** | Fastify, Prisma, TypeBox, TypeScript |
13
+ | **<%= paths.fastify %>/** | Fastify, <%= orm === 'drizzle' ? 'Drizzle' : 'Prisma' %>, TypeBox, TypeScript |
14
+ <% } %>
15
+ <% if (components.includes('express')) { %>
16
+ | **<%= paths.express %>/** | Express 5, TypeScript, <%= orm === 'drizzle' ? 'Drizzle' : 'Prisma' %> |
14
17
  <% } %>
15
18
  <% if (components.includes('frontend')) { %>
16
19
  | **<%= paths.frontend %>/** | React 19, TypeScript, Vite, React Router |
@@ -30,8 +33,9 @@ Scaffolded with [Projx](https://github.com/ukanhaupa/projx).
30
33
  ## Getting Started
31
34
 
32
35
  ```bash
33
- ./scripts/setup.sh # Install all dependencies
34
- docker compose -f docker-compose.dev.yml up # Start with Docker (dev mode)
36
+ ./scripts/setup.sh # Install all dependencies
37
+ # Ensure PostgreSQL is running locally, then run `<%= pm.runDev %>` in each service.
38
+ # Production stack: docker compose up --build
35
39
  ```
36
40
  <% if (components.includes('fastapi')) { %>
37
41
 
@@ -48,11 +52,21 @@ API docs at `http://localhost:7860/docs`.
48
52
  ### <%= paths.fastify %>/
49
53
 
50
54
  ```bash
51
- cd <%= paths.fastify %> && cp .env.example .env && <%= pm.install %> && <%= pm.exec %> prisma migrate dev && <%= pm.run %> dev
55
+ cd <%= paths.fastify %> && cp .env.example .env && <%= pm.install %> && <%= orm === 'drizzle' ? `${pm.exec} drizzle-kit push --force` : `${pm.exec} prisma migrate dev` %> && <%= pm.run %> dev
52
56
  ```
53
57
 
54
58
  API docs at `http://localhost:3000/docs`.
55
59
  <% } %>
60
+ <% if (components.includes('express')) { %>
61
+
62
+ ### <%= paths.express %>/
63
+
64
+ ```bash
65
+ cd <%= paths.express %> && cp .env.example .env && <%= pm.install %><%= orm === 'drizzle' ? ` && ${pm.exec} drizzle-kit push --force` : '' %> && <%= pm.run %> dev
66
+ ```
67
+
68
+ Health check at `http://localhost:3000/api/health`.
69
+ <% } %>
56
70
  <% if (components.includes('frontend')) { %>
57
71
 
58
72
  ### <%= paths.frontend %>/
@@ -79,6 +93,9 @@ cd <%= paths.fastapi %> && uv run pytest
79
93
  <% if (components.includes('fastify')) { %>
80
94
  cd <%= paths.fastify %> && <%= pm.run %> test
81
95
  <% } %>
96
+ <% if (components.includes('express')) { %>
97
+ cd <%= paths.express %> && <%= pm.run %> test
98
+ <% } %>
82
99
  <% if (components.includes('frontend')) { %>
83
100
  cd <%= paths.frontend %> && <%= pm.exec %> vitest run
84
101
  <% } %>
@@ -5,13 +5,16 @@ on:
5
5
  branches: [main]
6
6
  push:
7
7
  branches: [main]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+ pull-requests: read
8
13
 
9
14
  jobs:
10
15
  changes:
11
16
  name: Detect changes
12
17
  runs-on: ubuntu-latest
13
- permissions:
14
- pull-requests: read
15
18
  outputs:
16
19
  <% for (const inst of fastapiInstances) { %>
17
20
  <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
@@ -19,6 +22,9 @@ jobs:
19
22
  <% for (const inst of fastifyInstances) { %>
20
23
  <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
21
24
  <% } %>
25
+ <% for (const inst of expressInstances) { %>
26
+ <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
27
+ <% } %>
22
28
  <% for (const inst of frontendInstances) { %>
23
29
  <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
24
30
  <% } %>
@@ -32,8 +38,8 @@ jobs:
32
38
  <%= inst.path %>: ${{ steps.filter.outputs.<%= inst.path %> }}
33
39
  <% } %>
34
40
  steps:
35
- - uses: actions/checkout@v5
36
- - uses: dorny/paths-filter@v3
41
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
42
+ - uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
37
43
  id: filter
38
44
  with:
39
45
  filters: |
@@ -45,6 +51,10 @@ jobs:
45
51
  <%= inst.path %>:
46
52
  - '<%= inst.path %>/**'
47
53
  <% } %>
54
+ <% for (const inst of expressInstances) { %>
55
+ <%= inst.path %>:
56
+ - '<%= inst.path %>/**'
57
+ <% } %>
48
58
  <% for (const inst of frontendInstances) { %>
49
59
  <%= inst.path %>:
50
60
  - '<%= inst.path %>/**'
@@ -66,88 +76,204 @@ jobs:
66
76
  name: Secret scan
67
77
  runs-on: ubuntu-latest
68
78
  steps:
69
- - uses: actions/checkout@v5
79
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
70
80
  with:
71
81
  fetch-depth: 0
72
- - uses: gitleaks/gitleaks-action@v2
82
+ - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2
73
83
  env:
74
84
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
85
+ - run: python3 scripts/style-check.py frontend/src
75
86
  <% for (const inst of fastapiInstances) { %>
76
87
 
77
88
  <%= inst.path %>:
78
- name: <%= inst.display %> (format + lint + typecheck + test + audit)
89
+ name: <%= inst.display %> (format + lint + typecheck + audit)
79
90
  needs: changes
80
- if: needs.changes.outputs.<%= inst.path %> == 'true'
91
+ if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
81
92
  runs-on: ubuntu-latest
93
+ services:
94
+ postgres:
95
+ image: postgres:16
96
+ env:
97
+ POSTGRES_USER: postgres
98
+ POSTGRES_PASSWORD: postgres
99
+ POSTGRES_DB: ci_test
100
+ ports:
101
+ - 5432:5432
102
+ options: >-
103
+ --health-cmd pg_isready
104
+ --health-interval 5s
105
+ --health-timeout 5s
106
+ --health-retries 5
82
107
  defaults:
83
108
  run:
84
109
  working-directory: <%= inst.path %>
110
+ env:
111
+ SQLALCHEMY_DATABASE_URI: postgresql+asyncpg://postgres:postgres@localhost:5432/ci_test
112
+ JWT_PROVIDER: shared_secret
113
+ JWT_SECRET: ci-test-secret # gitleaks:allow
114
+ JWT_ALGORITHMS: HS256
85
115
  steps:
86
- - uses: actions/checkout@v5
87
- - uses: astral-sh/setup-uv@v4
116
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
117
+ - uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
88
118
  - run: uv sync --group dev
89
119
  - run: uv run ruff format --check src tests
90
120
  - run: uv run ruff check src tests
91
121
  - run: uv run mypy
92
- - run: uv run pytest
93
- - run: uv run pip-audit
122
+ - name: Audit Python dependencies
123
+ run: |
124
+ run_pip_audit() {
125
+ for attempt in 1 2 3; do
126
+ if uv run pip-audit --ignore-vuln CVE-2026-3219; then
127
+ return 0
128
+ fi
129
+ if [ "$attempt" -eq 3 ]; then
130
+ return 1
131
+ fi
132
+ sleep $((attempt * 5))
133
+ done
134
+ }
135
+ run_pip_audit
94
136
  <% } %>
95
137
  <% for (const inst of fastifyInstances) { %>
96
138
 
97
139
  <%= inst.path %>:
98
- name: <%= inst.display %> (format + lint + typecheck + audit)
140
+ name: <%= inst.display %> (format + lint + typecheck + build + audit)
141
+ needs: changes
142
+ if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
143
+ runs-on: ubuntu-latest
144
+ services:
145
+ postgres:
146
+ image: postgres:16
147
+ env:
148
+ POSTGRES_USER: postgres
149
+ POSTGRES_PASSWORD: postgres
150
+ POSTGRES_DB: ci_test
151
+ ports:
152
+ - 5432:5432
153
+ options: >-
154
+ --health-cmd pg_isready
155
+ --health-interval 5s
156
+ --health-timeout 5s
157
+ --health-retries 5
158
+ defaults:
159
+ run:
160
+ working-directory: <%= inst.path %>
161
+ env:
162
+ DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ci_test
163
+ JWT_SECRET: ci-test-secret # gitleaks:allow
164
+ steps:
165
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
166
+ <% if (pm === 'pnpm') { %>
167
+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
168
+ with:
169
+ version: 10
170
+ <% } %>
171
+ <% if (pm === 'bun') { %>
172
+ - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
173
+ <% } %>
174
+ <% if (pm !== 'bun') { %>
175
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
176
+ with:
177
+ node-version: 22
178
+ cache: <%= pm.name %>
179
+ cache-dependency-path: <%= inst.path %>/<%= pm.lockfile %>
180
+ <% } %>
181
+ - run: <%= pm.ci %>
182
+ <% if (orm === 'drizzle') { %>
183
+ - run: <%= pm.exec %> drizzle-kit push --force
184
+ <% } else if (orm === 'sequelize' || orm === 'typeorm') { %>
185
+ - run: <%= pm.exec %> tsx scripts/db-sync.ts
186
+ <% } else { %>
187
+ - run: <%= pm.prismaExec %> generate
188
+ - run: <%= pm.prismaExec %> migrate deploy
189
+ <% } %>
190
+ - run: <%= pm.exec %> prettier --check .
191
+ - run: <%= pm.exec %> eslint .
192
+ - run: <%= pm.exec %> tsc --noEmit
193
+ - run: <%= pm.run %> build
194
+ - run: <%= pm.audit %>
195
+ <% } %>
196
+ <% for (const inst of expressInstances) { %>
197
+
198
+ <%= inst.path %>:
199
+ name: <%= inst.display %> (format + lint + typecheck + build + test + audit)
99
200
  needs: changes
100
- if: needs.changes.outputs.<%= inst.path %> == 'true'
201
+ if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
101
202
  runs-on: ubuntu-latest
203
+ services:
204
+ postgres:
205
+ image: postgres:16
206
+ env:
207
+ POSTGRES_USER: postgres
208
+ POSTGRES_PASSWORD: postgres
209
+ POSTGRES_DB: ci_test
210
+ ports:
211
+ - 5432:5432
212
+ options: >-
213
+ --health-cmd pg_isready
214
+ --health-interval 5s
215
+ --health-timeout 5s
216
+ --health-retries 5
102
217
  defaults:
103
218
  run:
104
219
  working-directory: <%= inst.path %>
220
+ env:
221
+ DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ci_test
105
222
  steps:
106
- - uses: actions/checkout@v5
223
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
107
224
  <% if (pm === 'pnpm') { %>
108
- - uses: pnpm/action-setup@v4
225
+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
109
226
  with:
110
227
  version: 10
111
228
  <% } %>
112
229
  <% if (pm === 'bun') { %>
113
- - uses: oven-sh/setup-bun@v2
230
+ - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
114
231
  <% } %>
115
232
  <% if (pm !== 'bun') { %>
116
- - uses: actions/setup-node@v5
233
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
117
234
  with:
118
- node-version: 20
235
+ node-version: 22
119
236
  cache: <%= pm.name %>
120
237
  cache-dependency-path: <%= inst.path %>/<%= pm.lockfile %>
121
238
  <% } %>
122
239
  - run: <%= pm.ci %>
240
+ <% if (orm === 'drizzle') { %>
241
+ - run: <%= pm.exec %> drizzle-kit push --force
242
+ <% } else if (orm === 'sequelize' || orm === 'typeorm') { %>
243
+ - run: <%= pm.exec %> tsx scripts/db-sync.ts
244
+ <% } else { %>
123
245
  - run: <%= pm.prismaExec %> generate
246
+ - run: <%= pm.prismaExec %> migrate deploy
247
+ <% } %>
124
248
  - run: <%= pm.exec %> prettier --check .
125
249
  - run: <%= pm.exec %> eslint .
126
250
  - run: <%= pm.exec %> tsc --noEmit
251
+ - run: <%= pm.run %> build
252
+ - run: <%= pm.exec %> vitest run --coverage.enabled=false
127
253
  - run: <%= pm.audit %>
128
254
  <% } %>
129
255
  <% for (const inst of frontendInstances) { %>
130
256
 
131
257
  <%= inst.path %>:
132
- name: <%= inst.display %> (format + lint + typecheck + audit)
258
+ name: <%= inst.display %> (format + lint + typecheck + build + audit)
133
259
  needs: changes
134
- if: needs.changes.outputs.<%= inst.path %> == 'true'
260
+ if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
135
261
  runs-on: ubuntu-latest
136
262
  defaults:
137
263
  run:
138
264
  working-directory: <%= inst.path %>
139
265
  steps:
140
- - uses: actions/checkout@v5
266
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
141
267
  <% if (pm === 'pnpm') { %>
142
- - uses: pnpm/action-setup@v4
268
+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
143
269
  with:
144
270
  version: 10
145
271
  <% } %>
146
272
  <% if (pm === 'bun') { %>
147
- - uses: oven-sh/setup-bun@v2
273
+ - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
148
274
  <% } %>
149
275
  <% if (pm !== 'bun') { %>
150
- - uses: actions/setup-node@v5
276
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
151
277
  with:
152
278
  node-version: 22
153
279
  cache: <%= pm.name %>
@@ -157,50 +283,54 @@ jobs:
157
283
  - run: <%= pm.exec %> prettier --check .
158
284
  - run: <%= pm.exec %> eslint 'src/**/*.{ts,tsx}'
159
285
  - run: <%= pm.exec %> tsc --noEmit
286
+ - run: <%= pm.run %> build
287
+ - run: bash scripts/check-bundle-size.sh
160
288
  - run: <%= pm.audit %>
161
289
  <% } %>
162
290
  <% for (const inst of mobileInstances) { %>
163
291
 
164
292
  <%= inst.path %>:
165
- name: <%= inst.display %> (format + analyze)
293
+ name: <%= inst.display %> (format + analyze + test + coverage)
166
294
  needs: changes
167
- if: needs.changes.outputs.<%= inst.path %> == 'true'
295
+ if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
168
296
  runs-on: ubuntu-latest
169
297
  defaults:
170
298
  run:
171
299
  working-directory: <%= inst.path %>
172
300
  steps:
173
- - uses: actions/checkout@v5
174
- - uses: subosito/flutter-action@v2
301
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
302
+ - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
175
303
  with:
176
304
  channel: stable
177
305
  - run: flutter pub get
178
306
  - run: dart run build_runner build --delete-conflicting-outputs
179
307
  - run: dart format --set-exit-if-changed .
180
308
  - run: dart analyze --fatal-infos
309
+ - run: flutter test --coverage
310
+ - run: bash scripts/check-coverage.sh
181
311
  <% } %>
182
312
  <% for (const inst of e2eInstances) { %>
183
313
 
184
314
  <%= inst.path %>:
185
315
  name: <%= inst.display %> (format + lint + typecheck + audit)
186
316
  needs: changes
187
- if: needs.changes.outputs.<%= inst.path %> == 'true'
317
+ if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
188
318
  runs-on: ubuntu-latest
189
319
  defaults:
190
320
  run:
191
321
  working-directory: <%= inst.path %>
192
322
  steps:
193
- - uses: actions/checkout@v5
323
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
194
324
  <% if (pm === 'pnpm') { %>
195
- - uses: pnpm/action-setup@v4
325
+ - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
196
326
  with:
197
327
  version: 10
198
328
  <% } %>
199
329
  <% if (pm === 'bun') { %>
200
- - uses: oven-sh/setup-bun@v2
330
+ - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
201
331
  <% } %>
202
332
  <% if (pm !== 'bun') { %>
203
- - uses: actions/setup-node@v5
333
+ - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
204
334
  with:
205
335
  node-version: 22
206
336
  cache: <%= pm.name %>
@@ -217,14 +347,14 @@ jobs:
217
347
  <%= inst.path %>:
218
348
  name: <%= inst.display %> (fmt + validate)
219
349
  needs: changes
220
- if: needs.changes.outputs.<%= inst.path %> == 'true'
350
+ if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
221
351
  runs-on: ubuntu-latest
222
352
  defaults:
223
353
  run:
224
354
  working-directory: <%= inst.path %>/stack
225
355
  steps:
226
- - uses: actions/checkout@v5
227
- - uses: hashicorp/setup-terraform@v3
356
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
357
+ - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3
228
358
  with:
229
359
  terraform_version: '1.11'
230
360
  - run: terraform fmt -check -recursive
@@ -30,12 +30,17 @@ services:
30
30
  retries: 3
31
31
  start_period: 15s
32
32
  networks:
33
- - app-network
33
+ app-network:
34
+ <% if (fastifyInstances.length === 0 && expressInstances.length === 0 && inst.path === fastapiInstances[0].path) { %>
35
+ aliases:
36
+ - backend
37
+ <% } %>
34
38
  <% } %>
35
39
  <% for (const inst of fastifyInstances) { %>
36
40
  <%= inst.path %>-migrate:
37
- build: ./<%= inst.path %>
38
- command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
41
+ build:
42
+ context: ./<%= inst.path %>
43
+ target: migrate
39
44
  env_file:
40
45
  - ./<%= inst.path %>/.env
41
46
  networks:
@@ -51,13 +56,61 @@ services:
51
56
  <%= inst.path %>-migrate:
52
57
  condition: service_completed_successfully
53
58
  healthcheck:
54
- test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
59
+ test:
60
+ [
61
+ "CMD",
62
+ "node",
63
+ "-e",
64
+ "require('http').get('http://localhost:3000/api/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))",
65
+ ]
55
66
  interval: 30s
56
67
  timeout: 10s
57
68
  retries: 3
58
69
  start_period: 15s
70
+ networks:
71
+ app-network:
72
+ <% if (inst.path === fastifyInstances[0].path) { %>
73
+ aliases:
74
+ - backend
75
+ <% } %>
76
+ <% } %>
77
+ <% for (const inst of expressInstances) { %>
78
+ <%= inst.path %>-migrate:
79
+ build:
80
+ context: ./<%= inst.path %>
81
+ target: migrate
82
+ env_file:
83
+ - ./<%= inst.path %>/.env
59
84
  networks:
60
85
  - app-network
86
+ <%= inst.path %>:
87
+ build: ./<%= inst.path %>
88
+ expose:
89
+ - "3000"
90
+ env_file:
91
+ - ./<%= inst.path %>/.env
92
+ restart: unless-stopped
93
+ depends_on:
94
+ <%= inst.path %>-migrate:
95
+ condition: service_completed_successfully
96
+ healthcheck:
97
+ test:
98
+ [
99
+ "CMD",
100
+ "node",
101
+ "-e",
102
+ "require('http').get('http://localhost:3000/api/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))",
103
+ ]
104
+ interval: 30s
105
+ timeout: 10s
106
+ retries: 3
107
+ start_period: 15s
108
+ networks:
109
+ app-network:
110
+ <% if (fastifyInstances.length === 0 && inst.path === expressInstances[0].path) { %>
111
+ aliases:
112
+ - backend
113
+ <% } %>
61
114
  <% } %>
62
115
  <% for (const inst of frontendInstances) { %>
63
116
  <%= inst.path %>:
@@ -65,6 +118,8 @@ services:
65
118
  context: ./<%= inst.path %>
66
119
  args:
67
120
  VITE_API_URL: ""
121
+ environment:
122
+ DOMAIN: ${DOMAIN:-localhost}
68
123
  ports:
69
124
  - "80:80"
70
125
  - "443:443"
@@ -75,6 +130,10 @@ services:
75
130
  depends_on:
76
131
  <%= fastifyInstances[0].path %>:
77
132
  condition: service_healthy
133
+ <% } else if (expressInstances.length > 0) { %>
134
+ depends_on:
135
+ <%= expressInstances[0].path %>:
136
+ condition: service_healthy
78
137
  <% } else if (fastapiInstances.length > 0) { %>
79
138
  depends_on:
80
139
  <%= fastapiInstances[0].path %>:
@@ -82,7 +141,15 @@ services:
82
141
  <% } %>
83
142
  restart: unless-stopped
84
143
  healthcheck:
85
- test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/"]
144
+ test:
145
+ [
146
+ "CMD",
147
+ "wget",
148
+ "--spider",
149
+ "-q",
150
+ "--no-check-certificate",
151
+ "https://127.0.0.1/",
152
+ ]
86
153
  interval: 30s
87
154
  timeout: 5s
88
155
  retries: 3
@@ -4,17 +4,20 @@ set -e
4
4
  # ── Secret detection ──────────────────────────────────────────────
5
5
  STAGED_DIFF="$(git diff --cached --diff-filter=ACMR)"
6
6
 
7
- if echo "$STAGED_DIFF" | grep -qE '(AKIA|ASIA)[A-Z0-9]{16}'; then
8
- echo "ERROR: Possible AWS access key detected in staged changes."
7
+ AKIA_HITS="$(echo "$STAGED_DIFF" | grep -E '(AKIA|ASIA)[A-Z0-9]{16}' | grep -vE '(pragma: allowlist secret|gitleaks:allow)' || true)"
8
+ if [ -n "$AKIA_HITS" ]; then
9
+ echo "ERROR: Possible AWS access key detected in staged changes:"
10
+ echo "$AKIA_HITS" | head -5
11
+ echo "Add 'pragma: allowlist secret' on the line to whitelist if intentional."
9
12
  exit 1
10
13
  fi
11
14
 
12
15
  if echo "$STAGED_DIFF" | grep -qE '(password|secret|token)\s*[:=]\s*["\x27][^"'\'']{8,}' 2>/dev/null; then
13
- REAL_SECRETS="$(echo "$STAGED_DIFF" | grep -E '(password|secret|token)\s*[:=]\s*["\x27][^"'\'']{8,}' | grep -vE '(test-secret|dev-secret|example|placeholder|your-|changeme|CHANGE_ME)' || true)"
16
+ REAL_SECRETS="$(echo "$STAGED_DIFF" | grep -E '(password|secret|token)\s*[:=]\s*["\x27][^"'\'']{8,}' | grep -vE '(test-secret|dev-secret|example|placeholder|your-|changeme|CHANGE_ME|pragma: allowlist secret|gitleaks:allow)' || true)"
14
17
  if [ -n "$REAL_SECRETS" ]; then
15
18
  echo "WARNING: Possible secret detected in staged changes:"
16
19
  echo "$REAL_SECRETS" | head -5
17
- echo "If intentional, commit with: git commit --no-verify"
20
+ echo "Add 'pragma: allowlist secret' on the line to whitelist, or commit with --no-verify if intentional."
18
21
  exit 1
19
22
  fi
20
23
  fi
@@ -36,12 +39,33 @@ if [ -n "$<%= inst.upper %>_PY" ]; then
36
39
  echo "$<%= inst.upper %>_PY" | sed 's|^<%= inst.path %>/||' | xargs uv run ruff format
37
40
  echo "$<%= inst.upper %>_PY" | sed 's|^<%= inst.path %>/||' | xargs uv run ruff check --fix
38
41
  uv run mypy
42
+ if grep -rEn 'from src\.[a-z_]+(\.[a-z_]+)?\._[a-z_]+ import' src/; then
43
+ echo "ERROR: src/ files cannot import from another module's _-prefixed file. Import from the package."
44
+ exit 1
45
+ fi
46
+ uv run lint-imports
39
47
  cd ..
40
48
  echo "$<%= inst.upper %>_PY" | xargs git add
41
49
  fi
42
50
  <% } %>
43
51
  <% for (const inst of fastifyInstances) { %>
44
52
 
53
+ <%= inst.upper %>_TS=$(echo "$STAGED_FILES" | grep '^<%= inst.path %>/.*\.ts$' || true)
54
+ <%= inst.upper %>_ALL=$(echo "$STAGED_FILES" | grep '^<%= inst.path %>/' || true)
55
+ if [ -n "$<%= inst.upper %>_ALL" ]; then
56
+ echo "Formatting <%= inst.path %>..."
57
+ cd <%= inst.path %>
58
+ echo "$<%= inst.upper %>_ALL" | sed 's|^<%= inst.path %>/||' | xargs <%= pm.exec %> prettier --write --ignore-unknown
59
+ if [ -n "$<%= inst.upper %>_TS" ]; then
60
+ echo "$<%= inst.upper %>_TS" | sed 's|^<%= inst.path %>/||' | xargs <%= pm.exec %> eslint --fix
61
+ <%= pm.exec %> tsc --noEmit
62
+ fi
63
+ cd ..
64
+ echo "$<%= inst.upper %>_ALL" | xargs git add
65
+ fi
66
+ <% } %>
67
+ <% for (const inst of expressInstances) { %>
68
+
45
69
  <%= inst.upper %>_TS=$(echo "$STAGED_FILES" | grep '^<%= inst.path %>/.*\.ts$' || true)
46
70
  <%= inst.upper %>_ALL=$(echo "$STAGED_FILES" | grep '^<%= inst.path %>/' || true)
47
71
  if [ -n "$<%= inst.upper %>_ALL" ]; then