create-projx 1.4.2 → 1.5.0

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
@@ -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.
@@ -31,6 +32,16 @@ npx create-projx my-app --components fastify,frontend,e2e
31
32
  npx create-projx my-app -y
32
33
  ```
33
34
 
35
+ ## Package Manager Support
36
+
37
+ Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're prompted to pick one. The choice is stored in `.projx` and used everywhere — setup.sh, Docker, CI, pre-commit hooks, and README.
38
+
39
+ ```json
40
+ { "packageManager": "pnpm" }
41
+ ```
42
+
43
+ For `init`, the package manager is auto-detected from lockfiles (`pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `bun.lockb` → bun). Falls back to a prompt if no lockfile is found.
44
+
34
45
  ## Components
35
46
 
36
47
  | Component | Stack | What You Get |
@@ -111,7 +122,7 @@ To skip root-level files (docker-compose, README), add `skip` to `.projx`:
111
122
 
112
123
  ```json
113
124
  {
114
- "version": "1.3.6",
125
+ "version": "1.4.2",
115
126
  "components": ["fastapi", "frontend"],
116
127
  "skip": ["docker-compose.yml", "README.md"]
117
128
  }
@@ -131,10 +142,12 @@ npx create-projx pin <patterns...>
131
142
  npx create-projx unpin <patterns...>
132
143
  npx create-projx pin --list
133
144
  npx create-projx doctor [--fix]
134
- npx create-projx gen entity <name>
145
+ npx create-projx gen entity <name> [--ai | --backend]
135
146
  npx create-projx sync [--url <url>]
136
147
 
137
148
  --components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
149
+ --ai Target fastapi (AI/ML) for gen entity
150
+ --backend Target fastify (API backend) for gen entity
138
151
  --no-git Skip git init
139
152
  --no-install Skip dependency installation
140
153
  -y, --yes Accept defaults (fastify + frontend + e2e)
@@ -178,23 +191,30 @@ Checks: config validity, component markers, baseline ref, stale worktrees, skip
178
191
 
179
192
  ### Generate Entities
180
193
 
181
- Scaffold a new entity across all components in your project:
194
+ Scaffold a new entity in your primary backend + typed models for frontend/mobile:
182
195
 
183
196
  ```bash
184
197
  npx create-projx gen entity invoice # interactive
185
198
  npx create-projx gen entity invoice --fields "name:string,amount:number" # non-interactive
199
+ npx create-projx gen entity embedding --ai --fields "name:string,vector:json" # target AI backend
200
+ ```
201
+
202
+ 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`:
203
+
204
+ ```json
205
+ { "primaryBackend": "fastify" }
186
206
  ```
187
207
 
188
- Generates based on what's in your `.projx`:
208
+ Override with `--ai` (fastapi) or `--backend` (fastify).
189
209
 
190
210
  | Component | Generated |
191
211
  | --------- | --------- |
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 |
212
+ | Primary backend (fastapi) | `src/entities/<name>/_model.py` — auto-discovered by registry |
213
+ | Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + app.ts import |
194
214
  | `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
195
215
  | `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
196
216
 
197
- No migrations — run `alembic revision --autogenerate` or `npx prisma migrate dev` when ready.
217
+ No migrations — run `alembic revision --autogenerate` or `prisma migrate dev` (via your package manager) when ready.
198
218
 
199
219
  ### Sync Types
200
220
 
package/dist/index.js CHANGED
@@ -21,6 +21,26 @@ var COMPONENTS = [
21
21
  "e2e",
22
22
  "infra"
23
23
  ];
24
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
25
+ function pmCommands(pm) {
26
+ switch (pm) {
27
+ case "npm":
28
+ return { name: "npm", install: "npm install", ci: "npm ci", run: "npm run", exec: "npx", dlx: "npx", lockfile: "package-lock.json", prismaExec: "npx prisma", runDev: "npm run dev" };
29
+ case "pnpm":
30
+ return { name: "pnpm", install: "pnpm install", ci: "pnpm install --frozen-lockfile", run: "pnpm", exec: "pnpm exec", dlx: "pnpm dlx", lockfile: "pnpm-lock.yaml", prismaExec: "pnpm prisma", runDev: "pnpm dev" };
31
+ case "yarn":
32
+ return { name: "yarn", install: "yarn", ci: "yarn --frozen-lockfile", run: "yarn", exec: "yarn", dlx: "yarn dlx", lockfile: "yarn.lock", prismaExec: "yarn prisma", runDev: "yarn dev" };
33
+ case "bun":
34
+ return { name: "bun", install: "bun install", ci: "bun install --frozen-lockfile", run: "bun run", exec: "bunx", dlx: "bunx", lockfile: "bun.lockb", prismaExec: "bunx prisma", runDev: "bun run dev" };
35
+ }
36
+ }
37
+ function detectPackageManager(cwd) {
38
+ if (existsSync(join(cwd, "bun.lockb"))) return "bun";
39
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
40
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
41
+ if (existsSync(join(cwd, "package-lock.json"))) return "npm";
42
+ return null;
43
+ }
24
44
  function toKebab(s) {
25
45
  return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
26
46
  }
