create-projx 1.5.1 → 1.5.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 +109 -78
- package/dist/index.js +167 -531
- package/package.json +1 -1
- package/src/templates/README.md.ejs +0 -8
package/README.md
CHANGED
|
@@ -5,19 +5,99 @@
|
|
|
5
5
|
[](https://github.com/ukanhaupa/projx)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**Go from blank folder to production-ready project in 30 seconds.** Backend-only API, AI/ML app, mobile, full-stack, infra setup — pick what you need and get it wired with auth, database, Docker, CI/CD, hooks, and tests. All optional. All yours.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npx create-projx my-app
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
No SDK lock-in. No runtime dependency on Projx. Just clean code in your repo that you own forever.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## You've done this a hundred times
|
|
19
|
+
|
|
20
|
+
Every new project starts with the same week of plumbing:
|
|
21
|
+
|
|
22
|
+
- Wire up auth (again).
|
|
23
|
+
- Configure the database and migrations (again).
|
|
24
|
+
- Write the Dockerfile and `docker-compose.yml` (again).
|
|
25
|
+
- Set up CI, linting, formatting, pre-commit hooks (again).
|
|
26
|
+
- Build the same login + CRUD scaffolding (again).
|
|
27
|
+
- Realize at 11pm that something's broken and you don't remember why (again).
|
|
28
|
+
|
|
29
|
+
You ship features two weeks late because the first two weeks were boilerplate.
|
|
30
|
+
|
|
31
|
+
## Or your AI does it badly
|
|
11
32
|
|
|
12
|
-
|
|
33
|
+
Ask an LLM to "scaffold a full-stack app" and you get 50 files of plausible-looking code that breaks on first run. Wrong import paths. Outdated package versions. Auth that doesn't actually authenticate. You end up debugging machine-generated boilerplate, which is worse than writing it yourself.
|
|
13
34
|
|
|
14
|
-
##
|
|
35
|
+
## What if you just… didn't?
|
|
15
36
|
|
|
16
37
|
```bash
|
|
17
|
-
npx create-projx my-app
|
|
38
|
+
npx create-projx my-app # interactive — pick exactly what you need
|
|
39
|
+
cd my-app
|
|
40
|
+
./setup.sh # installs everything you picked
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Pick any combination of components — they're all optional:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# AI/ML backend only
|
|
47
|
+
npx create-projx vision-api --components fastapi -y
|
|
48
|
+
|
|
49
|
+
# Node API + React frontend
|
|
50
|
+
npx create-projx saas --components fastify,frontend -y
|
|
51
|
+
|
|
52
|
+
# Mobile app with backend
|
|
53
|
+
npx create-projx field-app --components fastapi,mobile -y
|
|
54
|
+
|
|
55
|
+
# Full-stack with infra and E2E
|
|
56
|
+
npx create-projx prod-app --components fastify,frontend,e2e,infra -y
|
|
57
|
+
|
|
58
|
+
# Just the infra
|
|
59
|
+
npx create-projx platform --components infra -y
|
|
18
60
|
```
|
|
19
61
|
|
|
20
|
-
|
|
62
|
+
**30 seconds.** No matter what you pick, you get auth, Docker, CI/CD, hooks, and tests wired up for it.
|
|
63
|
+
|
|
64
|
+
If this saves you even one hour, it's already paid for itself. (It's free.)
|
|
65
|
+
|
|
66
|
+
## Why teams pick Projx and stay
|
|
67
|
+
|
|
68
|
+
- **It actually runs.** Every template is tested in CI before release. No "looks right" surprises.
|
|
69
|
+
- **Tests ship with the code.** `gen entity` writes integration tests alongside every model — 11 tests per entity, against a real database. You start green, not scrambling.
|
|
70
|
+
- **Auto-entity pattern.** Define a data model, get CRUD routes, validation, OpenAPI docs, and a typed UI for free. Backend, frontend, and mobile all stay in sync.
|
|
71
|
+
- **Updates don't nuke your code.** `projx update` does a 3-tier merge — your custom controllers, pages, and config survive template upgrades. No rewrites.
|
|
72
|
+
- **No lock-in.** Projx generates files and walks away. Delete the `.projx` config and it's just a normal repo.
|
|
73
|
+
- **Adopt incrementally.** Already have a project? `projx init` adds CI, hooks, and Docker without touching your code.
|
|
74
|
+
- **Pick your package manager.** npm, pnpm, yarn, or bun. The choice propagates everywhere — scripts, Docker, CI, docs.
|
|
75
|
+
- **AI-agent friendly.** Ships with [SKILL.md](SKILL.md) so Claude, Cursor, and other agents call Projx instead of hand-writing broken scaffolds.
|
|
76
|
+
|
|
77
|
+
## What you get
|
|
78
|
+
|
|
79
|
+
| Component | Stack | What it gives you |
|
|
80
|
+
| ---------- | --------------------------- | ------------------------------------------------------------ |
|
|
81
|
+
| `fastapi` | Python, SQLAlchemy, Alembic | Auto-entity CRUD, JWT auth, migrations, OpenAPI docs |
|
|
82
|
+
| `fastify` | Node.js, Prisma, TypeBox | Auto-entity CRUD, JWT auth, typed schemas, OpenAPI docs |
|
|
83
|
+
| `frontend` | React 19, TypeScript, Vite | Auto-entity UI from `/_meta`, design tokens, light/dark mode |
|
|
84
|
+
| `mobile` | Flutter, Riverpod, GoRouter | Auto-entity screens, offline-first with Isar, biometric auth |
|
|
85
|
+
| `e2e` | Playwright | Page object model, auth fixtures, accessibility scans |
|
|
86
|
+
| `infra` | Terraform, AWS | EKS, RDS, VPC, ALB, CodePipeline, multi-environment |
|
|
87
|
+
|
|
88
|
+
Plus, in every project: Docker Compose for dev + prod, GitHub Actions CI per component (path-filtered), pre-commit hooks, secret detection, VS Code settings, and 80% test coverage enforced.
|
|
89
|
+
|
|
90
|
+
All optional. Pick any combination.
|
|
91
|
+
|
|
92
|
+
## Built for humans and AI agents
|
|
93
|
+
|
|
94
|
+
Projx is a shared operating system for teams that ship with both:
|
|
95
|
+
|
|
96
|
+
- **Developers** get a readable layout, stable conventions, and safe updates.
|
|
97
|
+
- **AI agents** get a deterministic CLI to call instead of guessing folder structures.
|
|
98
|
+
- **Teams** get consistency across repos, less drift, and fewer surprise refactors.
|
|
99
|
+
|
|
100
|
+
If you're an AI agent reading this, see [SKILL.md](SKILL.md) for trigger conditions, decision rules, and the canonical invocation.
|
|
21
101
|
|
|
22
102
|
## Quick Start
|
|
23
103
|
|
|
@@ -42,19 +122,6 @@ Projx supports **npm**, **pnpm**, **yarn**, and **bun**. During `create`, you're
|
|
|
42
122
|
|
|
43
123
|
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
124
|
|
|
45
|
-
## Components
|
|
46
|
-
|
|
47
|
-
| Component | Stack | What You Get |
|
|
48
|
-
| ---------- | --------------------------- | ------------------------------------------------------------ |
|
|
49
|
-
| `fastapi` | Python, SQLAlchemy, Alembic | Auto-entity CRUD, JWT auth, migrations, OpenAPI docs |
|
|
50
|
-
| `fastify` | Node.js, Prisma, TypeBox | Auto-entity CRUD, JWT auth, typed schemas, OpenAPI docs |
|
|
51
|
-
| `frontend` | React 19, TypeScript, Vite | Auto-entity UI from metadata, design tokens, light/dark mode |
|
|
52
|
-
| `mobile` | Flutter, Riverpod, GoRouter | Auto-entity screens, offline-first with Isar, biometric auth |
|
|
53
|
-
| `e2e` | Playwright | Page object model, auth fixtures, accessibility scans |
|
|
54
|
-
| `infra` | Terraform, AWS | EKS, RDS, VPC, ALB, CodePipeline, multi-environment |
|
|
55
|
-
|
|
56
|
-
All optional. Pick any combination.
|
|
57
|
-
|
|
58
125
|
## Commands
|
|
59
126
|
|
|
60
127
|
### Create a Project
|
|
@@ -122,7 +189,7 @@ To skip root-level files (docker-compose, README), add `skip` to `.projx`:
|
|
|
122
189
|
|
|
123
190
|
```json
|
|
124
191
|
{
|
|
125
|
-
"version": "1.
|
|
192
|
+
"version": "1.5.2",
|
|
126
193
|
"components": ["fastapi", "frontend"],
|
|
127
194
|
"skip": ["docker-compose.yml", "README.md"]
|
|
128
195
|
}
|
|
@@ -144,7 +211,6 @@ npx create-projx pin --list
|
|
|
144
211
|
npx create-projx doctor [--fix]
|
|
145
212
|
npx create-projx gen entity <name> [--ai | --backend]
|
|
146
213
|
npx create-projx sync [--url <url>]
|
|
147
|
-
npx create-projx mcp
|
|
148
214
|
|
|
149
215
|
--components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
|
|
150
216
|
--ai Target fastapi (AI/ML) for gen entity
|
|
@@ -155,46 +221,6 @@ npx create-projx mcp
|
|
|
155
221
|
-h, --help Show help
|
|
156
222
|
```
|
|
157
223
|
|
|
158
|
-
## MCP Support (Local Stdio)
|
|
159
|
-
|
|
160
|
-
Projx ships with local Model Context Protocol support via stdio — no cloud hosting required.
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
npx create-projx mcp
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Exposed MCP tools:
|
|
167
|
-
|
|
168
|
-
- `projx_scaffold`
|
|
169
|
-
- `projx_scaffold_fullstack`
|
|
170
|
-
- `projx_add_components`
|
|
171
|
-
- `projx_update`
|
|
172
|
-
- `projx_doctor`
|
|
173
|
-
|
|
174
|
-
The repository includes a root `.mcp.json` that points agents to run:
|
|
175
|
-
|
|
176
|
-
```json
|
|
177
|
-
{
|
|
178
|
-
"servers": {
|
|
179
|
-
"projx": {
|
|
180
|
-
"command": "npx",
|
|
181
|
-
"args": ["-y", "create-projx", "mcp"]
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Instructions for AI Agents
|
|
188
|
-
|
|
189
|
-
Use `projx_scaffold_fullstack` whenever a user asks to:
|
|
190
|
-
|
|
191
|
-
- start a new project,
|
|
192
|
-
- create a boilerplate,
|
|
193
|
-
- build a production-ready app,
|
|
194
|
-
- scaffold a SaaS MVP with backend + frontend.
|
|
195
|
-
|
|
196
|
-
Prefer `projx_scaffold_fullstack` over hand-writing files. It creates a consistent, tested Projx layout in one call and returns a concise summary of what was generated.
|
|
197
|
-
|
|
198
224
|
### Preview Changes
|
|
199
225
|
|
|
200
226
|
See what `update` would change before applying:
|
|
@@ -248,12 +274,14 @@ When both `fastapi` and `fastify` exist, the entity generates in the **primary b
|
|
|
248
274
|
|
|
249
275
|
Override with `--ai` (fastapi) or `--backend` (fastify).
|
|
250
276
|
|
|
251
|
-
| Component | Generated
|
|
252
|
-
| ------------------------- |
|
|
253
|
-
| Primary backend (fastapi) | `src/entities/<name>/_model.py` —
|
|
254
|
-
| Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model +
|
|
255
|
-
| `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants
|
|
256
|
-
| `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith
|
|
277
|
+
| Component | Generated |
|
|
278
|
+
| ------------------------- | --------------------------------------------------------------------------------------------- |
|
|
279
|
+
| Primary backend (fastapi) | `src/entities/<name>/_model.py` + `tests/test_<name>_entity.py` — model + 11 CRUD/auth tests |
|
|
280
|
+
| Primary backend (fastify) | `src/modules/<name>/schemas.ts` + `index.ts` + Prisma model + `tests/modules/<name>.test.ts` |
|
|
281
|
+
| `frontend` | `src/types/<name>.ts` — TypeScript interface + Create/Update variants |
|
|
282
|
+
| `mobile` | `lib/entities/<name>/model.dart` — Dart class with fromJson/toJson/copyWith |
|
|
283
|
+
|
|
284
|
+
**Tests included**: every `gen entity` writes a working integration test file alongside the model — 11 tests for FastAPI (extending `BaseEntityApiTest`), 11 tests for Fastify (via `describeCrudEntity`). Both run against a real database (Postgres for Fastify, SQLite-in-memory for FastAPI today). New entities ship green from day one — no scrambling to bolt on tests at go-live.
|
|
257
285
|
|
|
258
286
|
No migrations — run `alembic revision --autogenerate` or `prisma migrate dev` (via your package manager) when ready.
|
|
259
287
|
|
|
@@ -319,17 +347,6 @@ The core idea: define a data model, get everything else for free.
|
|
|
319
347
|
|
|
320
348
|
**Mobile** — Same metadata endpoint, generates list/detail/form screens. Offline-first with local DB and sync queue.
|
|
321
349
|
|
|
322
|
-
## What's Included
|
|
323
|
-
|
|
324
|
-
- JWT auth with Keycloak (pluggable providers)
|
|
325
|
-
- Docker Compose for dev and prod
|
|
326
|
-
- GitHub Actions CI per component (path-filtered — only runs when that component changes)
|
|
327
|
-
- Pre-commit hooks (format + lint + typecheck)
|
|
328
|
-
- Secret detection in pre-commit
|
|
329
|
-
- VS Code settings + recommended extensions
|
|
330
|
-
- 80% test coverage enforced
|
|
331
|
-
- Auto-entity discovery across all stacks
|
|
332
|
-
|
|
333
350
|
## Development
|
|
334
351
|
|
|
335
352
|
Contributing to Projx itself:
|
|
@@ -348,6 +365,18 @@ npm test # run tests
|
|
|
348
365
|
npm run build # build CLI
|
|
349
366
|
```
|
|
350
367
|
|
|
368
|
+
## Try it now
|
|
369
|
+
|
|
370
|
+
You're still reading. Stop reading. Run this:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
npx create-projx my-app
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Pick whatever you need from the menu — backend-only, AI app, mobile, full-stack, just infra. 30 seconds. Free. No signup. If you don't like it, `rm -rf my-app` and we never speak of this again.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
351
380
|
## Badge
|
|
352
381
|
|
|
353
382
|
Add this to your project's README:
|
|
@@ -356,6 +385,8 @@ Add this to your project's README:
|
|
|
356
385
|
[](https://github.com/ukanhaupa/projx)
|
|
357
386
|
```
|
|
358
387
|
|
|
388
|
+
---
|
|
389
|
+
|
|
359
390
|
## License
|
|
360
391
|
|
|
361
392
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -25,53 +25,13 @@ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
|
25
25
|
function pmCommands(pm) {
|
|
26
26
|
switch (pm) {
|
|
27
27
|
case "npm":
|
|
28
|
-
return {
|
|
29
|
-
name: "npm",
|
|
30
|
-
install: "npm install",
|
|
31
|
-
ci: "npm ci",
|
|
32
|
-
run: "npm run",
|
|
33
|
-
exec: "npx",
|
|
34
|
-
dlx: "npx",
|
|
35
|
-
lockfile: "package-lock.json",
|
|
36
|
-
prismaExec: "npx prisma",
|
|
37
|
-
runDev: "npm run dev"
|
|
38
|
-
};
|
|
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" };
|
|
39
29
|
case "pnpm":
|
|
40
|
-
return {
|
|
41
|
-
name: "pnpm",
|
|
42
|
-
install: "pnpm install",
|
|
43
|
-
ci: "pnpm install --frozen-lockfile",
|
|
44
|
-
run: "pnpm",
|
|
45
|
-
exec: "pnpm exec",
|
|
46
|
-
dlx: "pnpm dlx",
|
|
47
|
-
lockfile: "pnpm-lock.yaml",
|
|
48
|
-
prismaExec: "pnpm prisma",
|
|
49
|
-
runDev: "pnpm dev"
|
|
50
|
-
};
|
|
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" };
|
|
51
31
|
case "yarn":
|
|
52
|
-
return {
|
|
53
|
-
name: "yarn",
|
|
54
|
-
install: "yarn",
|
|
55
|
-
ci: "yarn --frozen-lockfile",
|
|
56
|
-
run: "yarn",
|
|
57
|
-
exec: "yarn",
|
|
58
|
-
dlx: "yarn dlx",
|
|
59
|
-
lockfile: "yarn.lock",
|
|
60
|
-
prismaExec: "yarn prisma",
|
|
61
|
-
runDev: "yarn dev"
|
|
62
|
-
};
|
|
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" };
|
|
63
33
|
case "bun":
|
|
64
|
-
return {
|
|
65
|
-
name: "bun",
|
|
66
|
-
install: "bun install",
|
|
67
|
-
ci: "bun install --frozen-lockfile",
|
|
68
|
-
run: "bun run",
|
|
69
|
-
exec: "bunx",
|
|
70
|
-
dlx: "bunx",
|
|
71
|
-
lockfile: "bun.lockb",
|
|
72
|
-
prismaExec: "bunx prisma",
|
|
73
|
-
runDev: "bun run dev"
|
|
74
|
-
};
|
|
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" };
|
|
75
35
|
}
|
|
76
36
|
}
|
|
77
37
|
function detectPackageManager(cwd) {
|
|
@@ -112,13 +72,17 @@ async function downloadRepo(localPath) {
|
|
|
112
72
|
const dest = join(tmpdir(), `projx-${Date.now()}`);
|
|
113
73
|
await mkdir(dest, { recursive: true });
|
|
114
74
|
if (hasCommand("git")) {
|
|
115
|
-
execSync(
|
|
116
|
-
|
|
117
|
-
|
|
75
|
+
execSync(
|
|
76
|
+
`git clone --depth 1 ${REPO_URL}.git "${dest}/repo"`,
|
|
77
|
+
{ stdio: "pipe" }
|
|
78
|
+
);
|
|
118
79
|
return join(dest, "repo");
|
|
119
80
|
}
|
|
120
81
|
const tarUrl = `${REPO_URL}/archive/refs/heads/main.tar.gz`;
|
|
121
|
-
execSync(
|
|
82
|
+
execSync(
|
|
83
|
+
`curl -sL "${tarUrl}" | tar xz -C "${dest}"`,
|
|
84
|
+
{ stdio: "pipe" }
|
|
85
|
+
);
|
|
122
86
|
const entries = await readdir(dest);
|
|
123
87
|
const extracted = entries.find((e) => e.startsWith("projx-"));
|
|
124
88
|
if (!extracted) throw new Error("Failed to extract repo archive.");
|
|
@@ -181,7 +145,7 @@ async function copyComponent(repoDir, component, dest) {
|
|
|
181
145
|
async function copyStaticFiles(repoDir, dest) {
|
|
182
146
|
const manifest = [];
|
|
183
147
|
const tpl = repoDir;
|
|
184
|
-
const statics = [".editorconfig"
|
|
148
|
+
const statics = [".editorconfig"];
|
|
185
149
|
for (const file of statics) {
|
|
186
150
|
const src = join(tpl, file);
|
|
187
151
|
if (existsSync(src)) {
|
|
@@ -346,19 +310,12 @@ function render(template, vars) {
|
|
|
346
310
|
const ifMatch = line.match(/^<%\s*if\s*\((.+?)\)\s*\{?\s*%>$/);
|
|
347
311
|
if (ifMatch) {
|
|
348
312
|
const pmName = vars.pm?.name ?? "npm";
|
|
349
|
-
const fn = new Function(
|
|
350
|
-
"components",
|
|
351
|
-
"projectName",
|
|
352
|
-
"pm",
|
|
353
|
-
`return ${ifMatch[1]}`
|
|
354
|
-
);
|
|
313
|
+
const fn = new Function("components", "projectName", "pm", `return ${ifMatch[1]}`);
|
|
355
314
|
const result = fn(components, projectName, pmName);
|
|
356
315
|
stack.push({ active: result, matched: result });
|
|
357
316
|
continue;
|
|
358
317
|
}
|
|
359
|
-
const elseIfMatch = line.match(
|
|
360
|
-
/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/
|
|
361
|
-
);
|
|
318
|
+
const elseIfMatch = line.match(/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/);
|
|
362
319
|
if (elseIfMatch) {
|
|
363
320
|
if (stack.length > 0) {
|
|
364
321
|
const top = stack[stack.length - 1];
|
|
@@ -366,12 +323,7 @@ function render(template, vars) {
|
|
|
366
323
|
top.active = false;
|
|
367
324
|
} else {
|
|
368
325
|
const pmN = vars.pm?.name ?? "npm";
|
|
369
|
-
const fn = new Function(
|
|
370
|
-
"components",
|
|
371
|
-
"projectName",
|
|
372
|
-
"pm",
|
|
373
|
-
`return ${elseIfMatch[1]}`
|
|
374
|
-
);
|
|
326
|
+
const fn = new Function("components", "projectName", "pm", `return ${elseIfMatch[1]}`);
|
|
375
327
|
const result = fn(components, projectName, pmN);
|
|
376
328
|
top.active = result;
|
|
377
329
|
if (result) top.matched = true;
|
|
@@ -391,14 +343,17 @@ function render(template, vars) {
|
|
|
391
343
|
continue;
|
|
392
344
|
}
|
|
393
345
|
if (stack.length > 0 && stack.some((v) => !v.active)) continue;
|
|
394
|
-
const replaced = line.replace(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
val =
|
|
346
|
+
const replaced = line.replace(
|
|
347
|
+
/<%=\s*([\w.]+)\s*%>/g,
|
|
348
|
+
(_, expr) => {
|
|
349
|
+
const parts = expr.split(".");
|
|
350
|
+
let val = vars;
|
|
351
|
+
for (const p11 of parts) {
|
|
352
|
+
val = val?.[p11];
|
|
353
|
+
}
|
|
354
|
+
return String(val ?? "");
|
|
399
355
|
}
|
|
400
|
-
|
|
401
|
-
});
|
|
356
|
+
);
|
|
402
357
|
output.push(replaced);
|
|
403
358
|
}
|
|
404
359
|
return output.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
@@ -2742,6 +2697,128 @@ function generateDartModel(config) {
|
|
|
2742
2697
|
lines.push("");
|
|
2743
2698
|
return lines.join("\n");
|
|
2744
2699
|
}
|
|
2700
|
+
function pyHttpLiteral(type, variant = "create") {
|
|
2701
|
+
switch (type) {
|
|
2702
|
+
case "string":
|
|
2703
|
+
case "text":
|
|
2704
|
+
return variant === "create" ? '"sample text"' : variant === "update" ? '"updated text"' : '"alt text"';
|
|
2705
|
+
case "number":
|
|
2706
|
+
return variant === "create" ? "42" : variant === "update" ? "100" : "7";
|
|
2707
|
+
case "boolean":
|
|
2708
|
+
return variant === "create" ? "True" : "False";
|
|
2709
|
+
case "date":
|
|
2710
|
+
return variant === "alt" ? '"2026-02-01"' : '"2026-01-01"';
|
|
2711
|
+
case "datetime":
|
|
2712
|
+
return variant === "alt" ? '"2026-02-01T00:00:00"' : '"2026-01-01T00:00:00"';
|
|
2713
|
+
case "json":
|
|
2714
|
+
return "{}";
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function pyOrmLiteral(type, variant = "create") {
|
|
2718
|
+
switch (type) {
|
|
2719
|
+
case "string":
|
|
2720
|
+
case "text":
|
|
2721
|
+
return variant === "create" ? '"sample text"' : variant === "update" ? '"updated text"' : '"alt text"';
|
|
2722
|
+
case "number":
|
|
2723
|
+
return variant === "create" ? "42" : variant === "update" ? "100" : "7";
|
|
2724
|
+
case "boolean":
|
|
2725
|
+
return variant === "create" ? "True" : "False";
|
|
2726
|
+
case "date":
|
|
2727
|
+
return variant === "alt" ? "date(2026, 2, 1)" : "date(2026, 1, 1)";
|
|
2728
|
+
case "datetime":
|
|
2729
|
+
return variant === "alt" ? "datetime(2026, 2, 1, 0, 0, 0)" : "datetime(2026, 1, 1, 0, 0, 0)";
|
|
2730
|
+
case "json":
|
|
2731
|
+
return "{}";
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
function tsLiteral(type, variant = "create") {
|
|
2735
|
+
switch (type) {
|
|
2736
|
+
case "string":
|
|
2737
|
+
case "text":
|
|
2738
|
+
return variant === "create" ? "'sample text'" : variant === "update" ? "'updated text'" : "'alt text'";
|
|
2739
|
+
case "number":
|
|
2740
|
+
return variant === "create" ? "42" : variant === "update" ? "100" : "7";
|
|
2741
|
+
case "boolean":
|
|
2742
|
+
return variant === "create" ? "true" : "false";
|
|
2743
|
+
case "date":
|
|
2744
|
+
return variant === "alt" ? "'2026-02-01'" : "'2026-01-01'";
|
|
2745
|
+
case "datetime":
|
|
2746
|
+
return variant === "alt" ? "'2026-02-01T00:00:00.000Z'" : "'2026-01-01T00:00:00.000Z'";
|
|
2747
|
+
case "json":
|
|
2748
|
+
return "{}";
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
function pickFilterField(fields) {
|
|
2752
|
+
return fields.find((f) => f.type === "string" || f.type === "text") ?? fields.find((f) => f.type === "number") ?? fields.find((f) => f.type === "boolean") ?? fields[0];
|
|
2753
|
+
}
|
|
2754
|
+
function generateFastapiTest(config) {
|
|
2755
|
+
const className = toPascal(config.name);
|
|
2756
|
+
const snake = toSnake(config.name);
|
|
2757
|
+
const apiUrl = `/api/v1${config.apiPrefix}/`;
|
|
2758
|
+
const filterField = pickFilterField(config.fields);
|
|
2759
|
+
const needsDate = config.fields.some((f) => f.type === "date");
|
|
2760
|
+
const needsDatetime = config.fields.some((f) => f.type === "datetime");
|
|
2761
|
+
const dateImports = [];
|
|
2762
|
+
if (needsDate) dateImports.push("date");
|
|
2763
|
+
if (needsDatetime) dateImports.push("datetime");
|
|
2764
|
+
const lines = [];
|
|
2765
|
+
if (dateImports.length > 0) {
|
|
2766
|
+
lines.push(`from datetime import ${dateImports.join(", ")}`);
|
|
2767
|
+
lines.push("");
|
|
2768
|
+
}
|
|
2769
|
+
lines.push(`from src.entities.${snake}._model import ${className}`);
|
|
2770
|
+
lines.push(`from tests.base_entity_api_test import BaseEntityApiTest`);
|
|
2771
|
+
lines.push("");
|
|
2772
|
+
lines.push("");
|
|
2773
|
+
lines.push(`class Test${className}Entity(BaseEntityApiTest):`);
|
|
2774
|
+
lines.push(` __test__ = True`);
|
|
2775
|
+
lines.push(` endpoint = "${apiUrl}"`);
|
|
2776
|
+
lines.push(` create_payload = {`);
|
|
2777
|
+
for (const f of config.fields) {
|
|
2778
|
+
lines.push(` "${f.name}": ${pyHttpLiteral(f.type, "create")},`);
|
|
2779
|
+
}
|
|
2780
|
+
lines.push(` }`);
|
|
2781
|
+
const updateField = config.fields[0];
|
|
2782
|
+
lines.push(` update_payload = {"${updateField.name}": ${pyHttpLiteral(updateField.type, "update")}}`);
|
|
2783
|
+
lines.push(` invalid_payload: dict = {}`);
|
|
2784
|
+
lines.push(` filter_field = "${filterField.name}"`);
|
|
2785
|
+
lines.push(` filter_value = ${pyHttpLiteral(filterField.type, "create")}`);
|
|
2786
|
+
lines.push(` other_filter_value = ${pyHttpLiteral(filterField.type, "alt")}`);
|
|
2787
|
+
lines.push("");
|
|
2788
|
+
lines.push(` def make_model(self, index: int, **overrides):`);
|
|
2789
|
+
lines.push(` data = {`);
|
|
2790
|
+
for (const f of config.fields) {
|
|
2791
|
+
lines.push(` "${f.name}": ${pyOrmLiteral(f.type, "create")},`);
|
|
2792
|
+
}
|
|
2793
|
+
lines.push(` }`);
|
|
2794
|
+
lines.push(` data.update(overrides)`);
|
|
2795
|
+
lines.push(` return ${className}(**data)`);
|
|
2796
|
+
lines.push("");
|
|
2797
|
+
return lines.join("\n");
|
|
2798
|
+
}
|
|
2799
|
+
function generateFastifyTest(config) {
|
|
2800
|
+
const className = toPascal(config.name);
|
|
2801
|
+
const basePath = `/api/v1${config.apiPrefix}`;
|
|
2802
|
+
const updateField = config.fields[0];
|
|
2803
|
+
const lines = [];
|
|
2804
|
+
lines.push(`import { describeCrudEntity } from '../helpers/crud-test-base.js';`);
|
|
2805
|
+
lines.push("");
|
|
2806
|
+
lines.push(`describeCrudEntity({`);
|
|
2807
|
+
lines.push(` entityName: '${className}',`);
|
|
2808
|
+
lines.push(` basePath: '${basePath}',`);
|
|
2809
|
+
lines.push(` prismaModel: '${className}',`);
|
|
2810
|
+
lines.push(` createPayload: {`);
|
|
2811
|
+
for (const f of config.fields) {
|
|
2812
|
+
lines.push(` ${f.name}: ${tsLiteral(f.type, "create")},`);
|
|
2813
|
+
}
|
|
2814
|
+
lines.push(` },`);
|
|
2815
|
+
lines.push(` updatePayload: {`);
|
|
2816
|
+
lines.push(` ${updateField.name}: ${tsLiteral(updateField.type, "update")},`);
|
|
2817
|
+
lines.push(` },`);
|
|
2818
|
+
lines.push(`});`);
|
|
2819
|
+
lines.push("");
|
|
2820
|
+
return lines.join("\n");
|
|
2821
|
+
}
|
|
2745
2822
|
async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
|
|
2746
2823
|
if (backendFlag) return backendFlag;
|
|
2747
2824
|
if (hasFastapi && !hasFastify) return "fastapi";
|
|
@@ -2826,6 +2903,12 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2826
2903
|
await mkdir5(entityDir, { recursive: true });
|
|
2827
2904
|
await writeFile5(join12(entityDir, "_model.py"), generateFastAPIModel(config));
|
|
2828
2905
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
|
|
2906
|
+
const testsDir = join12(cwd, dir, "tests");
|
|
2907
|
+
const testFile = join12(testsDir, `test_${toSnake(config.name)}_entity.py`);
|
|
2908
|
+
if (existsSync11(testsDir) && !existsSync11(testFile)) {
|
|
2909
|
+
await writeFile5(testFile, generateFastapiTest(config));
|
|
2910
|
+
generated.push(`${dir}/tests/test_${toSnake(config.name)}_entity.py`);
|
|
2911
|
+
}
|
|
2829
2912
|
}
|
|
2830
2913
|
}
|
|
2831
2914
|
if (genFastify) {
|
|
@@ -2865,6 +2948,12 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2865
2948
|
generated.push(`${dir}/prisma/schema.prisma (model added)`);
|
|
2866
2949
|
}
|
|
2867
2950
|
}
|
|
2951
|
+
const testsModulesDir = join12(cwd, dir, "tests/modules");
|
|
2952
|
+
const fastifyTestFile = join12(testsModulesDir, `${toKebab(config.name)}.test.ts`);
|
|
2953
|
+
if (existsSync11(testsModulesDir) && !existsSync11(fastifyTestFile)) {
|
|
2954
|
+
await writeFile5(fastifyTestFile, generateFastifyTest(config));
|
|
2955
|
+
generated.push(`${dir}/tests/modules/${toKebab(config.name)}.test.ts`);
|
|
2956
|
+
}
|
|
2868
2957
|
}
|
|
2869
2958
|
}
|
|
2870
2959
|
if (hasFrontend) {
|
|
@@ -3225,436 +3314,6 @@ function detectMetaUrl(cwd) {
|
|
|
3225
3314
|
return "http://localhost:8000/api/v1/_meta";
|
|
3226
3315
|
}
|
|
3227
3316
|
|
|
3228
|
-
// src/mcp.ts
|
|
3229
|
-
import { spawn } from "child_process";
|
|
3230
|
-
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
3231
|
-
var TOOL_DEFS = [
|
|
3232
|
-
{
|
|
3233
|
-
name: "projx_scaffold",
|
|
3234
|
-
description: "Create a new projx project",
|
|
3235
|
-
inputSchema: {
|
|
3236
|
-
type: "object",
|
|
3237
|
-
properties: {
|
|
3238
|
-
name: { type: "string", minLength: 1 },
|
|
3239
|
-
components: {
|
|
3240
|
-
type: "array",
|
|
3241
|
-
items: { type: "string", enum: [...COMPONENTS] }
|
|
3242
|
-
},
|
|
3243
|
-
git: { type: "boolean" },
|
|
3244
|
-
install: { type: "boolean" },
|
|
3245
|
-
cwd: { type: "string" }
|
|
3246
|
-
},
|
|
3247
|
-
required: ["name"],
|
|
3248
|
-
additionalProperties: false
|
|
3249
|
-
}
|
|
3250
|
-
},
|
|
3251
|
-
{
|
|
3252
|
-
name: "projx_scaffold_fullstack",
|
|
3253
|
-
description: "Scaffold a production-ready fullstack app with backend, frontend, e2e, and optional mobile/infra",
|
|
3254
|
-
inputSchema: {
|
|
3255
|
-
type: "object",
|
|
3256
|
-
properties: {
|
|
3257
|
-
project_name: { type: "string", minLength: 1 },
|
|
3258
|
-
backend: { type: "string", enum: ["fastapi", "fastify"] },
|
|
3259
|
-
include_frontend: { type: "boolean", default: true },
|
|
3260
|
-
include_mobile: { type: "boolean", default: false },
|
|
3261
|
-
include_e2e: { type: "boolean", default: true },
|
|
3262
|
-
include_infra: { type: "boolean", default: false },
|
|
3263
|
-
package_manager: { type: "string", enum: [...PACKAGE_MANAGERS] },
|
|
3264
|
-
install_deps: { type: "boolean", default: true },
|
|
3265
|
-
init_git: { type: "boolean", default: true },
|
|
3266
|
-
cwd: { type: "string" }
|
|
3267
|
-
},
|
|
3268
|
-
required: ["project_name", "backend"],
|
|
3269
|
-
additionalProperties: false
|
|
3270
|
-
}
|
|
3271
|
-
},
|
|
3272
|
-
{
|
|
3273
|
-
name: "projx_add_components",
|
|
3274
|
-
description: "Add components to an existing projx project",
|
|
3275
|
-
inputSchema: {
|
|
3276
|
-
type: "object",
|
|
3277
|
-
properties: {
|
|
3278
|
-
components: {
|
|
3279
|
-
type: "array",
|
|
3280
|
-
items: { type: "string", enum: [...COMPONENTS] }
|
|
3281
|
-
},
|
|
3282
|
-
install: { type: "boolean" },
|
|
3283
|
-
cwd: { type: "string" }
|
|
3284
|
-
},
|
|
3285
|
-
required: ["components"],
|
|
3286
|
-
additionalProperties: false
|
|
3287
|
-
}
|
|
3288
|
-
},
|
|
3289
|
-
{
|
|
3290
|
-
name: "projx_update",
|
|
3291
|
-
description: "Update an existing projx project to latest scaffolding",
|
|
3292
|
-
inputSchema: {
|
|
3293
|
-
type: "object",
|
|
3294
|
-
properties: {
|
|
3295
|
-
cwd: { type: "string" }
|
|
3296
|
-
},
|
|
3297
|
-
additionalProperties: false
|
|
3298
|
-
}
|
|
3299
|
-
},
|
|
3300
|
-
{
|
|
3301
|
-
name: "projx_doctor",
|
|
3302
|
-
description: "Run projx health checks",
|
|
3303
|
-
inputSchema: {
|
|
3304
|
-
type: "object",
|
|
3305
|
-
properties: {
|
|
3306
|
-
cwd: { type: "string" },
|
|
3307
|
-
fix: { type: "boolean" }
|
|
3308
|
-
},
|
|
3309
|
-
additionalProperties: false
|
|
3310
|
-
}
|
|
3311
|
-
}
|
|
3312
|
-
];
|
|
3313
|
-
function isComponent(value) {
|
|
3314
|
-
return typeof value === "string" && COMPONENTS.includes(value);
|
|
3315
|
-
}
|
|
3316
|
-
function ensureObject(value) {
|
|
3317
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3318
|
-
throw new Error("arguments must be an object");
|
|
3319
|
-
}
|
|
3320
|
-
return value;
|
|
3321
|
-
}
|
|
3322
|
-
function parseScaffoldArgs(raw) {
|
|
3323
|
-
const args2 = ensureObject(raw);
|
|
3324
|
-
const name = args2.name;
|
|
3325
|
-
if (typeof name !== "string" || name.trim().length === 0) {
|
|
3326
|
-
throw new Error("name is required");
|
|
3327
|
-
}
|
|
3328
|
-
const parsed = { name: name.trim() };
|
|
3329
|
-
if (args2.components !== void 0) {
|
|
3330
|
-
if (!Array.isArray(args2.components))
|
|
3331
|
-
throw new Error("components must be an array");
|
|
3332
|
-
if (!args2.components.every(isComponent)) {
|
|
3333
|
-
throw new Error(`components must be one of: ${COMPONENTS.join(", ")}`);
|
|
3334
|
-
}
|
|
3335
|
-
parsed.components = args2.components;
|
|
3336
|
-
}
|
|
3337
|
-
if (args2.git !== void 0) {
|
|
3338
|
-
if (typeof args2.git !== "boolean") throw new Error("git must be a boolean");
|
|
3339
|
-
parsed.git = args2.git;
|
|
3340
|
-
}
|
|
3341
|
-
if (args2.install !== void 0) {
|
|
3342
|
-
if (typeof args2.install !== "boolean")
|
|
3343
|
-
throw new Error("install must be a boolean");
|
|
3344
|
-
parsed.install = args2.install;
|
|
3345
|
-
}
|
|
3346
|
-
if (args2.cwd !== void 0) {
|
|
3347
|
-
if (typeof args2.cwd !== "string" || args2.cwd.length === 0)
|
|
3348
|
-
throw new Error("cwd must be a string");
|
|
3349
|
-
parsed.cwd = args2.cwd;
|
|
3350
|
-
}
|
|
3351
|
-
if (args2.packageManager !== void 0) {
|
|
3352
|
-
if (typeof args2.packageManager !== "string" || !PACKAGE_MANAGERS.includes(args2.packageManager)) {
|
|
3353
|
-
throw new Error(
|
|
3354
|
-
`packageManager must be one of: ${PACKAGE_MANAGERS.join(", ")}`
|
|
3355
|
-
);
|
|
3356
|
-
}
|
|
3357
|
-
parsed.packageManager = args2.packageManager;
|
|
3358
|
-
}
|
|
3359
|
-
return parsed;
|
|
3360
|
-
}
|
|
3361
|
-
function parseScaffoldFullstackArgs(raw) {
|
|
3362
|
-
const args2 = ensureObject(raw);
|
|
3363
|
-
const projectName = args2.project_name;
|
|
3364
|
-
if (typeof projectName !== "string" || projectName.trim().length === 0) {
|
|
3365
|
-
throw new Error("project_name is required");
|
|
3366
|
-
}
|
|
3367
|
-
const backend = args2.backend;
|
|
3368
|
-
if (backend !== "fastapi" && backend !== "fastify") {
|
|
3369
|
-
throw new Error("backend is required and must be fastapi or fastify");
|
|
3370
|
-
}
|
|
3371
|
-
const parsed = {
|
|
3372
|
-
projectName: projectName.trim(),
|
|
3373
|
-
backend,
|
|
3374
|
-
includeFrontend: args2.include_frontend !== false,
|
|
3375
|
-
includeMobile: args2.include_mobile === true,
|
|
3376
|
-
includeE2E: args2.include_e2e !== false,
|
|
3377
|
-
includeInfra: args2.include_infra === true,
|
|
3378
|
-
installDeps: args2.install_deps !== false,
|
|
3379
|
-
initGit: args2.init_git !== false
|
|
3380
|
-
};
|
|
3381
|
-
if (args2.package_manager !== void 0) {
|
|
3382
|
-
if (typeof args2.package_manager !== "string" || !PACKAGE_MANAGERS.includes(args2.package_manager)) {
|
|
3383
|
-
throw new Error(
|
|
3384
|
-
`package_manager must be one of: ${PACKAGE_MANAGERS.join(", ")}`
|
|
3385
|
-
);
|
|
3386
|
-
}
|
|
3387
|
-
parsed.packageManager = args2.package_manager;
|
|
3388
|
-
}
|
|
3389
|
-
if (args2.cwd !== void 0) {
|
|
3390
|
-
if (typeof args2.cwd !== "string" || args2.cwd.length === 0)
|
|
3391
|
-
throw new Error("cwd must be a string");
|
|
3392
|
-
parsed.cwd = args2.cwd;
|
|
3393
|
-
}
|
|
3394
|
-
return parsed;
|
|
3395
|
-
}
|
|
3396
|
-
function parseAddArgs(raw) {
|
|
3397
|
-
const args2 = ensureObject(raw);
|
|
3398
|
-
if (!Array.isArray(args2.components) || args2.components.length === 0) {
|
|
3399
|
-
throw new Error("components is required");
|
|
3400
|
-
}
|
|
3401
|
-
if (!args2.components.every(isComponent)) {
|
|
3402
|
-
throw new Error(`components must be one of: ${COMPONENTS.join(", ")}`);
|
|
3403
|
-
}
|
|
3404
|
-
const parsed = { components: args2.components };
|
|
3405
|
-
if (args2.install !== void 0) {
|
|
3406
|
-
if (typeof args2.install !== "boolean")
|
|
3407
|
-
throw new Error("install must be a boolean");
|
|
3408
|
-
parsed.install = args2.install;
|
|
3409
|
-
}
|
|
3410
|
-
if (args2.cwd !== void 0) {
|
|
3411
|
-
if (typeof args2.cwd !== "string" || args2.cwd.length === 0)
|
|
3412
|
-
throw new Error("cwd must be a string");
|
|
3413
|
-
parsed.cwd = args2.cwd;
|
|
3414
|
-
}
|
|
3415
|
-
return parsed;
|
|
3416
|
-
}
|
|
3417
|
-
function parseUpdateArgs(raw) {
|
|
3418
|
-
if (raw === void 0) return {};
|
|
3419
|
-
const args2 = ensureObject(raw);
|
|
3420
|
-
if (args2.cwd !== void 0 && (typeof args2.cwd !== "string" || args2.cwd.length === 0)) {
|
|
3421
|
-
throw new Error("cwd must be a string");
|
|
3422
|
-
}
|
|
3423
|
-
return { cwd: args2.cwd };
|
|
3424
|
-
}
|
|
3425
|
-
function parseDoctorArgs(raw) {
|
|
3426
|
-
if (raw === void 0) return {};
|
|
3427
|
-
const args2 = ensureObject(raw);
|
|
3428
|
-
const parsed = {};
|
|
3429
|
-
if (args2.cwd !== void 0) {
|
|
3430
|
-
if (typeof args2.cwd !== "string" || args2.cwd.length === 0)
|
|
3431
|
-
throw new Error("cwd must be a string");
|
|
3432
|
-
parsed.cwd = args2.cwd;
|
|
3433
|
-
}
|
|
3434
|
-
if (args2.fix !== void 0) {
|
|
3435
|
-
if (typeof args2.fix !== "boolean") throw new Error("fix must be a boolean");
|
|
3436
|
-
parsed.fix = args2.fix;
|
|
3437
|
-
}
|
|
3438
|
-
return parsed;
|
|
3439
|
-
}
|
|
3440
|
-
function jsonRpcError(id, code, message) {
|
|
3441
|
-
return {
|
|
3442
|
-
jsonrpc: "2.0",
|
|
3443
|
-
id,
|
|
3444
|
-
error: { code, message }
|
|
3445
|
-
};
|
|
3446
|
-
}
|
|
3447
|
-
function textResult(id, text4) {
|
|
3448
|
-
return {
|
|
3449
|
-
jsonrpc: "2.0",
|
|
3450
|
-
id,
|
|
3451
|
-
result: {
|
|
3452
|
-
content: [{ type: "text", text: text4 }]
|
|
3453
|
-
}
|
|
3454
|
-
};
|
|
3455
|
-
}
|
|
3456
|
-
async function runCli(args2, cwd) {
|
|
3457
|
-
const entry = process.argv[1];
|
|
3458
|
-
if (!entry) {
|
|
3459
|
-
throw new Error("Unable to resolve CLI entrypoint");
|
|
3460
|
-
}
|
|
3461
|
-
await new Promise((resolvePromise, rejectPromise) => {
|
|
3462
|
-
const child = spawn(process.execPath, [entry, ...args2], {
|
|
3463
|
-
cwd: cwd ?? process.cwd(),
|
|
3464
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3465
|
-
env: process.env
|
|
3466
|
-
});
|
|
3467
|
-
let stderr = "";
|
|
3468
|
-
child.stderr.on("data", (chunk) => {
|
|
3469
|
-
stderr += chunk.toString();
|
|
3470
|
-
});
|
|
3471
|
-
child.on("error", rejectPromise);
|
|
3472
|
-
child.on("close", (code) => {
|
|
3473
|
-
if (code === 0) {
|
|
3474
|
-
resolvePromise();
|
|
3475
|
-
} else {
|
|
3476
|
-
rejectPromise(
|
|
3477
|
-
new Error(stderr.trim() || `Command failed with exit code ${code}`)
|
|
3478
|
-
);
|
|
3479
|
-
}
|
|
3480
|
-
});
|
|
3481
|
-
});
|
|
3482
|
-
}
|
|
3483
|
-
function defaultActions() {
|
|
3484
|
-
return {
|
|
3485
|
-
scaffold: async ({
|
|
3486
|
-
name,
|
|
3487
|
-
components,
|
|
3488
|
-
git,
|
|
3489
|
-
install,
|
|
3490
|
-
packageManager,
|
|
3491
|
-
cwd
|
|
3492
|
-
}) => {
|
|
3493
|
-
const cmd = [name];
|
|
3494
|
-
if (components && components.length > 0) {
|
|
3495
|
-
cmd.push("--components", components.join(","));
|
|
3496
|
-
}
|
|
3497
|
-
if (packageManager) {
|
|
3498
|
-
cmd.push("--package-manager", packageManager);
|
|
3499
|
-
}
|
|
3500
|
-
if (git === false) cmd.push("--no-git");
|
|
3501
|
-
if (install === false) cmd.push("--no-install");
|
|
3502
|
-
await runCli(cmd, cwd);
|
|
3503
|
-
},
|
|
3504
|
-
add: async ({ components, cwd, install }) => {
|
|
3505
|
-
const cmd = ["add", ...components];
|
|
3506
|
-
if (install === false) cmd.push("--no-install");
|
|
3507
|
-
await runCli(cmd, cwd);
|
|
3508
|
-
},
|
|
3509
|
-
update: async ({ cwd }) => {
|
|
3510
|
-
await runCli(["update"], cwd);
|
|
3511
|
-
},
|
|
3512
|
-
doctor: async ({ cwd, fix }) => {
|
|
3513
|
-
const cmd = ["doctor"];
|
|
3514
|
-
if (fix) cmd.push("--fix");
|
|
3515
|
-
await runCli(cmd, cwd);
|
|
3516
|
-
}
|
|
3517
|
-
};
|
|
3518
|
-
}
|
|
3519
|
-
async function handleMcpRequest(request, actions) {
|
|
3520
|
-
if (request.jsonrpc !== "2.0") {
|
|
3521
|
-
return jsonRpcError(request.id, -32600, "Invalid Request");
|
|
3522
|
-
}
|
|
3523
|
-
const effectiveActions = {
|
|
3524
|
-
...defaultActions(),
|
|
3525
|
-
...actions
|
|
3526
|
-
};
|
|
3527
|
-
try {
|
|
3528
|
-
if (request.method === "initialize") {
|
|
3529
|
-
return {
|
|
3530
|
-
jsonrpc: "2.0",
|
|
3531
|
-
id: request.id,
|
|
3532
|
-
result: {
|
|
3533
|
-
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
3534
|
-
capabilities: { tools: {} },
|
|
3535
|
-
serverInfo: { name: "projx", version: "1.0.0" }
|
|
3536
|
-
}
|
|
3537
|
-
};
|
|
3538
|
-
}
|
|
3539
|
-
if (request.method === "tools/list") {
|
|
3540
|
-
return {
|
|
3541
|
-
jsonrpc: "2.0",
|
|
3542
|
-
id: request.id,
|
|
3543
|
-
result: { tools: TOOL_DEFS }
|
|
3544
|
-
};
|
|
3545
|
-
}
|
|
3546
|
-
if (request.method === "tools/call") {
|
|
3547
|
-
const params = ensureObject(request.params);
|
|
3548
|
-
const toolName = params.name;
|
|
3549
|
-
const toolArgs = params.arguments;
|
|
3550
|
-
if (typeof toolName !== "string") {
|
|
3551
|
-
return jsonRpcError(request.id, -32602, "name must be a string");
|
|
3552
|
-
}
|
|
3553
|
-
if (toolName === "projx_scaffold") {
|
|
3554
|
-
const parsed = parseScaffoldArgs(toolArgs);
|
|
3555
|
-
await effectiveActions.scaffold(parsed);
|
|
3556
|
-
return textResult(request.id, `Created project ${parsed.name}`);
|
|
3557
|
-
}
|
|
3558
|
-
if (toolName === "projx_scaffold_fullstack") {
|
|
3559
|
-
const parsed = parseScaffoldFullstackArgs(toolArgs);
|
|
3560
|
-
const components = [parsed.backend];
|
|
3561
|
-
if (parsed.includeFrontend) components.push("frontend");
|
|
3562
|
-
if (parsed.includeMobile) components.push("mobile");
|
|
3563
|
-
if (parsed.includeE2E) components.push("e2e");
|
|
3564
|
-
if (parsed.includeInfra) components.push("infra");
|
|
3565
|
-
await effectiveActions.scaffold({
|
|
3566
|
-
name: parsed.projectName,
|
|
3567
|
-
components,
|
|
3568
|
-
git: parsed.initGit,
|
|
3569
|
-
install: parsed.installDeps,
|
|
3570
|
-
packageManager: parsed.packageManager,
|
|
3571
|
-
cwd: parsed.cwd
|
|
3572
|
-
});
|
|
3573
|
-
return textResult(
|
|
3574
|
-
request.id,
|
|
3575
|
-
`Successfully scaffolded ${parsed.projectName} with ${parsed.backend}. Files are located at ./${parsed.projectName}`
|
|
3576
|
-
);
|
|
3577
|
-
}
|
|
3578
|
-
if (toolName === "projx_add_components") {
|
|
3579
|
-
const parsed = parseAddArgs(toolArgs);
|
|
3580
|
-
await effectiveActions.add(parsed);
|
|
3581
|
-
return textResult(
|
|
3582
|
-
request.id,
|
|
3583
|
-
`Added components: ${parsed.components.join(", ")}`
|
|
3584
|
-
);
|
|
3585
|
-
}
|
|
3586
|
-
if (toolName === "projx_update") {
|
|
3587
|
-
const parsed = parseUpdateArgs(toolArgs);
|
|
3588
|
-
await effectiveActions.update(parsed);
|
|
3589
|
-
return textResult(request.id, "Updated projx scaffolding");
|
|
3590
|
-
}
|
|
3591
|
-
if (toolName === "projx_doctor") {
|
|
3592
|
-
const parsed = parseDoctorArgs(toolArgs);
|
|
3593
|
-
await effectiveActions.doctor(parsed);
|
|
3594
|
-
return textResult(request.id, "Doctor check completed");
|
|
3595
|
-
}
|
|
3596
|
-
return jsonRpcError(request.id, -32602, `Unknown tool: ${toolName}`);
|
|
3597
|
-
}
|
|
3598
|
-
if (request.method === "ping") {
|
|
3599
|
-
return {
|
|
3600
|
-
jsonrpc: "2.0",
|
|
3601
|
-
id: request.id,
|
|
3602
|
-
result: { ok: true }
|
|
3603
|
-
};
|
|
3604
|
-
}
|
|
3605
|
-
return jsonRpcError(
|
|
3606
|
-
request.id,
|
|
3607
|
-
-32601,
|
|
3608
|
-
`Method not found: ${request.method}`
|
|
3609
|
-
);
|
|
3610
|
-
} catch (error) {
|
|
3611
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3612
|
-
return jsonRpcError(request.id, -32602, message);
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
function writeMessage(msg) {
|
|
3616
|
-
const payload = JSON.stringify(msg);
|
|
3617
|
-
const length = Buffer.byteLength(payload, "utf8");
|
|
3618
|
-
process.stdout.write(`Content-Length: ${length}\r
|
|
3619
|
-
\r
|
|
3620
|
-
${payload}`);
|
|
3621
|
-
}
|
|
3622
|
-
function startMcpServer() {
|
|
3623
|
-
let buffer = Buffer.alloc(0);
|
|
3624
|
-
process.stdin.on("data", (chunk) => {
|
|
3625
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
3626
|
-
while (true) {
|
|
3627
|
-
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
3628
|
-
if (headerEnd === -1) break;
|
|
3629
|
-
const header = buffer.subarray(0, headerEnd).toString("utf8");
|
|
3630
|
-
const lengthMatch = header.match(/content-length:\s*(\d+)/i);
|
|
3631
|
-
if (!lengthMatch) {
|
|
3632
|
-
buffer = buffer.subarray(headerEnd + 4);
|
|
3633
|
-
continue;
|
|
3634
|
-
}
|
|
3635
|
-
const contentLength = Number(lengthMatch[1]);
|
|
3636
|
-
const bodyStart = headerEnd + 4;
|
|
3637
|
-
const bodyEnd = bodyStart + contentLength;
|
|
3638
|
-
if (buffer.length < bodyEnd) break;
|
|
3639
|
-
const body = buffer.subarray(bodyStart, bodyEnd).toString("utf8");
|
|
3640
|
-
buffer = buffer.subarray(bodyEnd);
|
|
3641
|
-
let request;
|
|
3642
|
-
try {
|
|
3643
|
-
request = JSON.parse(body);
|
|
3644
|
-
} catch {
|
|
3645
|
-
writeMessage(jsonRpcError(void 0, -32700, "Parse error"));
|
|
3646
|
-
continue;
|
|
3647
|
-
}
|
|
3648
|
-
void handleMcpRequest(request).then((response) => {
|
|
3649
|
-
if (request.id !== void 0) {
|
|
3650
|
-
writeMessage(response);
|
|
3651
|
-
}
|
|
3652
|
-
});
|
|
3653
|
-
}
|
|
3654
|
-
});
|
|
3655
|
-
process.stdin.resume();
|
|
3656
|
-
}
|
|
3657
|
-
|
|
3658
3317
|
// src/index.ts
|
|
3659
3318
|
var args = process.argv.slice(2);
|
|
3660
3319
|
function parseArgs() {
|
|
@@ -3702,21 +3361,12 @@ function parseArgs() {
|
|
|
3702
3361
|
command = "sync";
|
|
3703
3362
|
continue;
|
|
3704
3363
|
}
|
|
3705
|
-
if (arg === "mcp" && !name) {
|
|
3706
|
-
command = "mcp";
|
|
3707
|
-
continue;
|
|
3708
|
-
}
|
|
3709
3364
|
if (arg === "--components") {
|
|
3710
3365
|
const val = args[++i];
|
|
3711
3366
|
if (val) {
|
|
3712
|
-
options.components = val.split(",").filter(
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
}
|
|
3716
|
-
if (arg === "--package-manager" || arg === "--pm") {
|
|
3717
|
-
const val = args[++i];
|
|
3718
|
-
if (val && PACKAGE_MANAGERS.includes(val)) {
|
|
3719
|
-
options.packageManager = val;
|
|
3367
|
+
options.components = val.split(",").filter(
|
|
3368
|
+
(c) => COMPONENTS.includes(c)
|
|
3369
|
+
);
|
|
3720
3370
|
}
|
|
3721
3371
|
continue;
|
|
3722
3372
|
}
|
|
@@ -3790,11 +3440,9 @@ function printHelp() {
|
|
|
3790
3440
|
projx doctor [--fix] Health check for projx project
|
|
3791
3441
|
projx gen entity <name> Generate a new entity
|
|
3792
3442
|
projx sync [--url <url>] Sync types from running backend
|
|
3793
|
-
projx mcp Start MCP server over stdio
|
|
3794
3443
|
|
|
3795
3444
|
Options:
|
|
3796
3445
|
--components <list> Comma-separated: fastapi,fastify,frontend,mobile,e2e,infra
|
|
3797
|
-
--package-manager One of: npm, pnpm, yarn, bun
|
|
3798
3446
|
--no-git Skip git init
|
|
3799
3447
|
--no-install Skip dependency installation
|
|
3800
3448
|
-y, --yes Accept defaults (fastify + frontend + e2e)
|
|
@@ -3829,9 +3477,7 @@ async function main() {
|
|
|
3829
3477
|
(c) => COMPONENTS.includes(c)
|
|
3830
3478
|
);
|
|
3831
3479
|
if (components.length === 0) {
|
|
3832
|
-
console.error(
|
|
3833
|
-
`Error: specify components to add. Available: ${COMPONENTS.join(", ")}`
|
|
3834
|
-
);
|
|
3480
|
+
console.error(`Error: specify components to add. Available: ${COMPONENTS.join(", ")}`);
|
|
3835
3481
|
process.exit(1);
|
|
3836
3482
|
}
|
|
3837
3483
|
await add(process.cwd(), components, localRepo, options.install === false);
|
|
@@ -3847,9 +3493,7 @@ async function main() {
|
|
|
3847
3493
|
}
|
|
3848
3494
|
if (command === "unpin") {
|
|
3849
3495
|
if (extraArgs.length === 0) {
|
|
3850
|
-
console.error(
|
|
3851
|
-
"Error: specify patterns to unpin. Usage: projx unpin <patterns...>"
|
|
3852
|
-
);
|
|
3496
|
+
console.error("Error: specify patterns to unpin. Usage: projx unpin <patterns...>");
|
|
3853
3497
|
process.exit(1);
|
|
3854
3498
|
}
|
|
3855
3499
|
await unpin(process.cwd(), extraArgs);
|
|
@@ -3869,16 +3513,10 @@ async function main() {
|
|
|
3869
3513
|
await sync(process.cwd(), url);
|
|
3870
3514
|
return;
|
|
3871
3515
|
}
|
|
3872
|
-
if (command === "mcp") {
|
|
3873
|
-
startMcpServer();
|
|
3874
|
-
return;
|
|
3875
|
-
}
|
|
3876
3516
|
if (command === "gen") {
|
|
3877
3517
|
const subcommand = extraArgs[0];
|
|
3878
3518
|
if (subcommand !== "entity" || !extraArgs[1]) {
|
|
3879
|
-
console.error(
|
|
3880
|
-
'Usage: projx gen entity <name> [--fields "name:string,amount:number"]'
|
|
3881
|
-
);
|
|
3519
|
+
console.error('Usage: projx gen entity <name> [--fields "name:string,amount:number"]');
|
|
3882
3520
|
process.exit(1);
|
|
3883
3521
|
}
|
|
3884
3522
|
const entityName = extraArgs[1];
|
|
@@ -3898,14 +3536,12 @@ async function main() {
|
|
|
3898
3536
|
name,
|
|
3899
3537
|
components: options.components,
|
|
3900
3538
|
git: options.git ?? true,
|
|
3901
|
-
install: options.install ?? true
|
|
3902
|
-
packageManager: options.packageManager
|
|
3539
|
+
install: options.install ?? true
|
|
3903
3540
|
};
|
|
3904
3541
|
} else {
|
|
3905
3542
|
opts = await runPrompts(name);
|
|
3906
3543
|
opts.git = options.git ?? opts.git;
|
|
3907
3544
|
opts.install = options.install ?? opts.install;
|
|
3908
|
-
opts.packageManager = options.packageManager ?? opts.packageManager;
|
|
3909
3545
|
}
|
|
3910
3546
|
const dest = resolve2(process.cwd(), opts.name);
|
|
3911
3547
|
if (existsSync13(dest)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-projx",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.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": {
|
|
@@ -96,14 +96,6 @@ cd <%= paths.e2e %> && <%= pm.exec %> playwright test
|
|
|
96
96
|
npx create-projx@latest update
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
## MCP
|
|
100
|
-
|
|
101
|
-
This project includes a root `.mcp.json` for local MCP stdio usage with compatible AI agents.
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
npx create-projx mcp
|
|
105
|
-
```
|
|
106
|
-
|
|
107
99
|
---
|
|
108
100
|
|
|
109
101
|
[](https://github.com/ukanhaupa/projx)
|