create-projx 1.4.1 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +16 -6
  2. package/dist/index.js +107 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/create-projx)](https://www.npmjs.com/package/create-projx)
4
4
  [![CI](https://github.com/ukanhaupa/projx/actions/workflows/ci.yml/badge.svg)](https://github.com/ukanhaupa/projx/actions/workflows/ci.yml)
5
+ [![GitHub stars](https://img.shields.io/github/stars/ukanhaupa/projx)](https://github.com/ukanhaupa/projx)
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
8
  Production-grade project scaffolder. Pick your stack, get a fully wired project with auth, database, CI/CD, and E2E tests — ready to deploy.
@@ -111,7 +112,7 @@ To skip root-level files (docker-compose, README), add `skip` to `.projx`:
111
112
 
112
113
  ```json
113
114
  {
114
- "version": "1.3.6",
115
+ "version": "1.4.2",
115
116
  "components": ["fastapi", "frontend"],
116
117
  "skip": ["docker-compose.yml", "README.md"]
117
118
  }
@@ -131,10 +132,12 @@ npx create-projx pin <patterns...>
131
132
  npx create-projx unpin <patterns...>
132
133
  npx create-projx pin --list
133
134
  npx create-projx doctor [--fix]
134
- npx create-projx gen entity <name>
135
+ npx create-projx gen entity <name> [--ai | --backend]
135
136
  npx create-projx sync [--url <url>]
136
137
 
137
138
  --components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
139
+ --ai Target fastapi (AI/ML) for gen entity
140
+ --backend Target fastify (API backend) for gen entity
138
141
  --no-git Skip git init
139
142
  --no-install Skip dependency installation
140
143
  -y, --yes Accept defaults (fastify + frontend + e2e)
@@ -178,19 +181,26 @@ Checks: config validity, component markers, baseline ref, stale worktrees, skip
178
181
 
179
182
  ### Generate Entities
180
183
 
181
- Scaffold a new entity across all components in your project:
184
+ Scaffold a new entity in your primary backend + typed models for frontend/mobile:
182
185
 
183
186
  ```bash
184
187
  npx create-projx gen entity invoice # interactive
185
188
  npx create-projx gen entity invoice --fields "name:string,amount:number" # non-interactive
189
+ npx create-projx gen entity embedding --ai --fields "name:string,vector:json" # target AI backend
186
190
  ```
187
191
 
188
- Generates based on what's in your `.projx`:
192
+ When both `fastapi` and `fastify` exist, the entity generates in the **primary backend** only (not both). First run prompts you to choose and saves to `.projx`:
193
+
194
+ ```json
195
+ { "primaryBackend": "fastify" }
196
+ ```
197
+
198
+ Override with `--ai` (fastapi) or `--backend` (fastify).
189
199
 
190
200
  | Component | Generated |
191
201
  | --------- | --------- |
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 |
202
+ | Primary backend (fastapi) | `src/entities/<name>/_model.py` — auto-discovered by registry |
203
+ | Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + app.ts import |
194
204
  | `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
195
205
  | `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
196
206
 
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;
@@ -925,17 +954,19 @@ async function update(cwd, localRepo) {
925
954
  const configPath = join5(cwd, ".projx");
926
955
  let config;
927
956
  if (existsSync4(configPath)) {
928
- 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 };
929
960
  p3.log.info(`Found .projx (v${config.version}, components: ${config.components.join(", ")})`);
930
961
  } else {
931
962
  p3.log.warn("No .projx file found. Detecting components from directories.");
932
- const detected = COMPONENTS.filter((c) => existsSync4(join5(cwd, c)));
933
- if (detected.length === 0) {
963
+ const { components: discovered } = await discoverComponentsFromMarkers(cwd);
964
+ if (discovered.length === 0) {
934
965
  p3.log.error("No projx components found. Run 'projx init' first.");
935
966
  process.exit(1);
936
967
  }
937
- config = { version: "0.0.0", components: detected, createdAt: "unknown" };
938
- p3.log.info(`Detected: ${detected.join(", ")}`);
968
+ config = { version: "0.0.0", components: discovered, createdAt: "unknown" };
969
+ p3.log.info(`Detected: ${discovered.join(", ")}`);
939
970
  }
940
971
  const componentPaths = await discoverComponentPaths(cwd, config.components);
941
972
  for (const c of config.components) {
@@ -1493,7 +1524,7 @@ async function pin(cwd, patterns) {
1493
1524
  process.exit(1);
1494
1525
  }
1495
1526
  const config = JSON.parse(await readFile8(configPath, "utf-8"));
1496
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1527
+ const componentPaths = (await discoverComponentsFromMarkers(cwd)).paths;
1497
1528
  const rootAdds = [];
1498
1529
  const componentAdds = {};
1499
1530
  for (const pattern of patterns) {
@@ -1550,7 +1581,7 @@ async function unpin(cwd, patterns) {
1550
1581
  process.exit(1);
1551
1582
  }
1552
1583
  const config = JSON.parse(await readFile8(configPath, "utf-8"));
1553
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1584
+ const componentPaths = (await discoverComponentsFromMarkers(cwd)).paths;
1554
1585
  const rootRemoves = [];
1555
1586
  const componentRemoves = {};
1556
1587
  for (const pattern of patterns) {
@@ -1611,7 +1642,7 @@ async function listPins(cwd) {
1611
1642
  process.exit(1);
1612
1643
  }
1613
1644
  const config = JSON.parse(await readFile8(configPath, "utf-8"));
1614
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1645
+ const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
1615
1646
  let hasAny = false;
1616
1647
  if (config.skip && config.skip.length > 0) {
1617
1648
  hasAny = true;
@@ -1620,7 +1651,7 @@ async function listPins(cwd) {
1620
1651
  p6.log.info(` ${s}`);
1621
1652
  }
1622
1653
  }
1623
- for (const component of config.components) {
1654
+ for (const component of discovered) {
1624
1655
  const dir = componentPaths[component];
1625
1656
  const marker = await readComponentMarker(join9(cwd, dir));
1626
1657
  if (marker?.skip && marker.skip.length > 0) {
@@ -1877,10 +1908,11 @@ async function doctor(cwd, fix = false) {
1877
1908
  printReport(allResults);
1878
1909
  process.exit(1);
1879
1910
  }
1880
- const componentPaths = await discoverComponentPaths(cwd, config.components);
1881
- 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));
1882
1914
  allResults.push(...checkGit(cwd, fix));
1883
- allResults.push(...await checkSkipPatterns(cwd, config, componentPaths));
1915
+ allResults.push(...await checkSkipPatterns(cwd, resolvedConfig, componentPaths));
1884
1916
  printReport(allResults);
1885
1917
  const passed = allResults.filter((r) => r.status === "pass").length;
1886
1918
  const warns = allResults.filter((r) => r.status === "warn").length;
@@ -1935,8 +1967,9 @@ async function diff(cwd, localRepo) {
1935
1967
  p8.log.error("No .projx file found. Run 'npx create-projx init' first.");
1936
1968
  process.exit(1);
1937
1969
  }
1938
- const config = JSON.parse(await readFile10(configPath, "utf-8"));
1939
- 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 };
1940
1973
  const componentSkips = {};
1941
1974
  for (const component of config.components) {
1942
1975
  const dir = componentPaths[component];
@@ -2589,23 +2622,58 @@ function generateDartModel(config) {
2589
2622
  lines.push("");
2590
2623
  return lines.join("\n");
2591
2624
  }
2592
- async function gen(cwd, entityName, fieldsFlag) {
2625
+ async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
2626
+ if (backendFlag) return backendFlag;
2627
+ if (hasFastapi && !hasFastify) return "fastapi";
2628
+ if (hasFastify && !hasFastapi) return "fastify";
2629
+ const configPath = join12(cwd, ".projx");
2630
+ if (existsSync11(configPath)) {
2631
+ try {
2632
+ const data = JSON.parse(await readFile11(configPath, "utf-8"));
2633
+ if (data.primaryBackend === "fastapi" || data.primaryBackend === "fastify") {
2634
+ return data.primaryBackend;
2635
+ }
2636
+ } catch {
2637
+ }
2638
+ }
2639
+ if (!process.stdin.isTTY) return "fastify";
2640
+ const choice = await p9.select({
2641
+ message: "Both backends detected. Which is your primary?",
2642
+ options: [
2643
+ { value: "fastify", label: "fastify (API backend)" },
2644
+ { value: "fastapi", label: "fastapi (AI/ML engine)" }
2645
+ ],
2646
+ initialValue: "fastify"
2647
+ });
2648
+ if (p9.isCancel(choice)) process.exit(0);
2649
+ try {
2650
+ const data = JSON.parse(await readFile11(configPath, "utf-8"));
2651
+ data.primaryBackend = choice;
2652
+ await writeFile5(configPath, JSON.stringify(data, null, 2) + "\n");
2653
+ p9.log.success(`Saved primaryBackend: ${choice} to .projx`);
2654
+ } catch {
2655
+ }
2656
+ return choice;
2657
+ }
2658
+ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
2593
2659
  p9.intro(`projx gen entity ${entityName}`);
2594
2660
  const configPath = join12(cwd, ".projx");
2595
2661
  if (!existsSync11(configPath)) {
2596
2662
  p9.log.error("No .projx file found. Run 'npx create-projx init' first.");
2597
2663
  process.exit(1);
2598
2664
  }
2599
- const projxConfig = JSON.parse(await readFile11(configPath, "utf-8"));
2600
- const componentPaths = await discoverComponentPaths(cwd, projxConfig.components);
2601
- const hasFastapi = projxConfig.components.includes("fastapi");
2602
- const hasFastify = projxConfig.components.includes("fastify");
2603
- const hasFrontend = projxConfig.components.includes("frontend");
2604
- const hasMobile = projxConfig.components.includes("mobile");
2665
+ const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
2666
+ const hasFastapi = discovered.includes("fastapi");
2667
+ const hasFastify = discovered.includes("fastify");
2668
+ const hasFrontend = discovered.includes("frontend");
2669
+ const hasMobile = discovered.includes("mobile");
2605
2670
  if (!hasFastapi && !hasFastify) {
2606
2671
  p9.log.error("No backend component found. Need fastapi or fastify.");
2607
2672
  process.exit(1);
2608
2673
  }
2674
+ const targetBackend = await resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag);
2675
+ const genFastapi = targetBackend === "fastapi" && hasFastapi;
2676
+ const genFastify = targetBackend === "fastify" && hasFastify;
2609
2677
  let config;
2610
2678
  if (fieldsFlag) {
2611
2679
  const fields = parseFieldsFlag(fieldsFlag);
@@ -2626,7 +2694,7 @@ async function gen(cwd, entityName, fieldsFlag) {
2626
2694
  config = await promptEntityConfig(entityName);
2627
2695
  }
2628
2696
  const generated = [];
2629
- if (hasFastapi) {
2697
+ if (genFastapi) {
2630
2698
  const dir = componentPaths.fastapi;
2631
2699
  const entityDir = join12(cwd, dir, "src/entities", toSnake(config.name));
2632
2700
  if (existsSync11(entityDir)) {
@@ -2637,7 +2705,7 @@ async function gen(cwd, entityName, fieldsFlag) {
2637
2705
  generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
2638
2706
  }
2639
2707
  }
2640
- if (hasFastify) {
2708
+ if (genFastify) {
2641
2709
  const dir = componentPaths.fastify;
2642
2710
  const moduleDir = join12(cwd, dir, "src/modules", toKebab(config.name));
2643
2711
  if (existsSync11(moduleDir)) {
@@ -2722,13 +2790,13 @@ async function gen(cwd, entityName, fieldsFlag) {
2722
2790
  p9.log.info(` ${f}`);
2723
2791
  }
2724
2792
  const className = toPascal(config.name);
2725
- if (hasFastapi) {
2793
+ if (genFastapi) {
2726
2794
  p9.log.info("");
2727
2795
  p9.log.info("FastAPI next steps:");
2728
2796
  p9.log.info(` alembic revision --autogenerate -m "add ${config.tableName}"`);
2729
2797
  p9.log.info(" alembic upgrade head");
2730
2798
  }
2731
- if (hasFastify) {
2799
+ if (genFastify) {
2732
2800
  p9.log.info("");
2733
2801
  p9.log.info("Fastify next steps:");
2734
2802
  p9.log.info(` npx prisma migrate dev --name add_${toSnake(config.name)}`);
@@ -2749,7 +2817,7 @@ async function gen(cwd, entityName, fieldsFlag) {
2749
2817
 
2750
2818
  // src/sync.ts
2751
2819
  import { existsSync as existsSync12, readFileSync as readFileSync2 } from "fs";
2752
- import { readFile as readFile12, writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
2820
+ import { writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
2753
2821
  import { join as join13 } from "path";
2754
2822
  import * as p10 from "@clack/prompts";
2755
2823
  function toPascal2(s) {
@@ -2926,15 +2994,9 @@ async function sync(cwd, url) {
2926
2994
  p10.log.error("No .projx file found. Run 'npx create-projx init' first.");
2927
2995
  process.exit(1);
2928
2996
  }
2929
- const projxConfig = JSON.parse(
2930
- await readFile12(configPath, "utf-8")
2931
- );
2932
- const componentPaths = await discoverComponentPaths(
2933
- cwd,
2934
- projxConfig.components
2935
- );
2936
- const hasFrontend = projxConfig.components.includes("frontend");
2937
- const hasMobile = projxConfig.components.includes("mobile");
2997
+ const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
2998
+ const hasFrontend = discovered.includes("frontend");
2999
+ const hasMobile = discovered.includes("mobile");
2938
3000
  if (!hasFrontend && !hasMobile) {
2939
3001
  p10.log.error("No frontend or mobile component found. Nothing to sync.");
2940
3002
  process.exit(1);
@@ -3120,6 +3182,14 @@ function parseArgs() {
3120
3182
  flags.fix = true;
3121
3183
  continue;
3122
3184
  }
3185
+ if (arg === "--ai") {
3186
+ flags.ai = true;
3187
+ continue;
3188
+ }
3189
+ if (arg === "--backend") {
3190
+ flags.backend = true;
3191
+ continue;
3192
+ }
3123
3193
  if (arg === "--url") {
3124
3194
  const val = args[++i];
3125
3195
  if (val) extraArgs.push(`--url=${val}`);
@@ -3240,7 +3310,8 @@ async function main() {
3240
3310
  const entityName = extraArgs[1];
3241
3311
  const fieldsArg = extraArgs.find((a) => a.startsWith("--fields="));
3242
3312
  const fieldsFlag = fieldsArg ? fieldsArg.split("=").slice(1).join("=") : void 0;
3243
- await gen(process.cwd(), entityName, fieldsFlag);
3313
+ const backendFlag = flags.ai ? "fastapi" : flags.backend ? "fastify" : void 0;
3314
+ await gen(process.cwd(), entityName, fieldsFlag, backendFlag);
3244
3315
  return;
3245
3316
  }
3246
3317
  let opts;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
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": {