@@ -289,8 +309,9 @@ function render(template, vars) {
289
309
  for (const line of lines) {
290
310
  const ifMatch = line.match(/^<%\s*if\s*\((.+?)\)\s*\{?\s*%>$/);
291
311
  if (ifMatch) {
292
- const fn = new Function("components", "projectName", `return ${ifMatch[1]}`);
293
- const result = fn(components, projectName);
312
+ const pmName = vars.pm?.name ?? "npm";
313
+ const fn = new Function("components", "projectName", "pm", `return ${ifMatch[1]}`);
314
+ const result = fn(components, projectName, pmName);
294
315
  stack.push({ active: result, matched: result });
295
316
  continue;
296
317
  }
@@ -301,8 +322,9 @@ function render(template, vars) {
301
322
  if (top.matched) {
302
323
  top.active = false;
303
324
  } else {
304
- const fn = new Function("components", "projectName", `return ${elseIfMatch[1]}`);
305
- const result = fn(components, projectName);
325
+ const pmN = vars.pm?.name ?? "npm";
326
+ const fn = new Function("components", "projectName", "pm", `return ${elseIfMatch[1]}`);
327
+ const result = fn(components, projectName, pmN);
306
328
  top.active = result;
307
329
  if (result) top.matched = true;
308
330
  }
@@ -391,7 +413,18 @@ async function runPrompts(nameArg) {
391
413
  if (components.length === 0) {
392
414
  p.log.warn("No components selected. Creating an empty project.");
393
415
  }
394
- return { name, components, git: true, install: true };
416
+ const hasJs = components.some((c) => ["fastify", "frontend", "e2e"].includes(c));
417
+ let packageManager = "npm";
418
+ if (hasJs) {
419
+ const pm = await p.select({
420
+ message: "Package manager",
421
+ options: PACKAGE_MANAGERS.map((pm2) => ({ value: pm2, label: pm2 })),
422
+ initialValue: "npm"
423
+ });
424
+ if (p.isCancel(pm)) process.exit(0);
425
+ packageManager = pm;
426
+ }
427
+ return { name, components, git: true, install: true, packageManager };
395
428
  }
396
429
 
397
430
  // src/scaffold.ts
@@ -686,6 +719,8 @@ async function writeTemplateToDir(dest, repoDir, components, componentPaths, var
686
719
  components,
687
720
  createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
688
721
  };
722
+ const pmObj = vars.pm;
723
+ if (pmObj?.name) projxConfig.packageManager = pmObj.name;
689
724
  await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2) + "\n");
690
725
  }
691
726
  async function substituteNames(dest, components, paths, name, nameSnake) {
@@ -774,6 +809,8 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
774
809
  components,
775
810
  createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
776
811
  };
812
+ const pmObj = vars.pm;
813
+ if (pmObj?.name) projxConfig.packageManager = pmObj.name;
777
814
  await writeFile2(join3(cwd, ".projx"), JSON.stringify(projxConfig, null, 2) + "\n");
778
815
  if (result.conflicted.length === 0) {
779
816
  execSync2("git add -A", { cwd, stdio: "pipe" });
@@ -817,10 +854,11 @@ async function applyTemplate(cwd, repoDir, components, componentPaths, vars, ver
817
854
  // src/scaffold.ts
818
855
  async function scaffold(opts, dest, localRepo) {
819
856
  const name = toKebab(opts.name);
857
+ const pm = opts.packageManager ?? "npm";
820
858
  const paths = Object.fromEntries(
821
859
  opts.components.map((c) => [c, c])
822
860
  );
823
- const vars = { projectName: name, components: opts.components, paths };
861
+ const vars = { projectName: name, components: opts.components, paths, pm: pmCommands(pm) };
824
862
  const isLocal = !!localRepo;
825
863
  await mkdir3(dest, { recursive: true });
826
864
  const dlSpinner = p2.spinner();
@@ -844,7 +882,7 @@ async function scaffold(opts, dest, localRepo) {
844
882
  await applyTemplate(dest, repoDir, opts.components, paths, vars, version);
845
883
  spinner7.stop("Scaffold complete.");
846
884
  if (opts.install) {
847
- await installDeps(dest, opts.components);
885
+ await installDeps(dest, opts.components, pm);
848
886
  }
849
887
  copyEnvExamples(dest, opts.components);
850
888
  if (opts.git) {
@@ -865,7 +903,9 @@ async function scaffold(opts, dest, localRepo) {
865
903
 
866
904
  Like projx? Star it: https://github.com/ukanhaupa/projx`);
867
905
  }
868
- async function installDeps(dest, components) {
906
+ async function installDeps(dest, components, pm) {
907
+ const cmds = pmCommands(pm);
908
+ const pmBin = pm === "bun" ? "bun" : pm;
869
909
  for (const component of components) {
870
910
  const spinner7 = p2.spinner();
871
911
  try {
@@ -880,25 +920,31 @@ async function installDeps(dest, components) {
880
920
  }
881
921
  break;
882
922
  case "fastify":
883
- if (hasCommand("pnpm")) {
884
- spinner7.start("Installing Fastify dependencies (pnpm install)");
885
- exec("pnpm install", join4(dest, "fastify"));
923
+ if (hasCommand(pmBin)) {
924
+ spinner7.start(`Installing Fastify dependencies (${cmds.install})`);
925
+ exec(cmds.install, join4(dest, "fastify"));
886
926
  spinner7.stop("Fastify dependencies installed.");
887
927
  } else {
888
- spinner7.start("Installing Fastify dependencies (npm install)");
889
- exec("npm install", join4(dest, "fastify"));
890
- spinner7.stop("Fastify dependencies installed.");
928
+ p2.log.warn(`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`);
891
929
  }
892
930
  break;
893
931
  case "frontend":
894
- spinner7.start("Installing Frontend dependencies (npm install)");
895
- exec("npm install", join4(dest, "frontend"));
896
- spinner7.stop("Frontend dependencies installed.");
932
+ if (hasCommand(pmBin)) {
933
+ spinner7.start(`Installing Frontend dependencies (${cmds.install})`);
934
+ exec(cmds.install, join4(dest, "frontend"));
935
+ spinner7.stop("Frontend dependencies installed.");
936
+ } else {
937
+ p2.log.warn(`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`);
938
+ }
897
939
  break;
898
940
  case "e2e":
899
- spinner7.start("Installing E2E dependencies (npm install)");
900
- exec("npm install", join4(dest, "e2e"));
901
- spinner7.stop("E2E dependencies installed.");
941
+ if (hasCommand(pmBin)) {
942
+ spinner7.start(`Installing E2E dependencies (${cmds.install})`);
943
+ exec(cmds.install, join4(dest, "e2e"));
944
+ spinner7.stop("E2E dependencies installed.");
945
+ } else {
946
+ p2.log.warn(`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`);
947
+ }
902
948
  break;
903
949
  case "mobile":
904
950
  if (hasCommand("flutter")) {
@@ -993,7 +1039,9 @@ async function update(cwd, localRepo) {
993
1039
  const pkg = JSON.parse(await readFile5(join5(repoDir, "cli/package.json"), "utf-8"));
994
1040
  const version = pkg.version;
995
1041
  const name = detectProjectName(cwd, config.components, componentPaths);
996
- const vars = { projectName: name, components: config.components, paths: componentPaths };
1042
+ const raw = existsSync4(configPath) ? JSON.parse(await readFile5(configPath, "utf-8")) : {};
1043
+ const pm = raw.packageManager ?? "npm";
1044
+ const vars = { projectName: name, components: config.components, paths: componentPaths, pm: pmCommands(pm) };
997
1045
  const spinner7 = p3.spinner();
998
1046
  spinner7.start("Applying template update");
999
1047
  const rootSkip = config.skip ?? [];
@@ -1184,8 +1232,9 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
1184
1232
  const existingPaths = await discoverComponentPaths(cwd, existing);
1185
1233
  const paths = { ...existingPaths };
1186
1234
  for (const c of toAdd) paths[c] = c;
1235
+ const pm = config.packageManager ?? "npm";
1187
1236
  const name = detectProjectName(cwd, existing, paths);
1188
- const vars = { projectName: name, components: allComponents, paths };
1237
+ const vars = { projectName: name, components: allComponents, paths, pm: pmCommands(pm) };
1189
1238
  const pkg = JSON.parse(await readFile6(join6(repoDir, "cli/package.json"), "utf-8"));
1190
1239
  const version = pkg.version;
1191
1240
  const spinner7 = p4.spinner();
@@ -1193,7 +1242,7 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
1193
1242
  await writeTemplateToDir(cwd, repoDir, allComponents, paths, vars, version, "scaffold");
1194
1243
  spinner7.stop("Components added.");
1195
1244
  if (!skipInstall) {
1196
- await installDeps2(cwd, toAdd);
1245
+ await installDeps2(cwd, toAdd, pm);
1197
1246
  }
1198
1247
  for (const component of toAdd) {
1199
1248
  const example = join6(cwd, component, ".env.example");
@@ -1212,7 +1261,9 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
1212
1261
  await cleanupRepo(repoDir, isLocal);
1213
1262
  }
1214
1263
  }
1215
- async function installDeps2(dest, components) {
1264
+ async function installDeps2(dest, components, pm) {
1265
+ const cmds = pmCommands(pm);
1266
+ const pmBin = pm === "bun" ? "bun" : pm;
1216
1267
  for (const component of components) {
1217
1268
  const spinner7 = p4.spinner();
1218
1269
  try {
@@ -1227,25 +1278,31 @@ async function installDeps2(dest, components) {
1227
1278
  }
1228
1279
  break;
1229
1280
  case "fastify":
1230
- if (hasCommand("pnpm")) {
1231
- spinner7.start("Installing Fastify dependencies");
1232
- exec("pnpm install", join6(dest, "fastify"));
1281
+ if (hasCommand(pmBin)) {
1282
+ spinner7.start(`Installing Fastify dependencies (${cmds.install})`);
1283
+ exec(cmds.install, join6(dest, "fastify"));
1233
1284
  spinner7.stop("Fastify dependencies installed.");
1234
1285
  } else {
1235
- spinner7.start("Installing Fastify dependencies");
1236
- exec("npm install", join6(dest, "fastify"));
1237
- spinner7.stop("Fastify dependencies installed.");
1286
+ p4.log.warn(`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`);
1238
1287
  }
1239
1288
  break;
1240
1289
  case "frontend":
1241
- spinner7.start("Installing Frontend dependencies");
1242
- exec("npm install", join6(dest, "frontend"));
1243
- spinner7.stop("Frontend dependencies installed.");
1290
+ if (hasCommand(pmBin)) {
1291
+ spinner7.start(`Installing Frontend dependencies (${cmds.install})`);
1292
+ exec(cmds.install, join6(dest, "frontend"));
1293
+ spinner7.stop("Frontend dependencies installed.");
1294
+ } else {
1295
+ p4.log.warn(`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`);
1296
+ }
1244
1297
  break;
1245
1298
  case "e2e":
1246
- spinner7.start("Installing E2E dependencies");
1247
- exec("npm install", join6(dest, "e2e"));
1248
- spinner7.stop("E2E dependencies installed.");
1299
+ if (hasCommand(pmBin)) {
1300
+ spinner7.start(`Installing E2E dependencies (${cmds.install})`);
1301
+ exec(cmds.install, join6(dest, "e2e"));
1302
+ spinner7.stop("E2E dependencies installed.");
1303
+ } else {
1304
+ p4.log.warn(`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`);
1305
+ }
1249
1306
  break;
1250
1307
  case "mobile":
1251
1308
  if (hasCommand("flutter")) {
@@ -1392,8 +1449,25 @@ async function init(cwd, localRepo) {
1392
1449
  const paths = Object.fromEntries(
1393
1450
  confirmed.map((c) => [c.component, c.directory])
1394
1451
  );
1452
+ const hasJs = components.some((c) => ["fastify", "frontend", "e2e"].includes(c));
1453
+ let pm = "npm";
1454
+ if (hasJs) {
1455
+ const detected2 = detectPackageManager(cwd);
1456
+ if (detected2) {
1457
+ pm = detected2;
1458
+ p5.log.info(`Detected package manager: ${pm}`);
1459
+ } else if (process.stdin.isTTY) {
1460
+ const choice = await p5.select({
1461
+ message: "Package manager",
1462
+ options: PACKAGE_MANAGERS.map((v) => ({ value: v, label: v })),
1463
+ initialValue: "npm"
1464
+ });
1465
+ if (p5.isCancel(choice)) process.exit(0);
1466
+ pm = choice;
1467
+ }
1468
+ }
1395
1469
  const projectName = toKebab(cwd.split("/").pop());
1396
- const vars = { projectName, components, paths };
1470
+ const vars = { projectName, components, paths, pm: pmCommands(pm) };
1397
1471
  const dlSpinner = p5.spinner();
1398
1472
  dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
1399
1473
  const repoDir = await downloadRepo(localRepo).catch((err) => {
@@ -1992,7 +2066,7 @@ async function diff(cwd, localRepo) {
1992
2066
  const version = pkg.version;
1993
2067
  p8.log.info(`Current: v${config.version} \u2192 Template: v${version}`);
1994
2068
  const name = detectProjectName(cwd, config.components, componentPaths);
1995
- const vars = { projectName: name, components: config.components, paths: componentPaths };
2069
+ const vars = { projectName: name, components: config.components, paths: componentPaths, pm: pmCommands(raw.packageManager ?? "npm") };
1996
2070
  const spinner7 = p8.spinner();
1997
2071
  spinner7.start("Analyzing changes");
1998
2072
  const tmpTemplate = join11(tmpdir3(), `projx-diff-${Date.now()}`);
@@ -2551,9 +2625,10 @@ function dartFromJson(fieldName, type, required) {
2551
2625
  })();
2552
2626
  return required ? `${key} as ${dartT}` : `${key} as ${dartT}?`;
2553
2627
  }
2554
- function dartToJson(fieldName, camelName, type) {
2628
+ function dartToJson(fieldName, camelName, type, required) {
2555
2629
  const isDate = type === "date" || type === "datetime";
2556
- if (isDate) return `'${fieldName}': ${camelName}?.toIso8601String()`;
2630
+ if (isDate && required) return `'${fieldName}': ${camelName}.toIso8601String()`;
2631
+ if (isDate && !required) return `'${fieldName}': ${camelName}?.toIso8601String()`;
2557
2632
  return `'${fieldName}': ${camelName}`;
2558
2633
  }
2559
2634
  function generateDartModel(config) {
@@ -2602,7 +2677,7 @@ function generateDartModel(config) {
2602
2677
  lines.push(` Map<String, dynamic> toJson() {`);
2603
2678
  lines.push(` return {`);
2604
2679
  for (const f of allFields) {
2605
- lines.push(` ${dartToJson(f.snake, f.camel, f.fieldType)},`);
2680
+ lines.push(` ${dartToJson(f.snake, f.camel, f.fieldType, f.required)},`);
2606
2681
  }
2607
2682
  lines.push(` };`);
2608
2683
  lines.push(` }`);
@@ -2622,13 +2697,49 @@ function generateDartModel(config) {
2622
2697
  lines.push("");
2623
2698
  return lines.join("\n");
2624
2699
  }
2625
- async function gen(cwd, entityName, fieldsFlag) {
2700
+ async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
2701
+ if (backendFlag) return backendFlag;
2702
+ if (hasFastapi && !hasFastify) return "fastapi";
2703
+ if (hasFastify && !hasFastapi) return "fastify";
2704
+ const configPath = join12(cwd, ".projx");
2705
+ if (existsSync11(configPath)) {
2706
+ try {
2707
+ const data = JSON.parse(await readFile11(configPath, "utf-8"));
2708
+ if (data.primaryBackend === "fastapi" || data.primaryBackend === "fastify") {
2709
+ return data.primaryBackend;
2710
+ }
2711
+ } catch {
2712
+ }
2713
+ }
2714
+ if (!process.stdin.isTTY) return "fastify";
2715
+ const choice = await p9.select({
2716
+ message: "Both backends detected. Which is your primary?",
2717
+ options: [
2718
+ { value: "fastify", label: "fastify (API backend)" },
2719
+ { value: "fastapi", label: "fastapi (AI/ML engine)" }
2720
+ ],
2721
+ initialValue: "fastify"
2722
+ });
2723
+ if (p9.isCancel(choice)) process.exit(0);
2724
+ try {
2725
+ const data = JSON.parse(await readFile11(configPath, "utf-8"));
2726
+ data.primaryBackend = choice;
2727
+ await writeFile5(configPath, JSON.stringify(data, null, 2) + "\n");
2728
+ p9.log.success(`Saved primaryBackend: ${choice} to .projx`);
2729
+ } catch {
2730
+ }
2731
+ return choice;
2732
+ }
2733
+ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
2626
2734
  p9.intro(`projx gen entity ${entityName}`);
2627
2735
  const configPath = join12(cwd, ".projx");
2628
2736
  if (!existsSync11(configPath)) {
2629
2737
  p9.log.error("No .projx file found. Run 'npx create-projx init' first.");
2630
2738
  process.exit(1);
2631
2739
  }
2740
+ const projxData = JSON.parse(await readFile11(configPath, "utf-8"));
2741
+ const pmName = projxData.packageManager ?? "npm";
2742
+ const pm = pmCommands(pmName);
2632
2743
  const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
2633
2744
  const hasFastapi = discovered.includes("fastapi");
2634
2745
  const hasFastify = discovered.includes("fastify");
@@ -2638,6 +2749,9 @@ async function gen(cwd, entityName, fieldsFlag) {
2638
2749
  p9.log.error("No backend component found. Need fastapi or fastify.");
2639
2750
  process.exit(1);
2640
2751
  }
2752
+ const targetBackend = await resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag);
2753
+ const genFastapi = targetBackend === "fastapi" && hasFastapi;
2754
+ const genFastify = targetBackend === "fastify" && hasFastify;
2641
2755
  let config;
2642
2756
  if (fieldsFlag) {
2643
2757
  const fields = parseFieldsFlag(fieldsFlag);
@@ -2658,7 +2772,7 @@ async function gen(cwd, entityName, fieldsFlag) {
2658
2772
  config = await promptEntityConfig(entityName);
2659
2773
  }
2660
2774
  const generated = [];
2661
- if (hasFastapi) {
2775
+ if (genFastapi) {
2662
2776
  const dir = componentPaths.fastapi;
2663
2777
  const entityDir = join12(cwd, dir, "src/entities", toSnake(config.name));
2664
2778
  if (existsSync11(entityDir)) {
@@ -2669,7 +2783,7 @@ async function gen(cwd, entityName, fieldsFlag) {
2669
2783
  generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
2670
2784
  }
2671
2785
  }
2672
- if (hasFastify) {
2786
+ if (genFastify) {
2673
2787
  const dir = componentPaths.fastify;
2674
2788
  const moduleDir = join12(cwd, dir, "src/modules", toKebab(config.name));
2675
2789
  if (existsSync11(moduleDir)) {
@@ -2754,16 +2868,16 @@ async function gen(cwd, entityName, fieldsFlag) {
2754
2868
  p9.log.info(` ${f}`);
2755
2869
  }
2756
2870
  const className = toPascal(config.name);
2757
- if (hasFastapi) {
2871
+ if (genFastapi) {
2758
2872
  p9.log.info("");
2759
2873
  p9.log.info("FastAPI next steps:");
2760
2874
  p9.log.info(` alembic revision --autogenerate -m "add ${config.tableName}"`);
2761
2875
  p9.log.info(" alembic upgrade head");
2762
2876
  }
2763
- if (hasFastify) {
2877
+ if (genFastify) {
2764
2878
  p9.log.info("");
2765
2879
  p9.log.info("Fastify next steps:");
2766
- p9.log.info(` npx prisma migrate dev --name add_${toSnake(config.name)}`);
2880
+ p9.log.info(` ${pm.prismaExec} migrate dev --name add_${toSnake(config.name)}`);
2767
2881
  }
2768
2882
  if (hasFrontend) {
2769
2883
  p9.log.info("");
@@ -3146,6 +3260,14 @@ function parseArgs() {
3146
3260
  flags.fix = true;
3147
3261
  continue;
3148
3262
  }
3263
+ if (arg === "--ai") {
3264
+ flags.ai = true;
3265
+ continue;
3266
+ }
3267
+ if (arg === "--backend") {
3268
+ flags.backend = true;
3269
+ continue;
3270
+ }
3149
3271
  if (arg === "--url") {
3150
3272
  const val = args[++i];
3151
3273
  if (val) extraArgs.push(`--url=${val}`);
@@ -3266,7 +3388,8 @@ async function main() {
3266
3388
  const entityName = extraArgs[1];
3267
3389
  const fieldsArg = extraArgs.find((a) => a.startsWith("--fields="));
3268
3390
  const fieldsFlag = fieldsArg ? fieldsArg.split("=").slice(1).join("=") : void 0;
3269
- await gen(process.cwd(), entityName, fieldsFlag);
3391
+ const backendFlag = flags.ai ? "fastapi" : flags.backend ? "fastify" : void 0;
3392
+ await gen(process.cwd(), entityName, fieldsFlag, backendFlag);
3270
3393
  return;
3271
3394
  }
3272
3395
  let opts;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
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": {
@@ -48,7 +48,7 @@ API docs at `http://localhost:7860/docs`.
48
48
  ### Fastify
49
49
 
50
50
  ```bash
51
- cd <%= paths.fastify %> && cp .env.example .env && pnpm install && npx prisma migrate dev && pnpm dev
51
+ cd <%= paths.fastify %> && cp .env.example .env && <%= pm.install %> && <%= pm.exec %> prisma migrate dev && <%= pm.run %> dev
52
52
  ```
53
53
 
54
54
  API docs at `http://localhost:3000/docs`.
@@ -58,7 +58,7 @@ API docs at `http://localhost:3000/docs`.
58
58
  ### Frontend
59
59
 
60
60
  ```bash
61
- cd <%= paths.frontend %> && cp .env.example .env && npm install && npm run dev
61
+ cd <%= paths.frontend %> && cp .env.example .env && <%= pm.install %> && <%= pm.run %> dev
62
62
  ```
63
63
  <% } %>
64
64
  <% if (components.includes('mobile')) { %>
@@ -77,16 +77,16 @@ cd <%= paths.mobile %> && cp .env.example .env && flutter pub get && flutter run
77
77
  cd <%= paths.fastapi %> && uv run pytest
78
78
  <% } %>
79
79
  <% if (components.includes('fastify')) { %>
80
- cd <%= paths.fastify %> && pnpm test
80
+ cd <%= paths.fastify %> && <%= pm.run %> test
81
81
  <% } %>
82
82
  <% if (components.includes('frontend')) { %>
83
- cd <%= paths.frontend %> && npx vitest run
83
+ cd <%= paths.frontend %> && <%= pm.exec %> vitest run
84
84
  <% } %>
85
85
  <% if (components.includes('mobile')) { %>
86
86
  cd <%= paths.mobile %> && flutter test
87
87
  <% } %>
88
88
  <% if (components.includes('e2e')) { %>
89
- cd <%= paths.e2e %> && npx playwright test
89
+ cd <%= paths.e2e %> && <%= pm.exec %> playwright test
90
90
  <% } %>
91
91
  ```
92
92
 
@@ -90,19 +90,26 @@ jobs:
90
90
  working-directory: <%= paths.fastify %>
91
91
  steps:
92
92
  - uses: actions/checkout@v5
93
+ <% if (pm === 'pnpm') { %>
93
94
  - uses: pnpm/action-setup@v4
94
95
  with:
95
96
  version: 9
97
+ <% } %>
98
+ <% if (pm === 'bun') { %>
99
+ - uses: oven-sh/setup-bun@v2
100
+ <% } %>
101
+ <% if (pm !== 'bun') { %>
96
102
  - uses: actions/setup-node@v5
97
103
  with:
98
104
  node-version: 20
99
- cache: pnpm
100
- cache-dependency-path: <%= paths.fastify %>/pnpm-lock.yaml
101
- - run: pnpm install --frozen-lockfile
102
- - run: npx prisma generate
103
- - run: npx prettier --check .
104
- - run: npx eslint .
105
- - run: npx tsc --noEmit
105
+ cache: <%= pm.name %>
106
+ cache-dependency-path: <%= paths.fastify %>/<%= pm.lockfile %>
107
+ <% } %>
108
+ - run: <%= pm.ci %>
109
+ - run: <%= pm.prismaExec %> generate
110
+ - run: <%= pm.exec %> prettier --check .
111
+ - run: <%= pm.exec %> eslint .
112
+ - run: <%= pm.exec %> tsc --noEmit
106
113
  <% } %>
107
114
  <% if (components.includes('frontend')) { %>
108
115
 
@@ -116,15 +123,25 @@ jobs:
116
123
  working-directory: <%= paths.frontend %>
117
124
  steps:
118
125
  - uses: actions/checkout@v5
126
+ <% if (pm === 'pnpm') { %>
127
+ - uses: pnpm/action-setup@v4
128
+ with:
129
+ version: 9
130
+ <% } %>
131
+ <% if (pm === 'bun') { %>
132
+ - uses: oven-sh/setup-bun@v2
133
+ <% } %>
134
+ <% if (pm !== 'bun') { %>
119
135
  - uses: actions/setup-node@v5
120
136
  with:
121
137
  node-version: 22
122
- cache: npm
123
- cache-dependency-path: <%= paths.frontend %>/package-lock.json
124
- - run: npm ci
125
- - run: npx prettier --check .
126
- - run: npx eslint 'src/**/*.{ts,tsx}'
127
- - run: npx tsc --noEmit
138
+ cache: <%= pm.name %>
139
+ cache-dependency-path: <%= paths.frontend %>/<%= pm.lockfile %>
140
+ <% } %>
141
+ - run: <%= pm.ci %>
142
+ - run: <%= pm.exec %> prettier --check .
143
+ - run: <%= pm.exec %> eslint 'src/**/*.{ts,tsx}'
144
+ - run: <%= pm.exec %> tsc --noEmit
128
145
  <% } %>
129
146
  <% if (components.includes('mobile')) { %>
130
147
 
@@ -158,15 +175,25 @@ jobs:
158
175
  working-directory: <%= paths.e2e %>
159
176
  steps:
160
177
  - uses: actions/checkout@v5
178
+ <% if (pm === 'pnpm') { %>
179
+ - uses: pnpm/action-setup@v4
180
+ with:
181
+ version: 9
182
+ <% } %>
183
+ <% if (pm === 'bun') { %>
184
+ - uses: oven-sh/setup-bun@v2
185
+ <% } %>
186
+ <% if (pm !== 'bun') { %>
161
187
  - uses: actions/setup-node@v5
162
188
  with:
163
189
  node-version: 22
164
- cache: npm
165
- cache-dependency-path: <%= paths.e2e %>/package-lock.json
166
- - run: npm ci
167
- - run: npx prettier --check .
168
- - run: npx eslint '**/*.ts'
169
- - run: npx tsc --noEmit
190
+ cache: <%= pm.name %>
191
+ cache-dependency-path: <%= paths.e2e %>/<%= pm.lockfile %>
192
+ <% } %>
193
+ - run: <%= pm.ci %>
194
+ - run: <%= pm.exec %> prettier --check .
195
+ - run: <%= pm.exec %> eslint '**/*.ts'
196
+ - run: <%= pm.exec %> tsc --noEmit
170
197
  <% } %>
171
198
  <% if (components.includes('infra')) { %>
172
199
 
@@ -84,7 +84,7 @@ services:
84
84
  <% if (components.includes('fastify')) { %>
85
85
  <%= paths.fastify %>-migrate:
86
86
  build: ./<%= paths.fastify %>
87
- command: ['pnpm', 'prisma', 'migrate', 'deploy']
87
+ command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
88
88
  environment:
89
89
  - DATABASE_URL=postgresql://dev:dev@db:5432/app
90
90
  depends_on:
@@ -99,7 +99,7 @@ services:
99
99
  - app-network
100
100
  <%= paths.fastify %>:
101
101
  build: ./<%= paths.fastify %>
102
- command: ['pnpm', 'dev']
102
+ command: ["sh", "-c", "<%= pm.runDev %>"]
103
103
  ports:
104
104
  - '3000:3000'
105
105
  environment:
@@ -131,7 +131,7 @@ services:
131
131
  frontend:
132
132
  image: node:20-alpine
133
133
  working_dir: /app
134
- command: sh -c "npm install && npm run dev -- --host 0.0.0.0"
134
+ command: sh -c "<%= pm.install %> && <%= pm.run %> dev -- --host 0.0.0.0"
135
135
  ports:
136
136
  - '5173:5173'
137
137
  environment:
@@ -35,7 +35,7 @@ services:
35
35
  <% if (components.includes('fastify')) { %>
36
36
  <%= paths.fastify %>-migrate:
37
37
  build: ./<%= paths.fastify %>
38
- command: ["pnpm", "prisma", "migrate", "deploy"]
38
+ command: ["sh", "-c", "<%= pm.prismaExec %> migrate deploy"]
39
39
  env_file:
40
40
  - ./<%= paths.fastify %>/.env
41
41
  networks:
@@ -46,10 +46,10 @@ FASTIFY_ALL=$(echo "$STAGED_FILES" | grep '^<%= paths.fastify %>/' || true)
46
46
  if [ -n "$FASTIFY_ALL" ]; then
47
47
  echo "Formatting <%= paths.fastify %>..."
48
48
  cd <%= paths.fastify %>
49
- echo "$FASTIFY_ALL" | sed 's|^<%= paths.fastify %>/||' | xargs npx prettier --write --ignore-unknown
49
+ echo "$FASTIFY_ALL" | sed 's|^<%= paths.fastify %>/||' | xargs <%= pm.exec %> prettier --write --ignore-unknown
50
50
  if [ -n "$FASTIFY_TS" ]; then
51
- echo "$FASTIFY_TS" | sed 's|^<%= paths.fastify %>/||' | xargs npx eslint --fix
52
- npx tsc --noEmit
51
+ echo "$FASTIFY_TS" | sed 's|^<%= paths.fastify %>/||' | xargs <%= pm.exec %> eslint --fix
52
+ <%= pm.exec %> tsc --noEmit
53
53
  fi
54
54
  cd ..
55
55
  echo "$FASTIFY_ALL" | xargs git add
@@ -62,10 +62,10 @@ FRONTEND_ALL=$(echo "$STAGED_FILES" | grep '^<%= paths.frontend %>/' || true)
62
62
  if [ -n "$FRONTEND_ALL" ]; then
63
63
  echo "Formatting <%= paths.frontend %>..."
64
64
  cd <%= paths.frontend %>
65
- echo "$FRONTEND_ALL" | sed 's|^<%= paths.frontend %>/||' | xargs npx prettier --write --ignore-unknown
65
+ echo "$FRONTEND_ALL" | sed 's|^<%= paths.frontend %>/||' | xargs <%= pm.exec %> prettier --write --ignore-unknown
66
66
  if [ -n "$FRONTEND_TS" ]; then
67
- echo "$FRONTEND_TS" | sed 's|^<%= paths.frontend %>/||' | xargs npx eslint --fix
68
- npx tsc --noEmit
67
+ echo "$FRONTEND_TS" | sed 's|^<%= paths.frontend %>/||' | xargs <%= pm.exec %> eslint --fix
68
+ <%= pm.exec %> tsc --noEmit
69
69
  fi
70
70
  cd ..
71
71
  echo "$FRONTEND_ALL" | xargs git add
@@ -78,10 +78,10 @@ E2E_ALL=$(echo "$STAGED_FILES" | grep '^<%= paths.e2e %>/' || true)
78
78
  if [ -n "$E2E_ALL" ]; then
79
79
  echo "Formatting <%= paths.e2e %>..."
80
80
  cd <%= paths.e2e %>
81
- echo "$E2E_ALL" | sed 's|^<%= paths.e2e %>/||' | xargs npx prettier --write --ignore-unknown
81
+ echo "$E2E_ALL" | sed 's|^<%= paths.e2e %>/||' | xargs <%= pm.exec %> prettier --write --ignore-unknown
82
82
  if [ -n "$E2E_TS" ]; then
83
- echo "$E2E_TS" | sed 's|^<%= paths.e2e %>/||' | xargs npx eslint --fix
84
- npx tsc --noEmit
83
+ echo "$E2E_TS" | sed 's|^<%= paths.e2e %>/||' | xargs <%= pm.exec %> eslint --fix
84
+ <%= pm.exec %> tsc --noEmit
85
85
  fi
86
86
  cd ..
87
87
  echo "$E2E_ALL" | xargs git add
@@ -10,17 +10,17 @@ echo "FastAPI dependencies installed."
10
10
  <% } %>
11
11
  <% if (components.includes('fastify')) { %>
12
12
 
13
- cd <%= paths.fastify %> && pnpm install --frozen-lockfile && cd ..
13
+ cd <%= paths.fastify %> && <%= pm.ci %> && cd ..
14
14
  echo "Fastify dependencies installed."
15
15
  <% } %>
16
16
  <% if (components.includes('frontend')) { %>
17
17
 
18
- cd <%= paths.frontend %> && npm ci && cd ..
18
+ cd <%= paths.frontend %> && <%= pm.ci %> && cd ..
19
19
  echo "Frontend dependencies installed."
20
20
  <% } %>
21
21
  <% if (components.includes('e2e')) { %>
22
22
 
23
- cd <%= paths.e2e %> && npm ci && cd ..
23
+ cd <%= paths.e2e %> && <%= pm.ci %> && cd ..
24
24
  echo "E2E dependencies installed."
25
25
  <% } %>
26
26
  <% if (components.includes('mobile')) { %>