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.
- package/README.md +16 -6
- package/dist/index.js +107 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/create-projx)
|
|
4
4
|
[](https://github.com/ukanhaupa/projx/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/ukanhaupa/projx)
|
|
5
6
|
[](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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
|
193
|
-
|
|
|
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
|
-
|
|
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
|
|
933
|
-
if (
|
|
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:
|
|
938
|
-
p3.log.info(`Detected: ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1881
|
-
|
|
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,
|
|
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
|
|
1939
|
-
const componentPaths = await
|
|
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
|
|
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
|
|
2600
|
-
const
|
|
2601
|
-
const
|
|
2602
|
-
const
|
|
2603
|
-
const
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 {
|
|
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
|
|
2930
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|