create-projx 1.4.0 → 1.4.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.
package/README.md CHANGED
@@ -131,6 +131,8 @@ npx create-projx pin <patterns...>
131
131
  npx create-projx unpin <patterns...>
132
132
  npx create-projx pin --list
133
133
  npx create-projx doctor [--fix]
134
+ npx create-projx gen entity <name>
135
+ npx create-projx sync [--url <url>]
134
136
 
135
137
  --components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
136
138
  --no-git Skip git init
@@ -174,6 +176,46 @@ npx create-projx doctor --fix # auto-fix what's possible
174
176
 
175
177
  Checks: config validity, component markers, baseline ref, stale worktrees, skip pattern coverage.
176
178
 
179
+ ### Generate Entities
180
+
181
+ Scaffold a new entity across all components in your project:
182
+
183
+ ```bash
184
+ npx create-projx gen entity invoice # interactive
185
+ npx create-projx gen entity invoice --fields "name:string,amount:number" # non-interactive
186
+ ```
187
+
188
+ Generates based on what's in your `.projx`:
189
+
190
+ | Component | Generated |
191
+ | --------- | --------- |
192
+ | `fastapi` | `src/entities/<name>/_model.py` — auto-discovered by registry |
193
+ | `fastify` | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + app.ts import |
194
+ | `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
195
+ | `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
196
+
197
+ No migrations — run `alembic revision --autogenerate` or `npx prisma migrate dev` when ready.
198
+
199
+ ### Sync Types
200
+
201
+ Regenerate all frontend/mobile types from a running backend:
202
+
203
+ ```bash
204
+ npx create-projx sync # auto-detects URL
205
+ npx create-projx sync --url http://localhost:8000/api/v1/_meta # explicit URL
206
+ ```
207
+
208
+ Fetches `/_meta` from your backend, generates typed interfaces for every entity. Run after any backend change — new field, renamed column, new entity.
209
+
210
+ The generic `api.ts` client accepts type parameters:
211
+
212
+ ```tsx
213
+ import type { Invoice } from '../types/invoice';
214
+
215
+ const { data } = await api.list<Invoice>('/invoices'); // data: Invoice[]
216
+ const item = await api.get<Invoice>('/invoices', id); // item: Invoice
217
+ ```
218
+
177
219
  ## Rename Component Directories
178
220
 
179
221
  Rename `fastapi/` to `backend/`? Just rename the folder — the `.projx-component` marker file moves with it. The `update` command auto-discovers where each component lives by scanning for these markers. No config changes needed.
package/dist/index.js CHANGED
@@ -251,6 +251,35 @@ async function discoverComponentPaths(cwd, components) {
251
251
  }
252
252
  return paths;
253
253
  }
