bunforge 1.0.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 +71 -0
- package/bun.lock +91 -0
- package/index.ts +1 -0
- package/package.json +20 -0
- package/src/index.ts +60 -0
- package/src/prompts.ts +31 -0
- package/src/tasks/auth.ts +119 -0
- package/src/tasks/prisma.ts +70 -0
- package/src/utils/files.ts +12 -0
- package/tsconfig.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# bunforge
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.11. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
|
16
|
+
|
|
17
|
+
# bunforge
|
|
18
|
+
|
|
19
|
+
A zero-config CLI scaffolder for Bun projects with optional Prisma + JWT auth.
|
|
20
|
+
|
|
21
|
+
## Install & run
|
|
22
|
+
````bash
|
|
23
|
+
# From within the bunforge repo
|
|
24
|
+
bun start
|
|
25
|
+
````
|
|
26
|
+
|
|
27
|
+
## What it does
|
|
28
|
+
````
|
|
29
|
+
bunforge
|
|
30
|
+
├── asks for a project name
|
|
31
|
+
├── runs bun init -y
|
|
32
|
+
├── [optional] installs & configures Prisma (PostgreSQL + pg adapter)
|
|
33
|
+
│ ├── bun add prisma @types/pg @prisma/client @prisma/adapter-pg pg dotenv
|
|
34
|
+
│ ├── bunx prisma init
|
|
35
|
+
│ ├── writes src/db.ts (PrismaClient with PrismaPg adapter)
|
|
36
|
+
│ └── appends users model + ROLE enum to prisma/schema.prisma
|
|
37
|
+
└── [optional, requires Prisma] scaffolds JWT auth
|
|
38
|
+
├── bun add express @types/express jsonwebtoken @types/jsonwebtoken zod
|
|
39
|
+
├── writes src/Types/types.ts (SignupSchema, SigninSchema via Zod)
|
|
40
|
+
└── writes src/index.ts (Express server, /api/auth/signup & /api/auth/login)
|
|
41
|
+
````
|
|
42
|
+
|
|
43
|
+
## Post-scaffold workflow
|
|
44
|
+
````bash
|
|
45
|
+
cd <your-project>
|
|
46
|
+
|
|
47
|
+
# 1. Fill in your real DB connection
|
|
48
|
+
nano .env # set DATABASE_URL, JWT_SECRET
|
|
49
|
+
|
|
50
|
+
# 2. Run first migration
|
|
51
|
+
bunx prisma migrate dev --name init
|
|
52
|
+
|
|
53
|
+
# 3. Start dev server
|
|
54
|
+
bun run src/index.ts
|
|
55
|
+
````
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
| Method | Path | Body | Response |
|
|
60
|
+
|--------|-------------------|-------------------------------|------------------------------|
|
|
61
|
+
| POST | /api/auth/signup | `{ email, password }` | `{ success: true }` |
|
|
62
|
+
| POST | /api/auth/login | `{ email, password }` | `{ success, data: { token, user } }` |
|
|
63
|
+
|
|
64
|
+
## Protecting routes
|
|
65
|
+
````typescript
|
|
66
|
+
import { authMiddleware } from "./index";
|
|
67
|
+
|
|
68
|
+
app.get("/api/me", authMiddleware(), (req, res) => {
|
|
69
|
+
res.json({ id: req.id, role: req.role });
|
|
70
|
+
});
|
|
71
|
+
````
|
package/bun.lock
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@clack/prompts": "^1.2.0",
|
|
8
|
+
"execa": "^9.6.1",
|
|
9
|
+
"picocolors": "^1.1.1",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "latest",
|
|
13
|
+
"@types/node": "^25.5.0",
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"typescript": "^5",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"packages": {
|
|
21
|
+
"@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="],
|
|
22
|
+
|
|
23
|
+
"@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="],
|
|
24
|
+
|
|
25
|
+
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
|
26
|
+
|
|
27
|
+
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
|
28
|
+
|
|
29
|
+
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
|
30
|
+
|
|
31
|
+
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
|
32
|
+
|
|
33
|
+
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
|
34
|
+
|
|
35
|
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
|
36
|
+
|
|
37
|
+
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
|
|
38
|
+
|
|
39
|
+
"fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="],
|
|
40
|
+
|
|
41
|
+
"fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="],
|
|
42
|
+
|
|
43
|
+
"fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="],
|
|
44
|
+
|
|
45
|
+
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
|
46
|
+
|
|
47
|
+
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
|
48
|
+
|
|
49
|
+
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
|
50
|
+
|
|
51
|
+
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
|
52
|
+
|
|
53
|
+
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
|
54
|
+
|
|
55
|
+
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
|
56
|
+
|
|
57
|
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
|
58
|
+
|
|
59
|
+
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
|
60
|
+
|
|
61
|
+
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
|
62
|
+
|
|
63
|
+
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
|
64
|
+
|
|
65
|
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
|
66
|
+
|
|
67
|
+
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
|
|
68
|
+
|
|
69
|
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
|
70
|
+
|
|
71
|
+
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
|
72
|
+
|
|
73
|
+
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
|
74
|
+
|
|
75
|
+
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
|
76
|
+
|
|
77
|
+
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
|
78
|
+
|
|
79
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
80
|
+
|
|
81
|
+
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
|
82
|
+
|
|
83
|
+
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
|
84
|
+
|
|
85
|
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
|
86
|
+
|
|
87
|
+
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
|
88
|
+
|
|
89
|
+
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
|
90
|
+
}
|
|
91
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("Hello via Bun!");
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bunforge",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"module": "src/index.ts",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bunforge": "./src/index.ts"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "bun src/index.ts",
|
|
10
|
+
"dev": "bun --watch src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@clack/prompts": "^0.9.0",
|
|
14
|
+
"execa": "^9.5.2",
|
|
15
|
+
"picocolors": "^1.1.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { mkdir } from "fs/promises";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
|
|
8
|
+
import { getProjectName, askPrisma, askAuth } from "./prompts.js";
|
|
9
|
+
import { setupPrisma } from "./tasks/prisma.js";
|
|
10
|
+
import { setupAuth } from "./tasks/auth.js";
|
|
11
|
+
|
|
12
|
+
p.intro(pc.bgCyan(pc.black(" bunforge ")) + " — Bun project scaffolder");
|
|
13
|
+
|
|
14
|
+
const name = await getProjectName();
|
|
15
|
+
const projectRoot = join(process.cwd(), name);
|
|
16
|
+
|
|
17
|
+
// ── 1. bun init ──────────────────────────────────────────────────────────────
|
|
18
|
+
const s = p.spinner();
|
|
19
|
+
s.start("Initialising bun project…");
|
|
20
|
+
await mkdir(projectRoot, { recursive: true });
|
|
21
|
+
await execa("bun", ["init", "-y"], { cwd: projectRoot });
|
|
22
|
+
s.stop("Bun project ready");
|
|
23
|
+
|
|
24
|
+
// ── 2. Prisma ────────────────────────────────────────────────────────────────
|
|
25
|
+
const wantsPrisma = await askPrisma();
|
|
26
|
+
if (wantsPrisma) {
|
|
27
|
+
s.start("Setting up Prisma…");
|
|
28
|
+
await setupPrisma(projectRoot);
|
|
29
|
+
s.stop("Prisma ready");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── 3. Auth ──────────────────────────────────────────────────────────────────
|
|
33
|
+
let wantsAuth = false;
|
|
34
|
+
if (wantsPrisma) {
|
|
35
|
+
wantsAuth = await askAuth();
|
|
36
|
+
if (wantsAuth) {
|
|
37
|
+
s.start("Scaffolding auth…");
|
|
38
|
+
await setupAuth(projectRoot);
|
|
39
|
+
s.stop("Auth ready");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── 4. .env ──────────────────────────────────────────────────────────────────
|
|
44
|
+
await Bun.write(
|
|
45
|
+
join(projectRoot, ".env"),
|
|
46
|
+
`DATABASE_URL="postgresql://user:password@localhost:5432/mydb"\nJWT_SECRET="supersecret"\nPORT=3000\n`
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// ── 5. .gitignore ────────────────────────────────────────────────────────────
|
|
50
|
+
await Bun.write(
|
|
51
|
+
join(projectRoot, ".gitignore"),
|
|
52
|
+
`node_modules\n.env\ngenerated\ndist\n`
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// ── Done ─────────────────────────────────────────────────────────────────────
|
|
56
|
+
p.outro(
|
|
57
|
+
pc.green("✔ Done! ") +
|
|
58
|
+
`cd ${name}` +
|
|
59
|
+
(wantsPrisma ? " → update .env → bunx prisma migrate dev --name init -> bunx prisma generate " : "")
|
|
60
|
+
);
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
|
|
4
|
+
export async function getProjectName(): Promise<string> {
|
|
5
|
+
const name = await p.text({
|
|
6
|
+
message: "Project name?",
|
|
7
|
+
placeholder: "my-bun-app",
|
|
8
|
+
validate: (v) => (!v ? "Name is required" : undefined),
|
|
9
|
+
});
|
|
10
|
+
if (p.isCancel(name)) cancel();
|
|
11
|
+
return name as string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function askPrisma(): Promise<boolean> {
|
|
15
|
+
const use = await p.confirm({ message: "Add Prisma (PostgreSQL)?" });
|
|
16
|
+
if (p.isCancel(use)) cancel();
|
|
17
|
+
return use as boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function askAuth(): Promise<boolean> {
|
|
21
|
+
const use = await p.confirm({
|
|
22
|
+
message: "Add JWT auth routes (requires Prisma)?",
|
|
23
|
+
});
|
|
24
|
+
if (p.isCancel(use)) cancel();
|
|
25
|
+
return use as boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cancel(): never {
|
|
29
|
+
p.cancel("Cancelled.");
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { writeFileDeep } from "../utils/files.js";
|
|
3
|
+
|
|
4
|
+
const TYPES_TS = `import z from "zod";
|
|
5
|
+
|
|
6
|
+
export const SignupSchema = z.object({
|
|
7
|
+
email: z.string().email(),
|
|
8
|
+
password: z.string(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const SigninSchema = z.object({
|
|
12
|
+
email: z.string().email(),
|
|
13
|
+
password: z.string(),
|
|
14
|
+
});
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const INDEX_TS = `import express, {
|
|
18
|
+
type NextFunction,
|
|
19
|
+
type Request,
|
|
20
|
+
type Response,
|
|
21
|
+
} from "express";
|
|
22
|
+
import { prisma } from "./prisma/db";
|
|
23
|
+
import jwt, { type JwtPayload } from "jsonwebtoken";
|
|
24
|
+
import { SignupSchema, SigninSchema } from "./Types/types";
|
|
25
|
+
|
|
26
|
+
const app = express();
|
|
27
|
+
const SECRET = process.env.JWT_SECRET ?? "changeme_in_prod";
|
|
28
|
+
|
|
29
|
+
app.use(express.json());
|
|
30
|
+
|
|
31
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
32
|
+
function ferr(msg: string, code: number, res: Response) {
|
|
33
|
+
return res.status(code).json({ success: false, data: null, error: msg });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface AuthRequest extends Request {
|
|
37
|
+
id?: string;
|
|
38
|
+
role?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function authMiddleware() {
|
|
42
|
+
return (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
43
|
+
try {
|
|
44
|
+
const token = req.headers.authorization?.split(" ")[1];
|
|
45
|
+
if (!token) return ferr("UNAUTHORIZED", 401, res);
|
|
46
|
+
const payload = jwt.verify(token, SECRET) as JwtPayload;
|
|
47
|
+
req.id = payload.id;
|
|
48
|
+
req.role = payload.role;
|
|
49
|
+
next();
|
|
50
|
+
} catch {
|
|
51
|
+
return ferr("UNAUTHORIZED", 401, res);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── routes ───────────────────────────────────────────────────────────────────
|
|
57
|
+
app.post("/api/auth/signup", async (req: Request, res: Response) => {
|
|
58
|
+
const parsed = SignupSchema.safeParse(req.body);
|
|
59
|
+
if (!parsed.success) return ferr("INVALID_REQUEST", 400, res);
|
|
60
|
+
|
|
61
|
+
const exists = await prisma.users.findUnique({
|
|
62
|
+
where: { email: parsed.data.email },
|
|
63
|
+
});
|
|
64
|
+
if (exists) return ferr("EMAIL_ALREADY_EXISTS", 400, res);
|
|
65
|
+
|
|
66
|
+
await prisma.users.create({
|
|
67
|
+
data: { email: parsed.data.email, password: parsed.data.password },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return res.status(201).json({ success: true, error: null });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
app.post("/api/auth/login", async (req: Request, res: Response) => {
|
|
74
|
+
const parsed = SigninSchema.safeParse(req.body);
|
|
75
|
+
if (!parsed.success) return ferr("INVALID_REQUEST", 400, res);
|
|
76
|
+
|
|
77
|
+
const user = await prisma.users.findUnique({
|
|
78
|
+
where: { email: parsed.data.email },
|
|
79
|
+
});
|
|
80
|
+
if (!user) return ferr("INVALID_CREDENTIALS", 401, res);
|
|
81
|
+
|
|
82
|
+
const token = jwt.sign({ id: user.id, role: user.role }, SECRET);
|
|
83
|
+
|
|
84
|
+
return res.status(200).json({
|
|
85
|
+
success: true,
|
|
86
|
+
data: { token, user: { id: user.id, email: user.email } },
|
|
87
|
+
error: null,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── start ────────────────────────────────────────────────────────────────────
|
|
92
|
+
const PORT = process.env.PORT ?? 3000;
|
|
93
|
+
app.listen(PORT, () => console.log(\`🚀 Server running on port \${PORT}\`));
|
|
94
|
+
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
export async function setupAuth(projectRoot: string): Promise<void> {
|
|
98
|
+
// Install deps
|
|
99
|
+
await execa(
|
|
100
|
+
"bun",
|
|
101
|
+
[
|
|
102
|
+
"add",
|
|
103
|
+
"express",
|
|
104
|
+
"@types/express",
|
|
105
|
+
"jsonwebtoken",
|
|
106
|
+
"@types/jsonwebtoken",
|
|
107
|
+
"zod",
|
|
108
|
+
],
|
|
109
|
+
{ cwd: projectRoot, stdio: "inherit" }
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Write Types/types.ts
|
|
113
|
+
await writeFileDeep(projectRoot, "Types/types.ts", TYPES_TS);
|
|
114
|
+
|
|
115
|
+
// Write src/index.ts
|
|
116
|
+
await writeFileDeep(projectRoot, "index.ts", INDEX_TS);
|
|
117
|
+
|
|
118
|
+
console.log("\n✅ Auth routes scaffolded in src/index.ts");
|
|
119
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { writeFileDeep } from "../utils/files.js";
|
|
3
|
+
|
|
4
|
+
const DB_TS = `import "dotenv/config";
|
|
5
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
6
|
+
import { PrismaClient } from "../generated/prisma/client";
|
|
7
|
+
|
|
8
|
+
const connectionString = \`\${process.env.DATABASE_URL}\`;
|
|
9
|
+
const adapter = new PrismaPg({ connectionString });
|
|
10
|
+
const prisma = new PrismaClient({ adapter });
|
|
11
|
+
|
|
12
|
+
export { prisma };
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const SCHEMA_ADDITION = `
|
|
16
|
+
enum ROLE {
|
|
17
|
+
customer
|
|
18
|
+
admin
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
model users {
|
|
22
|
+
id String @id @db.VarChar(255) @default(uuid())
|
|
23
|
+
email String @unique @db.VarChar(255)
|
|
24
|
+
password String @db.VarChar(255)
|
|
25
|
+
role ROLE @default(customer)
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
export async function setupPrisma(projectRoot: string): Promise<void> {
|
|
30
|
+
// 1. Install deps
|
|
31
|
+
await execa(
|
|
32
|
+
"bun",
|
|
33
|
+
[
|
|
34
|
+
"add",
|
|
35
|
+
"prisma",
|
|
36
|
+
"@types/pg",
|
|
37
|
+
"--save-dev",
|
|
38
|
+
"@prisma/client",
|
|
39
|
+
"@prisma/adapter-pg",
|
|
40
|
+
"pg",
|
|
41
|
+
"dotenv",
|
|
42
|
+
],
|
|
43
|
+
{ cwd: projectRoot, stdio: "inherit" }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// 2. Init prisma
|
|
47
|
+
await execa("bunx", ["prisma", "init"], {
|
|
48
|
+
cwd: projectRoot,
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 3. Write src/db.ts
|
|
53
|
+
await writeFileDeep(projectRoot, "prisma/db.ts", DB_TS);
|
|
54
|
+
|
|
55
|
+
// 4. Append models to prisma/schema.prisma
|
|
56
|
+
const schemaPath = `${projectRoot}/prisma/schema.prisma`;
|
|
57
|
+
const existing = await Bun.file(schemaPath).text();
|
|
58
|
+
|
|
59
|
+
// Add postgresql adapter preview feature
|
|
60
|
+
const updatedSchema = existing
|
|
61
|
+
.replace(
|
|
62
|
+
/generator client \{([^}]*)\}/,
|
|
63
|
+
`generator client {$1 previewFeatures = ["driverAdapters"]\n}`
|
|
64
|
+
)
|
|
65
|
+
.concat(SCHEMA_ADDITION);
|
|
66
|
+
|
|
67
|
+
await Bun.write(schemaPath, updatedSchema);
|
|
68
|
+
|
|
69
|
+
console.log("\n✅ Prisma set up. Don't forget to set DATABASE_URL in .env");
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
|
|
4
|
+
export async function writeFileDeep(
|
|
5
|
+
projectRoot: string,
|
|
6
|
+
relPath: string,
|
|
7
|
+
content: string
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
const fullPath = join(projectRoot, relPath);
|
|
10
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
11
|
+
await writeFile(fullPath, content, "utf-8");
|
|
12
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
// Environment setup & latest features
|
|
8
|
+
"lib": ["ESNext"],
|
|
9
|
+
"moduleDetection": "force",
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
|
|
13
|
+
// Bundler mode
|
|
14
|
+
"moduleResolution": "bundler",
|
|
15
|
+
"allowImportingTsExtensions": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
|
|
19
|
+
// Best practices
|
|
20
|
+
"strict": true,
|
|
21
|
+
"skipLibCheck": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedIndexedAccess": true,
|
|
24
|
+
"noImplicitOverride": true,
|
|
25
|
+
|
|
26
|
+
// Some stricter flags (disabled by default)
|
|
27
|
+
"noUnusedLocals": false,
|
|
28
|
+
"noUnusedParameters": false,
|
|
29
|
+
"noPropertyAccessFromIndexSignature": false
|
|
30
|
+
}
|
|
31
|
+
}
|