254
+ async function discoverComponentsFromMarkers(cwd) {
255
+ const components = [];
256
+ const paths = {};
257
+ const entries = await readdir(cwd, { withFileTypes: true });
258
+ for (const entry of entries) {
259
+ if (!entry.isDirectory()) continue;
260
+ if (EXCLUDE.has(entry.name)) continue;
261
+ if (entry.name.startsWith(".")) continue;
262
+ const full = join(cwd, entry.name);
263
+ const marker = join(full, COMPONENT_MARKER);
264
+ if (existsSync(marker)) {
265
+ try {
266
+ const data = JSON.parse(await readFile(marker, "utf-8"));
267
+ const markerComponents = data.components ?? (data.component ? [data.component] : []);
268
+ for (const mc of markerComponents) {
269
+ if (COMPONENTS.includes(mc) && !components.includes(mc)) {
270
+ components.push(mc);
271
+ paths[mc] = entry.name;
272
+ }
273
+ }
274
+ } catch {
275
+ }
276
+ }
277
+ }
278
+ for (const c of components) {
279
+ if (!paths[c]) paths[c] = c;
280
+ }
281
+ return { components, paths };
282
+ }
254
283
  function render(template, vars) {
255
284
  const components = vars.components;
256
285
  const projectName = vars.projectName;
@@ -261,12 +290,29 @@ function render(template, vars) {
261
290
  const ifMatch = line.match(/^<%\s*if\s*\((.+?)\)\s*\{?\s*%>$/);
262
291
  if (ifMatch) {
263
292
  const fn = new Function("components", "projectName", `return ${ifMatch[1]}`);
264
- stack.push(fn(components, projectName));
293
+ const result = fn(components, projectName);
294
+ stack.push({ active: result, matched: result });
295
+ continue;
296
+ }
297
+ const elseIfMatch = line.match(/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/);
298
+ if (elseIfMatch) {
299
+ if (stack.length > 0) {
300
+ const top = stack[stack.length - 1];
301
+ if (top.matched) {
302
+ top.active = false;
303
+ } else {
304
+ const fn = new Function("components", "projectName", `return ${elseIfMatch[1]}`);
305
+ const result = fn(components, projectName);
306
+ top.active = result;
307
+ if (result) top.matched = true;
308
+ }
309
+ }
265
310
  continue;
266
311
  }
267
312
  if (/^<%\s*\}\s*else\s*\{?\s*%>$/.test(line)) {
268
313
  if (stack.length > 0) {
269
- stack[stack.length - 1] = !stack[stack.length - 1];
314
+ const top = stack[stack.length - 1];
315
+ top.active = !top.matched;
270
316
  }
271
317
  continue;
272
318
  }
@@ -274,7 +320,7 @@ function render(template, vars) {
274
320
  stack.pop();
275
321
  continue;
276
322
  }
277
- if (stack.length > 0 && stack.some((v) => !v)) continue;
323
+ if (stack.length > 0 && stack.some((v) => !v.active)) continue;
278
324
  const replaced = line.replace(
279
325
  /<%=\s*([\w.]+)\s*%>/g,
280
326
  (_, expr) => {
@@ -908,17 +954,19 @@ async function update(cwd, localRepo) {
908
954
  const configPath = join5(cwd, ".projx");
909
955
  let config;
910
956
  if (existsSync4(configPath)) {
911
- config = JSON.parse(await readFile5(configPath, "utf-8"));
957
+ const raw = JSON.parse(await readFile5(configPath, "utf-8"));
958
+ const { components: discovered } = await discoverComponentsFromMarkers(cwd);
959
+ config = { ...raw, components: discovered.length > 0 ? discovered : raw.components };
912
960
  p3.log.info(`Found .projx (v${config.version}, components: ${config.components.join(", ")})`);
913
961
  } else {
914
962
  p3.log.warn("No .projx file found. Detecting components from directories.");
915
- const detected = COMPONENTS.filter((c) => existsSync4(join5(cwd, c)));
916
- if (detected.length === 0) {
963
+ const { components: discovered } = await discoverComponentsFromMarkers(cwd);
964
+ if (discovered.length === 0) {
917
965
  p3.log.error("No projx components found. Run 'projx init' first.");
918
966
  process.exit(1);
919
967
  }
920
- config = { version: "0.0.0", components: detected, createdAt: "unknown" };
921
- p3.log.info(`Detected: ${detected.join(", ")}`);
968
+ config = { version: "0.0.0", components: discovered, createdAt: "unknown" };
969
+ p3.log.info(`Detected: ${discovered.join(", ")}`);
922
970
  }
923
971
  const componentPaths = await discoverComponentPaths(cwd, config.components);
924
972
  for (const c of config.components) {
@@ -1476,7 +1524,7 @@ async function pin(cwd, patterns) {
1476
1524
  process.exit(1);
1477
1525
  }
1478
1526
  const config = JSON.parse(await readFile8(configPath, "utf-8"));
1479
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1527
+ const componentPaths = (await discoverComponentsFromMarkers(cwd)).paths;
1480
1528
  const rootAdds = [];
1481
1529
  const componentAdds = {};
1482
1530
  for (const pattern of patterns) {
@@ -1533,7 +1581,7 @@ async function unpin(cwd, patterns) {
1533
1581
  process.exit(1);
1534
1582
  }
1535
1583
  const config = JSON.parse(await readFile8(configPath, "utf-8"));
1536
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1584
+ const componentPaths = (await discoverComponentsFromMarkers(cwd)).paths;
1537
1585
  const rootRemoves = [];
1538
1586
  const componentRemoves = {};
1539
1587
  for (const pattern of patterns) {
@@ -1594,7 +1642,7 @@ async function listPins(cwd) {
1594
1642
  process.exit(1);
1595
1643
  }
1596
1644
  const config = JSON.parse(await readFile8(configPath, "utf-8"));
1597
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1645
+ const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
1598
1646
  let hasAny = false;
1599
1647
  if (config.skip && config.skip.length > 0) {
1600
1648
  hasAny = true;
@@ -1603,7 +1651,7 @@ async function listPins(cwd) {
1603
1651
  p6.log.info(` ${s}`);
1604
1652
  }
1605
1653
  }
1606
- for (const component of config.components) {
1654
+ for (const component of discovered) {
1607
1655
  const dir = componentPaths[component];
1608
1656
  const marker = await readComponentMarker(join9(cwd, dir));
1609
1657
  if (marker?.skip && marker.skip.length > 0) {
@@ -1860,10 +1908,11 @@ async function doctor(cwd, fix = false) {
1860
1908
  printReport(allResults);
1861
1909
  process.exit(1);
1862
1910
  }
1863
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1864
- allResults.push(...await checkComponents(cwd, config, componentPaths));
1911
+ const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
1912
+ const resolvedConfig = { ...config, components: discovered.length > 0 ? discovered : config.components };
1913
+ allResults.push(...await checkComponents(cwd, resolvedConfig, componentPaths));
1865
1914
  allResults.push(...checkGit(cwd, fix));
1866
- allResults.push(...await checkSkipPatterns(cwd, config, componentPaths));
1915
+ allResults.push(...await checkSkipPatterns(cwd, resolvedConfig, componentPaths));
1867
1916
  printReport(allResults);
1868
1917
  const passed = allResults.filter((r) => r.status === "pass").length;
1869
1918
  const warns = allResults.filter((r) => r.status === "warn").length;
@@ -1918,8 +1967,9 @@ async function diff(cwd, localRepo) {
1918
1967
  p8.log.error("No .projx file found. Run 'npx create-projx init' first.");
1919
1968
  process.exit(1);
1920
1969
  }
1921
- const config = JSON.parse(await readFile10(configPath, "utf-8"));
1922
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1970
+ const raw = JSON.parse(await readFile10(configPath, "utf-8"));
1971
+ const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
1972
+ const config = { ...raw, components: discovered.length > 0 ? discovered : raw.components };
1923
1973
  const componentSkips = {};
1924
1974
  for (const component of config.components) {
1925
1975
  const dir = componentPaths[component];
@@ -2579,12 +2629,11 @@ async function gen(cwd, entityName, fieldsFlag) {
2579
2629
  p9.log.error("No .projx file found. Run 'npx create-projx init' first.");
2580
2630
  process.exit(1);
2581
2631
  }
2582
- const projxConfig = JSON.parse(await readFile11(configPath, "utf-8"));
2583
- const componentPaths = await discoverComponentPaths(cwd, projxConfig.components);
2584
- const hasFastapi = projxConfig.components.includes("fastapi");
2585
- const hasFastify = projxConfig.components.includes("fastify");
2586
- const hasFrontend = projxConfig.components.includes("frontend");
2587
- const hasMobile = projxConfig.components.includes("mobile");
2632
+ const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
2633
+ const hasFastapi = discovered.includes("fastapi");
2634
+ const hasFastify = discovered.includes("fastify");
2635
+ const hasFrontend = discovered.includes("frontend");
2636
+ const hasMobile = discovered.includes("mobile");
2588
2637
  if (!hasFastapi && !hasFastify) {
2589
2638
  p9.log.error("No backend component found. Need fastapi or fastify.");
2590
2639
  process.exit(1);
@@ -2732,7 +2781,7 @@ async function gen(cwd, entityName, fieldsFlag) {
2732
2781
 
2733
2782
  // src/sync.ts
2734
2783
  import { existsSync as existsSync12, readFileSync as readFileSync2 } from "fs";
2735
- import { readFile as readFile12, writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
2784
+ import { writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
2736
2785
  import { join as join13 } from "path";
2737
2786
  import * as p10 from "@clack/prompts";
2738
2787
  function toPascal2(s) {
@@ -2909,15 +2958,9 @@ async function sync(cwd, url) {
2909
2958
  p10.log.error("No .projx file found. Run 'npx create-projx init' first.");
2910
2959
  process.exit(1);
2911
2960
  }
2912
- const projxConfig = JSON.parse(
2913
- await readFile12(configPath, "utf-8")
2914
- );
2915
- const componentPaths = await discoverComponentPaths(
2916
- cwd,
2917
- projxConfig.components
2918
- );
2919
- const hasFrontend = projxConfig.components.includes("frontend");
2920
- const hasMobile = projxConfig.components.includes("mobile");
2961
+ const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
2962
+ const hasFrontend = discovered.includes("frontend");
2963
+ const hasMobile = discovered.includes("mobile");
2921
2964
  if (!hasFrontend && !hasMobile) {
2922
2965
  p10.log.error("No frontend or mobile component found. Nothing to sync.");
2923
2966
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
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.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@ services:
24
24
  - app-network
25
25
  <% } %>
26
26
  <% if (components.includes('fastapi')) { %>
27
- migrate:
27
+ <%= paths.fastapi %>-migrate:
28
28
  build: ./<%= paths.fastapi %>
29
29
  command: ['uv', 'run', 'migrate.py']
30
30
  environment:
@@ -39,8 +39,7 @@ services:
39
39
  cpus: '0.5'
40
40
  networks:
41
41
  - app-network
42
-
43
- backend:
42
+ <%= paths.fastapi %>:
44
43
  build: ./<%= paths.fastapi %>
45
44
  command:
46
45
  [
@@ -61,7 +60,7 @@ services:
61
60
  - ./<%= paths.fastapi %>/alembic.ini:/app/alembic.ini
62
61
  - ./<%= paths.fastapi %>/migrate.py:/app/migrate.py
63
62
  depends_on:
64
- migrate:
63
+ <%= paths.fastapi %>-migrate:
65
64
  condition: service_completed_successfully
66
65
  restart: unless-stopped
67
66
  healthcheck:
@@ -83,7 +82,22 @@ services:
83
82
  - app-network
84
83
  <% } %>
85
84
  <% if (components.includes('fastify')) { %>
86
- backend-fastify:
85
+ <%= paths.fastify %>-migrate:
86
+ build: ./<%= paths.fastify %>
87
+ command: ['pnpm', 'prisma', 'migrate', 'deploy']
88
+ environment:
89
+ - DATABASE_URL=postgresql://dev:dev@db:5432/app
90
+ depends_on:
91
+ db:
92
+ condition: service_healthy
93
+ deploy:
94
+ resources:
95
+ limits:
96
+ memory: 256M
97
+ cpus: '0.5'
98
+ networks:
99
+ - app-network
100
+ <%= paths.fastify %>:
87
101
  build: ./<%= paths.fastify %>
88
102
  command: ['pnpm', 'dev']
89
103
  ports:
@@ -96,8 +110,8 @@ services:
96
110
  volumes:
97
111
  - ./<%= paths.fastify %>/src:/app/src
98
112
  depends_on:
99
- db:
100
- condition: service_healthy
113
+ <%= paths.fastify %>-migrate:
114
+ condition: service_completed_successfully
101
115
  restart: unless-stopped
102
116
  healthcheck:
103
117
  test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:3000/api/health']
@@ -121,23 +135,21 @@ services:
121
135
  ports:
122
136
  - '5173:5173'
123
137
  environment:
124
- <% if (components.includes('fastapi')) { %>
125
- - VITE_API_URL=http://localhost:7860
126
- <% } %>
127
- <% if (components.includes('fastify') && !components.includes('fastapi')) { %>
138
+ <% if (components.includes('fastify')) { %>
128
139
  - VITE_API_URL=http://localhost:3000
140
+ <% } else if (components.includes('fastapi')) { %>
141
+ - VITE_API_URL=http://localhost:7860
129
142
  <% } %>
130
143
  volumes:
131
144
  - ./<%= paths.frontend %>:/app
132
145
  - frontend_node_modules:/app/node_modules
133
- <% if (components.includes('fastapi')) { %>
146
+ <% if (components.includes('fastify')) { %>
134
147
  depends_on:
135
- backend:
148
+ <%= paths.fastify %>:
136
149
  condition: service_healthy
137
- <% } %>
138
- <% if (components.includes('fastify') && !components.includes('fastapi')) { %>
150
+ <% } else if (components.includes('fastapi')) { %>
139
151
  depends_on:
140
- backend-fastify:
152
+ <%= paths.fastapi %>:
141
153
  condition: service_healthy
142
154
  <% } %>
143
155
  healthcheck:
@@ -154,7 +166,6 @@ services:
154
166
  networks:
155
167
  - app-network
156
168
  <% } %>
157
-
158
169
  volumes:
159
170
  <% if (components.includes('fastapi') || components.includes('fastify')) { %>
160
171
  pgdata:
@@ -162,7 +173,6 @@ volumes:
162
173
  <% if (components.includes('frontend')) { %>
163
174
  frontend_node_modules:
164
175
  <% } %>
165
-
166
176
  networks:
167
177
  app-network:
168
178
  driver: bridge
@@ -1,14 +1,13 @@
1
1
  services:
2
2
  <% if (components.includes('fastapi')) { %>
3
- migrate:
3
+ <%= paths.fastapi %>-migrate:
4
4
  build: ./<%= paths.fastapi %>
5
5
  command: ["uv", "run", "migrate.py"]
6
6
  env_file:
7
7
  - ./<%= paths.fastapi %>/.env
8
8
  networks:
9
9
  - app-network
10
-
11
- backend:
10
+ <%= paths.fastapi %>:
12
11
  build: ./<%= paths.fastapi %>
13
12
  expose:
14
13
  - "7860"
@@ -16,7 +15,7 @@ services:
16
15
  - ./<%= paths.fastapi %>/.env
17
16
  restart: unless-stopped
18
17
  depends_on:
19
- migrate:
18
+ <%= paths.fastapi %>-migrate:
20
19
  condition: service_completed_successfully
21
20
  healthcheck:
22
21
  test:
@@ -34,13 +33,23 @@ services:
34
33
  - app-network
35
34
  <% } %>
36
35
  <% if (components.includes('fastify')) { %>
37
- backend-fastify:
36
+ <%= paths.fastify %>-migrate:
37
+ build: ./<%= paths.fastify %>
38
+ command: ["pnpm", "prisma", "migrate", "deploy"]
39
+ env_file:
40
+ - ./<%= paths.fastify %>/.env
41
+ networks:
42
+ - app-network
43
+ <%= paths.fastify %>:
38
44
  build: ./<%= paths.fastify %>
39
45
  expose:
40
46
  - "3000"
41
47
  env_file:
42
48
  - ./<%= paths.fastify %>/.env
43
49
  restart: unless-stopped
50
+ depends_on:
51
+ <%= paths.fastify %>-migrate:
52
+ condition: service_completed_successfully
44
53
  healthcheck:
45
54
  test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
46
55
  interval: 30s
@@ -62,14 +71,13 @@ services:
62
71
  volumes:
63
72
  - letsencrypt:/etc/letsencrypt
64
73
  - certbot-www:/var/www/certbot
65
- <% if (components.includes('fastapi')) { %>
74
+ <% if (components.includes('fastify')) { %>
66
75
  depends_on:
67
- backend:
76
+ <%= paths.fastify %>:
68
77
  condition: service_healthy
69
- <% } %>
70
- <% if (components.includes('fastify') && !components.includes('fastapi')) { %>
78
+ <% } else if (components.includes('fastapi')) { %>
71
79
  depends_on:
72
- backend-fastify:
80
+ <%= paths.fastapi %>:
73
81
  condition: service_healthy
74
82
  <% } %>
75
83
  restart: unless-stopped
@@ -81,7 +89,6 @@ services:
81
89
  start_period: 10s
82
90
  networks:
83
91
  - app-network
84
-
85
92
  certbot:
86
93
  image: certbot/certbot:latest
87
94
  volumes:
@@ -102,7 +109,6 @@ volumes:
102
109
  letsencrypt:
103
110
  certbot-www:
104
111
  <% } %>
105
-
106
112
  networks:
107
113
  app-network:
108
114
  driver: bridge