fossyl 0.1.6 → 0.9.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/CLAUDE.md +107 -0
- package/LICENSE +674 -0
- package/bin/fossyl.js +2 -0
- package/dist/index.d.mts +1 -203
- package/dist/index.d.ts +1 -203
- package/dist/index.js +1030 -75
- package/dist/index.mjs +1022 -66
- package/package.json +26 -24
- package/README.md +0 -101
- package/fossyl.svg +0 -2
- package/src/example.ts +0 -31
- package/src/index.ts +0 -22
- package/src/router/router.ts +0 -277
- package/src/router/tmp.sh +0 -15
- package/src/router/types/configuration.types.ts +0 -143
- package/src/router/types/params.types.ts +0 -13
- package/src/router/types/router-creation.types.ts +0 -36
- package/src/router/types/routes.types.ts +0 -124
package/dist/index.js
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __defProps = Object.defineProperties;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
-
var __spreadValues = (a, b) => {
|
|
12
|
-
for (var prop in b || (b = {}))
|
|
13
|
-
if (__hasOwnProp.call(b, prop))
|
|
14
|
-
__defNormalProp(a, prop, b[prop]);
|
|
15
|
-
if (__getOwnPropSymbols)
|
|
16
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
17
|
-
if (__propIsEnum.call(b, prop))
|
|
18
|
-
__defNormalProp(a, prop, b[prop]);
|
|
19
|
-
}
|
|
20
|
-
return a;
|
|
21
|
-
};
|
|
22
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
23
|
-
var __export = (target, all) => {
|
|
24
|
-
for (var name in all)
|
|
25
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
|
-
};
|
|
27
8
|
var __copyProps = (to, from, except, desc) => {
|
|
28
9
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
29
10
|
for (let key of __getOwnPropNames(from))
|
|
@@ -32,72 +13,1046 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
32
13
|
}
|
|
33
14
|
return to;
|
|
34
15
|
};
|
|
35
|
-
var
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
36
24
|
|
|
37
25
|
// src/index.ts
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// src/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
26
|
+
var import_node_util = require("util");
|
|
27
|
+
|
|
28
|
+
// src/commands/create.ts
|
|
29
|
+
var fs2 = __toESM(require("fs"));
|
|
30
|
+
var path2 = __toESM(require("path"));
|
|
31
|
+
var p2 = __toESM(require("@clack/prompts"));
|
|
32
|
+
|
|
33
|
+
// src/prompts.ts
|
|
34
|
+
var p = __toESM(require("@clack/prompts"));
|
|
35
|
+
async function promptForOptions(projectName) {
|
|
36
|
+
p.intro("Create Fossyl App");
|
|
37
|
+
const name = projectName ?? await p.text({
|
|
38
|
+
message: "Project name:",
|
|
39
|
+
placeholder: "my-fossyl-api",
|
|
40
|
+
validate: (v) => !v ? "Required" : void 0
|
|
41
|
+
});
|
|
42
|
+
if (p.isCancel(name)) {
|
|
43
|
+
p.cancel("Operation cancelled.");
|
|
44
|
+
return null;
|
|
53
45
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
const server = await p.select({
|
|
47
|
+
message: "Server adapter:",
|
|
48
|
+
options: [
|
|
49
|
+
{ value: "express", label: "Express", hint: "recommended" },
|
|
50
|
+
{ value: "byo", label: "Bring Your Own" }
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
if (p.isCancel(server)) {
|
|
54
|
+
p.cancel("Operation cancelled.");
|
|
55
|
+
return null;
|
|
60
56
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
const validator = await p.select({
|
|
58
|
+
message: "Validation library:",
|
|
59
|
+
options: [
|
|
60
|
+
{ value: "zod", label: "Zod", hint: "recommended" },
|
|
61
|
+
{ value: "byo", label: "Bring Your Own" }
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
if (p.isCancel(validator)) {
|
|
65
|
+
p.cancel("Operation cancelled.");
|
|
66
|
+
return null;
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
const database = await p.select({
|
|
69
|
+
message: "Database adapter:",
|
|
70
|
+
options: [
|
|
71
|
+
{ value: "kysely", label: "Kysely", hint: "recommended" },
|
|
72
|
+
{ value: "byo", label: "Bring Your Own" }
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
if (p.isCancel(database)) {
|
|
76
|
+
p.cancel("Operation cancelled.");
|
|
77
|
+
return null;
|
|
78
78
|
}
|
|
79
|
-
return {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
return { name, server, validator, database };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/scaffold.ts
|
|
83
|
+
var fs = __toESM(require("fs"));
|
|
84
|
+
var path = __toESM(require("path"));
|
|
85
|
+
|
|
86
|
+
// src/templates/base.ts
|
|
87
|
+
function generatePackageJson(options) {
|
|
88
|
+
const dependencies = {
|
|
89
|
+
"@fossyl/core": "^0.9.0"
|
|
84
90
|
};
|
|
91
|
+
const devDependencies = {
|
|
92
|
+
"@types/node": "^22.0.0",
|
|
93
|
+
tsx: "^4.0.0",
|
|
94
|
+
typescript: "^5.8.0"
|
|
95
|
+
};
|
|
96
|
+
if (options.server === "express") {
|
|
97
|
+
dependencies["@fossyl/express"] = "^0.9.0";
|
|
98
|
+
dependencies["express"] = "^4.21.0";
|
|
99
|
+
devDependencies["@types/express"] = "^4.17.0";
|
|
100
|
+
}
|
|
101
|
+
if (options.validator === "zod") {
|
|
102
|
+
dependencies["@fossyl/zod"] = "^0.9.0";
|
|
103
|
+
dependencies["zod"] = "^3.24.0";
|
|
104
|
+
}
|
|
105
|
+
if (options.database === "kysely") {
|
|
106
|
+
dependencies["@fossyl/kysely"] = "^0.9.0";
|
|
107
|
+
dependencies["kysely"] = "^0.27.0";
|
|
108
|
+
dependencies["pg"] = "^8.13.0";
|
|
109
|
+
devDependencies["@types/pg"] = "^8.11.0";
|
|
110
|
+
}
|
|
111
|
+
const pkg = {
|
|
112
|
+
name: options.name === "." ? "my-fossyl-api" : options.name,
|
|
113
|
+
version: "0.1.0",
|
|
114
|
+
type: "module",
|
|
115
|
+
scripts: {
|
|
116
|
+
dev: "tsx watch src/index.ts",
|
|
117
|
+
build: "tsc",
|
|
118
|
+
start: "node dist/index.js"
|
|
119
|
+
},
|
|
120
|
+
dependencies,
|
|
121
|
+
devDependencies
|
|
122
|
+
};
|
|
123
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
124
|
+
}
|
|
125
|
+
function generateTsConfig() {
|
|
126
|
+
const config = {
|
|
127
|
+
compilerOptions: {
|
|
128
|
+
target: "ES2022",
|
|
129
|
+
module: "NodeNext",
|
|
130
|
+
moduleResolution: "NodeNext",
|
|
131
|
+
esModuleInterop: true,
|
|
132
|
+
strict: true,
|
|
133
|
+
skipLibCheck: true,
|
|
134
|
+
outDir: "./dist",
|
|
135
|
+
rootDir: "./src",
|
|
136
|
+
declaration: true
|
|
137
|
+
},
|
|
138
|
+
include: ["src/**/*"],
|
|
139
|
+
exclude: ["node_modules", "dist"]
|
|
140
|
+
};
|
|
141
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
142
|
+
}
|
|
143
|
+
function generateEnvExample(options) {
|
|
144
|
+
let content = `# Server
|
|
145
|
+
PORT=3000
|
|
146
|
+
`;
|
|
147
|
+
if (options.database === "kysely") {
|
|
148
|
+
content += `
|
|
149
|
+
# Database
|
|
150
|
+
DATABASE_URL=postgres://user:password@localhost:5432/mydb
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
return content;
|
|
154
|
+
}
|
|
155
|
+
function generateClaudeMd(options) {
|
|
156
|
+
const adapterDocs = [];
|
|
157
|
+
if (options.server === "express") {
|
|
158
|
+
adapterDocs.push("- `@fossyl/express` - Express.js runtime adapter");
|
|
159
|
+
}
|
|
160
|
+
if (options.validator === "zod") {
|
|
161
|
+
adapterDocs.push("- `@fossyl/zod` - Zod validation adapter");
|
|
162
|
+
}
|
|
163
|
+
if (options.database === "kysely") {
|
|
164
|
+
adapterDocs.push("- `@fossyl/kysely` - Kysely database adapter");
|
|
165
|
+
}
|
|
166
|
+
const byoNotes = [];
|
|
167
|
+
if (options.server === "byo") {
|
|
168
|
+
byoNotes.push(`
|
|
169
|
+
### Server (BYO)
|
|
170
|
+
You need to implement your own server adapter. See \`src/server.ts\` for the placeholder.
|
|
171
|
+
Check the @fossyl/express source for reference: https://github.com/YoyoSaur/fossyl/tree/main/packages/express`);
|
|
172
|
+
}
|
|
173
|
+
if (options.validator === "byo") {
|
|
174
|
+
byoNotes.push(`
|
|
175
|
+
### Validator (BYO)
|
|
176
|
+
You need to implement your own validators. See \`src/features/ping/validators/ping.validators.ts\` for the placeholder.
|
|
177
|
+
Check the @fossyl/zod source for reference: https://github.com/YoyoSaur/fossyl/tree/main/packages/zod`);
|
|
178
|
+
}
|
|
179
|
+
if (options.database === "byo") {
|
|
180
|
+
byoNotes.push(`
|
|
181
|
+
### Database (BYO)
|
|
182
|
+
You need to implement your own database layer. See \`src/db.ts\` for the placeholder.
|
|
183
|
+
Check the @fossyl/kysely source for reference: https://github.com/YoyoSaur/fossyl/tree/main/packages/kysely`);
|
|
184
|
+
}
|
|
185
|
+
return `# ${options.name} - AI Development Guide
|
|
186
|
+
|
|
187
|
+
**Fossyl REST API project**
|
|
188
|
+
|
|
189
|
+
## Project Structure
|
|
190
|
+
|
|
191
|
+
\`\`\`
|
|
192
|
+
src/
|
|
193
|
+
\u251C\u2500\u2500 features/
|
|
194
|
+
\u2502 \u2514\u2500\u2500 ping/
|
|
195
|
+
\u2502 \u251C\u2500\u2500 routes/ping.route.ts # Route definitions
|
|
196
|
+
\u2502 \u251C\u2500\u2500 services/ping.service.ts # Business logic
|
|
197
|
+
\u2502 \u251C\u2500\u2500 validators/ # Request validators
|
|
198
|
+
\u2502 \u2514\u2500\u2500 repo/ping.repo.ts # Database access
|
|
199
|
+
\u251C\u2500\u2500 migrations/ # Database migrations
|
|
200
|
+
\u251C\u2500\u2500 types/
|
|
201
|
+
\u2502 \u2514\u2500\u2500 db.ts # Database type definitions
|
|
202
|
+
\u251C\u2500\u2500 db.ts # Database setup
|
|
203
|
+
\u2514\u2500\u2500 index.ts # Main entry point
|
|
204
|
+
\`\`\`
|
|
205
|
+
|
|
206
|
+
## Adapters Used
|
|
207
|
+
|
|
208
|
+
${adapterDocs.join("\n")}
|
|
209
|
+
|
|
210
|
+
## Quick Start
|
|
211
|
+
|
|
212
|
+
\`\`\`bash
|
|
213
|
+
# Install dependencies
|
|
214
|
+
pnpm install
|
|
215
|
+
|
|
216
|
+
# Start development server
|
|
217
|
+
pnpm dev
|
|
218
|
+
\`\`\`
|
|
219
|
+
|
|
220
|
+
## Adding New Features
|
|
221
|
+
|
|
222
|
+
1. Create a new feature directory under \`src/features/\`
|
|
223
|
+
2. Add route definitions in \`routes/\`
|
|
224
|
+
3. Add business logic in \`services/\`
|
|
225
|
+
4. Add database access in \`repo/\`
|
|
226
|
+
5. Add validators in \`validators/\`
|
|
227
|
+
6. Register routes in \`src/index.ts\`
|
|
228
|
+
|
|
229
|
+
## Route Types
|
|
230
|
+
|
|
231
|
+
Fossyl provides four route types:
|
|
232
|
+
|
|
233
|
+
- **OpenRoute**: No authentication or body validation
|
|
234
|
+
- **AuthenticatedRoute**: Requires authentication, no body validation
|
|
235
|
+
- **ValidatedRoute**: Requires body validation, no authentication
|
|
236
|
+
- **FullRoute**: Requires both authentication and body validation
|
|
237
|
+
|
|
238
|
+
## Handler Parameter Order
|
|
239
|
+
|
|
240
|
+
- Routes with body: \`handler(params, [auth,] body)\`
|
|
241
|
+
- Routes without body: \`handler(params [, auth])\`
|
|
242
|
+
${byoNotes.join("\n")}
|
|
243
|
+
|
|
244
|
+
## Documentation
|
|
245
|
+
|
|
246
|
+
- Core: https://github.com/YoyoSaur/fossyl/tree/main/packages/core
|
|
247
|
+
- Express: https://github.com/YoyoSaur/fossyl/tree/main/packages/express
|
|
248
|
+
- Zod: https://github.com/YoyoSaur/fossyl/tree/main/packages/zod
|
|
249
|
+
- Kysely: https://github.com/YoyoSaur/fossyl/tree/main/packages/kysely
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/templates/server/express.ts
|
|
254
|
+
function generateExpressIndex(options) {
|
|
255
|
+
const imports = [
|
|
256
|
+
"import { createRouter, authWrapper } from '@fossyl/core';",
|
|
257
|
+
"import { expressAdapter } from '@fossyl/express';"
|
|
258
|
+
];
|
|
259
|
+
if (options.database === "kysely") {
|
|
260
|
+
imports.push("import { kyselyAdapter } from '@fossyl/kysely';");
|
|
261
|
+
imports.push("import { db } from './db';");
|
|
262
|
+
imports.push("import { migrations } from './migrations';");
|
|
263
|
+
}
|
|
264
|
+
imports.push("import { pingRoutes } from './features/ping/routes/ping.route';");
|
|
265
|
+
const adapterConfig = [];
|
|
266
|
+
if (options.database === "kysely") {
|
|
267
|
+
adapterConfig.push(`const database = kyselyAdapter({
|
|
268
|
+
client: db,
|
|
269
|
+
migrations,
|
|
270
|
+
autoMigrate: true,
|
|
271
|
+
});`);
|
|
272
|
+
}
|
|
273
|
+
const expressOptions = ["cors: true"];
|
|
274
|
+
if (options.database === "kysely") {
|
|
275
|
+
expressOptions.push("database");
|
|
276
|
+
}
|
|
277
|
+
return `${imports.join("\n")}
|
|
278
|
+
|
|
279
|
+
// Authentication function (customize based on your auth strategy)
|
|
280
|
+
export const authenticator = async (headers: Record<string, string>) => {
|
|
281
|
+
// TODO: Implement your authentication logic
|
|
282
|
+
// Example: JWT verification, OAuth validation, API key check, etc.
|
|
283
|
+
const userId = headers['x-user-id'];
|
|
284
|
+
if (!userId) {
|
|
285
|
+
throw new Error('Unauthorized');
|
|
286
|
+
}
|
|
287
|
+
return authWrapper({ userId });
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Create router with base path
|
|
291
|
+
const api = createRouter('/api');
|
|
292
|
+
|
|
293
|
+
${adapterConfig.join("\n\n")}
|
|
294
|
+
|
|
295
|
+
// Create Express adapter
|
|
296
|
+
const adapter = expressAdapter({
|
|
297
|
+
${expressOptions.join(",\n ")},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Register all routes
|
|
301
|
+
const routes = [...pingRoutes(api, authenticator)];
|
|
302
|
+
adapter.register(routes);
|
|
303
|
+
|
|
304
|
+
// Start server
|
|
305
|
+
const PORT = process.env.PORT ?? 3000;
|
|
306
|
+
adapter.listen(Number(PORT)).then(() => {
|
|
307
|
+
console.log(\`Server running on http://localhost:\${PORT}\`);
|
|
308
|
+
});
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
function generateByoServerIndex(options) {
|
|
312
|
+
const imports = [
|
|
313
|
+
"import { createRouter, authWrapper } from '@fossyl/core';",
|
|
314
|
+
"import { startServer } from './server';"
|
|
315
|
+
];
|
|
316
|
+
if (options.database === "kysely") {
|
|
317
|
+
imports.push("import { db } from './db';");
|
|
318
|
+
}
|
|
319
|
+
imports.push("import { pingRoutes } from './features/ping/routes/ping.route';");
|
|
320
|
+
return `${imports.join("\n")}
|
|
321
|
+
|
|
322
|
+
// Authentication function (customize based on your auth strategy)
|
|
323
|
+
export const authenticator = async (headers: Record<string, string>) => {
|
|
324
|
+
// TODO: Implement your authentication logic
|
|
325
|
+
const userId = headers['x-user-id'];
|
|
326
|
+
if (!userId) {
|
|
327
|
+
throw new Error('Unauthorized');
|
|
328
|
+
}
|
|
329
|
+
return authWrapper({ userId });
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Create router with base path
|
|
333
|
+
const api = createRouter('/api');
|
|
334
|
+
|
|
335
|
+
// Collect all routes
|
|
336
|
+
const routes = [...pingRoutes(api, authenticator)];
|
|
337
|
+
|
|
338
|
+
// Start server (implement in ./server.ts)
|
|
339
|
+
const PORT = process.env.PORT ?? 3000;
|
|
340
|
+
startServer(routes, Number(PORT));
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/templates/server/byo.ts
|
|
345
|
+
function generateByoServerPlaceholder() {
|
|
346
|
+
return `import type { Route } from '@fossyl/core';
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* TODO: Implement your server adapter
|
|
350
|
+
*
|
|
351
|
+
* This function should:
|
|
352
|
+
* 1. Create an HTTP server (Express, Fastify, Hono, etc.)
|
|
353
|
+
* 2. Register each route from the routes array
|
|
354
|
+
* 3. Handle request/response transformation
|
|
355
|
+
* 4. Implement error handling
|
|
356
|
+
*
|
|
357
|
+
* Reference implementation: https://github.com/YoyoSaur/fossyl/tree/main/packages/express
|
|
358
|
+
*
|
|
359
|
+
* Example with Express:
|
|
360
|
+
*
|
|
361
|
+
* import express from 'express';
|
|
362
|
+
*
|
|
363
|
+
* export function startServer(routes: Route[], port: number) {
|
|
364
|
+
* const app = express();
|
|
365
|
+
* app.use(express.json());
|
|
366
|
+
*
|
|
367
|
+
* for (const route of routes) {
|
|
368
|
+
* const method = route.method.toLowerCase();
|
|
369
|
+
* app[method](route.path, async (req, res) => {
|
|
370
|
+
* try {
|
|
371
|
+
* // Handle authentication if route.authenticator exists
|
|
372
|
+
* // Handle body validation if route.validator exists
|
|
373
|
+
* // Call route.handler with appropriate params
|
|
374
|
+
* const result = await route.handler(...);
|
|
375
|
+
* res.json({ success: 'true', type: result.typeName, data: result });
|
|
376
|
+
* } catch (error) {
|
|
377
|
+
* res.status(500).json({ success: 'false', error: { message: error.message } });
|
|
378
|
+
* }
|
|
379
|
+
* });
|
|
380
|
+
* }
|
|
381
|
+
*
|
|
382
|
+
* app.listen(port, () => console.log(\`Server running on port \${port}\`));
|
|
383
|
+
* }
|
|
384
|
+
*/
|
|
385
|
+
export function startServer(routes: Route[], port: number): void {
|
|
386
|
+
// TODO: Implement your server
|
|
387
|
+
console.log('TODO: Implement server adapter');
|
|
388
|
+
console.log(\`Routes to register: \${routes.length}\`);
|
|
389
|
+
console.log(\`Port: \${port}\`);
|
|
390
|
+
|
|
391
|
+
throw new Error('Server adapter not implemented. See src/server.ts for instructions.');
|
|
392
|
+
}
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/templates/database/kysely.ts
|
|
397
|
+
function generateKyselySetup() {
|
|
398
|
+
return `import { Kysely, PostgresDialect } from 'kysely';
|
|
399
|
+
import { Pool } from 'pg';
|
|
400
|
+
import type { DB } from './types/db';
|
|
401
|
+
|
|
402
|
+
const connectionString = process.env.DATABASE_URL;
|
|
403
|
+
|
|
404
|
+
if (!connectionString) {
|
|
405
|
+
throw new Error('DATABASE_URL environment variable is required');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export const db = new Kysely<DB>({
|
|
409
|
+
dialect: new PostgresDialect({
|
|
410
|
+
pool: new Pool({ connectionString }),
|
|
411
|
+
}),
|
|
412
|
+
});
|
|
413
|
+
`;
|
|
414
|
+
}
|
|
415
|
+
function generateDbTypes() {
|
|
416
|
+
return `import type { Generated, Insertable, Selectable, Updateable } from 'kysely';
|
|
417
|
+
|
|
418
|
+
// Ping table types
|
|
419
|
+
export interface PingTable {
|
|
420
|
+
id: Generated<string>;
|
|
421
|
+
message: string;
|
|
422
|
+
created_by: string;
|
|
423
|
+
created_at: Generated<Date>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export type Ping = Selectable<PingTable>;
|
|
427
|
+
export type NewPing = Insertable<PingTable>;
|
|
428
|
+
export type PingUpdate = Updateable<PingTable>;
|
|
429
|
+
|
|
430
|
+
// Database schema
|
|
431
|
+
export interface DB {
|
|
432
|
+
ping: PingTable;
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
function generateMigrationIndex() {
|
|
437
|
+
return `import { createMigrationProvider } from '@fossyl/kysely';
|
|
438
|
+
import { migration as m001 } from './001_create_ping';
|
|
439
|
+
|
|
440
|
+
export const migrations = createMigrationProvider({
|
|
441
|
+
'001_create_ping': m001,
|
|
442
|
+
});
|
|
443
|
+
`;
|
|
85
444
|
}
|
|
86
|
-
function
|
|
445
|
+
function generatePingMigration() {
|
|
446
|
+
return `import { sql } from 'kysely';
|
|
447
|
+
import { defineMigration } from '@fossyl/kysely';
|
|
448
|
+
|
|
449
|
+
export const migration = defineMigration({
|
|
450
|
+
async up(db) {
|
|
451
|
+
await db.schema
|
|
452
|
+
.createTable('ping')
|
|
453
|
+
.addColumn('id', 'uuid', (col) =>
|
|
454
|
+
col.primaryKey().defaultTo(sql\`gen_random_uuid()\`)
|
|
455
|
+
)
|
|
456
|
+
.addColumn('message', 'varchar(255)', (col) => col.notNull())
|
|
457
|
+
.addColumn('created_by', 'varchar(255)', (col) => col.notNull())
|
|
458
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
459
|
+
col.notNull().defaultTo(sql\`now()\`)
|
|
460
|
+
)
|
|
461
|
+
.execute();
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
async down(db) {
|
|
465
|
+
await db.schema.dropTable('ping').execute();
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/templates/database/byo.ts
|
|
472
|
+
function generateByoDatabasePlaceholder() {
|
|
473
|
+
return `/**
|
|
474
|
+
* TODO: Implement your database adapter
|
|
475
|
+
*
|
|
476
|
+
* This file should:
|
|
477
|
+
* 1. Set up your database connection (Prisma, Drizzle, raw SQL, etc.)
|
|
478
|
+
* 2. Export a database client or query builder
|
|
479
|
+
* 3. Optionally implement transaction support
|
|
480
|
+
*
|
|
481
|
+
* Reference implementation: https://github.com/YoyoSaur/fossyl/tree/main/packages/kysely
|
|
482
|
+
*
|
|
483
|
+
* Example with Prisma:
|
|
484
|
+
*
|
|
485
|
+
* import { PrismaClient } from '@prisma/client';
|
|
486
|
+
* export const db = new PrismaClient();
|
|
487
|
+
*
|
|
488
|
+
* Example with Drizzle:
|
|
489
|
+
*
|
|
490
|
+
* import { drizzle } from 'drizzle-orm/postgres-js';
|
|
491
|
+
* import postgres from 'postgres';
|
|
492
|
+
* const queryClient = postgres(process.env.DATABASE_URL!);
|
|
493
|
+
* export const db = drizzle(queryClient);
|
|
494
|
+
*/
|
|
495
|
+
|
|
496
|
+
// Placeholder - replace with your database client
|
|
497
|
+
export const db = {
|
|
498
|
+
// Add your database client here
|
|
499
|
+
query: async (sql: string, params?: unknown[]) => {
|
|
500
|
+
throw new Error('Database not implemented. See src/db.ts for instructions.');
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/templates/validator/zod.ts
|
|
507
|
+
function generateZodValidators() {
|
|
508
|
+
return `import { z } from 'zod';
|
|
509
|
+
import { zodValidator, zodQueryValidator } from '@fossyl/zod';
|
|
510
|
+
|
|
511
|
+
// Create ping body schema
|
|
512
|
+
export const createPingSchema = z.object({
|
|
513
|
+
message: z.string().min(1).max(255),
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
export const createPingValidator = zodValidator(createPingSchema);
|
|
517
|
+
export type CreatePingBody = z.infer<typeof createPingSchema>;
|
|
518
|
+
|
|
519
|
+
// Update ping body schema
|
|
520
|
+
export const updatePingSchema = z.object({
|
|
521
|
+
message: z.string().min(1).max(255).optional(),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
export const updatePingValidator = zodValidator(updatePingSchema);
|
|
525
|
+
export type UpdatePingBody = z.infer<typeof updatePingSchema>;
|
|
526
|
+
|
|
527
|
+
// List ping query schema
|
|
528
|
+
export const listPingQuerySchema = z.object({
|
|
529
|
+
limit: z.coerce.number().min(1).max(100).default(10),
|
|
530
|
+
offset: z.coerce.number().min(0).default(0),
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
export const listPingQueryValidator = zodQueryValidator(listPingQuerySchema);
|
|
534
|
+
export type ListPingQuery = z.infer<typeof listPingQuerySchema>;
|
|
535
|
+
`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/templates/validator/byo.ts
|
|
539
|
+
function generateByoValidatorPlaceholder() {
|
|
540
|
+
return `/**
|
|
541
|
+
* TODO: Implement your validators
|
|
542
|
+
*
|
|
543
|
+
* Validators should:
|
|
544
|
+
* 1. Accept unknown data
|
|
545
|
+
* 2. Validate and parse the data
|
|
546
|
+
* 3. Return the typed result or throw an error
|
|
547
|
+
*
|
|
548
|
+
* Reference implementation: https://github.com/YoyoSaur/fossyl/tree/main/packages/zod
|
|
549
|
+
*
|
|
550
|
+
* Example with Yup:
|
|
551
|
+
*
|
|
552
|
+
* import * as yup from 'yup';
|
|
553
|
+
*
|
|
554
|
+
* const createPingSchema = yup.object({
|
|
555
|
+
* message: yup.string().required().max(255),
|
|
556
|
+
* });
|
|
557
|
+
*
|
|
558
|
+
* export const createPingValidator = (data: unknown) => {
|
|
559
|
+
* return createPingSchema.validateSync(data);
|
|
560
|
+
* };
|
|
561
|
+
*
|
|
562
|
+
* Example with manual validation:
|
|
563
|
+
*
|
|
564
|
+
* export const createPingValidator = (data: unknown): CreatePingBody => {
|
|
565
|
+
* if (typeof data !== 'object' || data === null) {
|
|
566
|
+
* throw new Error('Invalid request body');
|
|
567
|
+
* }
|
|
568
|
+
* const { message } = data as Record<string, unknown>;
|
|
569
|
+
* if (typeof message !== 'string' || message.length === 0 || message.length > 255) {
|
|
570
|
+
* throw new Error('Invalid message');
|
|
571
|
+
* }
|
|
572
|
+
* return { message };
|
|
573
|
+
* };
|
|
574
|
+
*/
|
|
575
|
+
|
|
576
|
+
// Type definitions
|
|
577
|
+
export interface CreatePingBody {
|
|
578
|
+
message: string;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export interface UpdatePingBody {
|
|
582
|
+
message?: string;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export interface ListPingQuery {
|
|
586
|
+
limit: number;
|
|
587
|
+
offset: number;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Validators - TODO: Implement actual validation
|
|
591
|
+
export const createPingValidator = (data: unknown): CreatePingBody => {
|
|
592
|
+
// TODO: Add validation logic
|
|
593
|
+
return data as CreatePingBody;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
export const updatePingValidator = (data: unknown): UpdatePingBody => {
|
|
597
|
+
// TODO: Add validation logic
|
|
598
|
+
return data as UpdatePingBody;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
export const listPingQueryValidator = (data: unknown): ListPingQuery => {
|
|
602
|
+
// TODO: Add validation logic
|
|
603
|
+
const parsed = data as Record<string, unknown>;
|
|
87
604
|
return {
|
|
88
|
-
|
|
89
|
-
|
|
605
|
+
limit: Number(parsed.limit) || 10,
|
|
606
|
+
offset: Number(parsed.offset) || 0,
|
|
90
607
|
};
|
|
608
|
+
};
|
|
609
|
+
`;
|
|
91
610
|
}
|
|
92
611
|
|
|
93
|
-
// src/
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
612
|
+
// src/templates/feature/ping.ts
|
|
613
|
+
function generatePingRoute(options) {
|
|
614
|
+
const validatorImport = options.validator === "zod" ? `import {
|
|
615
|
+
createPingValidator,
|
|
616
|
+
updatePingValidator,
|
|
617
|
+
listPingQueryValidator,
|
|
618
|
+
} from '../validators/ping.validators';` : `import {
|
|
619
|
+
createPingValidator,
|
|
620
|
+
updatePingValidator,
|
|
621
|
+
listPingQueryValidator,
|
|
622
|
+
} from '../validators/ping.validators';`;
|
|
623
|
+
return `import type { Router, AuthenticationFunction } from '@fossyl/core';
|
|
624
|
+
import * as pingService from '../services/ping.service';
|
|
625
|
+
${validatorImport}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Ping feature routes demonstrating all 4 route types:
|
|
629
|
+
* - OpenRoute: GET /api/ping (list all)
|
|
630
|
+
* - OpenRoute: GET /api/ping/:id (get one)
|
|
631
|
+
* - FullRoute: POST /api/ping (authenticated + validated)
|
|
632
|
+
* - FullRoute: PUT /api/ping/:id (authenticated + validated)
|
|
633
|
+
* - AuthenticatedRoute: DELETE /api/ping/:id (authenticated only)
|
|
634
|
+
*/
|
|
635
|
+
export function pingRoutes<T extends { userId: string }>(
|
|
636
|
+
router: Router,
|
|
637
|
+
authenticator: AuthenticationFunction<T>
|
|
638
|
+
) {
|
|
639
|
+
// OpenRoute - List all pings (public)
|
|
640
|
+
const listPings = router.createEndpoint('/ping').get({
|
|
641
|
+
queryValidator: listPingQueryValidator,
|
|
642
|
+
handler: async ({ query }) => {
|
|
643
|
+
const pings = await pingService.listPings(query.limit, query.offset);
|
|
644
|
+
return {
|
|
645
|
+
typeName: 'PingList' as const,
|
|
646
|
+
pings,
|
|
647
|
+
limit: query.limit,
|
|
648
|
+
offset: query.offset,
|
|
649
|
+
};
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// OpenRoute - Get single ping (public)
|
|
654
|
+
const getPing = router.createEndpoint('/ping/:id').get({
|
|
655
|
+
handler: async ({ url }) => {
|
|
656
|
+
const ping = await pingService.getPing(url.id);
|
|
657
|
+
return {
|
|
658
|
+
typeName: 'Ping' as const,
|
|
659
|
+
...ping,
|
|
660
|
+
};
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// FullRoute - Create ping (authenticated + validated)
|
|
665
|
+
const createPing = router.createEndpoint('/ping').post({
|
|
666
|
+
authenticator,
|
|
667
|
+
validator: createPingValidator,
|
|
668
|
+
handler: async ({ url }, auth, body) => {
|
|
669
|
+
const ping = await pingService.createPing(body.message, auth.userId);
|
|
670
|
+
return {
|
|
671
|
+
typeName: 'Ping' as const,
|
|
672
|
+
...ping,
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// FullRoute - Update ping (authenticated + validated)
|
|
678
|
+
const updatePing = router.createEndpoint('/ping/:id').put({
|
|
679
|
+
authenticator,
|
|
680
|
+
validator: updatePingValidator,
|
|
681
|
+
handler: async ({ url }, auth, body) => {
|
|
682
|
+
const ping = await pingService.updatePing(url.id, body, auth.userId);
|
|
683
|
+
return {
|
|
684
|
+
typeName: 'Ping' as const,
|
|
685
|
+
...ping,
|
|
686
|
+
};
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// AuthenticatedRoute - Delete ping (authenticated only, no body)
|
|
691
|
+
const deletePing = router.createEndpoint('/ping/:id').delete({
|
|
692
|
+
authenticator,
|
|
693
|
+
handler: async ({ url }, auth) => {
|
|
694
|
+
await pingService.deletePing(url.id, auth.userId);
|
|
695
|
+
return {
|
|
696
|
+
typeName: 'DeleteResult' as const,
|
|
697
|
+
id: url.id,
|
|
698
|
+
deleted: true,
|
|
699
|
+
};
|
|
700
|
+
},
|
|
97
701
|
});
|
|
702
|
+
|
|
703
|
+
return [listPings, getPing, createPing, updatePing, deletePing];
|
|
704
|
+
}
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
function generatePingService(_options) {
|
|
708
|
+
return `import * as pingRepo from '../repo/ping.repo';
|
|
709
|
+
|
|
710
|
+
export interface PingData {
|
|
711
|
+
id: string;
|
|
712
|
+
message: string;
|
|
713
|
+
created_by: string;
|
|
714
|
+
created_at: Date;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export async function listPings(limit: number, offset: number): Promise<PingData[]> {
|
|
718
|
+
return pingRepo.findAll(limit, offset);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export async function getPing(id: string): Promise<PingData> {
|
|
722
|
+
const ping = await pingRepo.findById(id);
|
|
723
|
+
if (!ping) {
|
|
724
|
+
throw new Error('Ping not found');
|
|
725
|
+
}
|
|
726
|
+
return ping;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export async function createPing(message: string, userId: string): Promise<PingData> {
|
|
730
|
+
return pingRepo.create({ message, created_by: userId });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export async function updatePing(
|
|
734
|
+
id: string,
|
|
735
|
+
data: { message?: string },
|
|
736
|
+
userId: string
|
|
737
|
+
): Promise<PingData> {
|
|
738
|
+
const existing = await pingRepo.findById(id);
|
|
739
|
+
if (!existing) {
|
|
740
|
+
throw new Error('Ping not found');
|
|
741
|
+
}
|
|
742
|
+
// Optional: Check if user owns the ping
|
|
743
|
+
// if (existing.created_by !== userId) {
|
|
744
|
+
// throw new Error('Not authorized');
|
|
745
|
+
// }
|
|
746
|
+
return pingRepo.update(id, data);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export async function deletePing(id: string, userId: string): Promise<void> {
|
|
750
|
+
const existing = await pingRepo.findById(id);
|
|
751
|
+
if (!existing) {
|
|
752
|
+
throw new Error('Ping not found');
|
|
753
|
+
}
|
|
754
|
+
// Optional: Check if user owns the ping
|
|
755
|
+
// if (existing.created_by !== userId) {
|
|
756
|
+
// throw new Error('Not authorized');
|
|
757
|
+
// }
|
|
758
|
+
await pingRepo.remove(id);
|
|
759
|
+
}
|
|
760
|
+
`;
|
|
761
|
+
}
|
|
762
|
+
function generatePingRepo(options) {
|
|
763
|
+
if (options.database === "kysely") {
|
|
764
|
+
return generateKyselyPingRepo();
|
|
765
|
+
}
|
|
766
|
+
return generateByoPingRepo();
|
|
767
|
+
}
|
|
768
|
+
function generateKyselyPingRepo() {
|
|
769
|
+
return `import { getTransaction } from '@fossyl/kysely';
|
|
770
|
+
import type { DB, Ping, NewPing, PingUpdate } from '../../../types/db';
|
|
771
|
+
|
|
772
|
+
export async function findAll(limit: number, offset: number): Promise<Ping[]> {
|
|
773
|
+
const db = getTransaction<DB>();
|
|
774
|
+
return db
|
|
775
|
+
.selectFrom('ping')
|
|
776
|
+
.selectAll()
|
|
777
|
+
.orderBy('created_at', 'desc')
|
|
778
|
+
.limit(limit)
|
|
779
|
+
.offset(offset)
|
|
780
|
+
.execute();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export async function findById(id: string): Promise<Ping | undefined> {
|
|
784
|
+
const db = getTransaction<DB>();
|
|
785
|
+
return db
|
|
786
|
+
.selectFrom('ping')
|
|
787
|
+
.where('id', '=', id)
|
|
788
|
+
.selectAll()
|
|
789
|
+
.executeTakeFirst();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export async function create(data: Omit<NewPing, 'id' | 'created_at'>): Promise<Ping> {
|
|
793
|
+
const db = getTransaction<DB>();
|
|
794
|
+
return db
|
|
795
|
+
.insertInto('ping')
|
|
796
|
+
.values(data)
|
|
797
|
+
.returningAll()
|
|
798
|
+
.executeTakeFirstOrThrow();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export async function update(id: string, data: PingUpdate): Promise<Ping> {
|
|
802
|
+
const db = getTransaction<DB>();
|
|
803
|
+
return db
|
|
804
|
+
.updateTable('ping')
|
|
805
|
+
.set(data)
|
|
806
|
+
.where('id', '=', id)
|
|
807
|
+
.returningAll()
|
|
808
|
+
.executeTakeFirstOrThrow();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export async function remove(id: string): Promise<void> {
|
|
812
|
+
const db = getTransaction<DB>();
|
|
813
|
+
await db.deleteFrom('ping').where('id', '=', id).execute();
|
|
814
|
+
}
|
|
815
|
+
`;
|
|
816
|
+
}
|
|
817
|
+
function generateByoPingRepo() {
|
|
818
|
+
return `/**
|
|
819
|
+
* TODO: Implement database operations
|
|
820
|
+
*
|
|
821
|
+
* This file contains placeholder implementations.
|
|
822
|
+
* Replace with your actual database queries using your chosen database client.
|
|
823
|
+
*/
|
|
824
|
+
|
|
825
|
+
export interface Ping {
|
|
826
|
+
id: string;
|
|
827
|
+
message: string;
|
|
828
|
+
created_by: string;
|
|
829
|
+
created_at: Date;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// In-memory store for demo purposes - replace with actual database
|
|
833
|
+
const pings: Map<string, Ping> = new Map();
|
|
834
|
+
|
|
835
|
+
export async function findAll(limit: number, offset: number): Promise<Ping[]> {
|
|
836
|
+
// TODO: Replace with actual database query
|
|
837
|
+
const all = Array.from(pings.values());
|
|
838
|
+
return all.slice(offset, offset + limit);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export async function findById(id: string): Promise<Ping | undefined> {
|
|
842
|
+
// TODO: Replace with actual database query
|
|
843
|
+
return pings.get(id);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export async function create(data: { message: string; created_by: string }): Promise<Ping> {
|
|
847
|
+
// TODO: Replace with actual database insert
|
|
848
|
+
const ping: Ping = {
|
|
849
|
+
id: crypto.randomUUID(),
|
|
850
|
+
message: data.message,
|
|
851
|
+
created_by: data.created_by,
|
|
852
|
+
created_at: new Date(),
|
|
853
|
+
};
|
|
854
|
+
pings.set(ping.id, ping);
|
|
855
|
+
return ping;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export async function update(id: string, data: { message?: string }): Promise<Ping> {
|
|
859
|
+
// TODO: Replace with actual database update
|
|
860
|
+
const existing = pings.get(id);
|
|
861
|
+
if (!existing) {
|
|
862
|
+
throw new Error('Not found');
|
|
863
|
+
}
|
|
864
|
+
const updated = { ...existing, ...data };
|
|
865
|
+
pings.set(id, updated);
|
|
866
|
+
return updated;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
export async function remove(id: string): Promise<void> {
|
|
870
|
+
// TODO: Replace with actual database delete
|
|
871
|
+
pings.delete(id);
|
|
872
|
+
}
|
|
873
|
+
`;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/scaffold.ts
|
|
877
|
+
function generateFiles(options) {
|
|
878
|
+
const files = [];
|
|
879
|
+
files.push({
|
|
880
|
+
path: "package.json",
|
|
881
|
+
content: generatePackageJson(options)
|
|
882
|
+
});
|
|
883
|
+
files.push({
|
|
884
|
+
path: "tsconfig.json",
|
|
885
|
+
content: generateTsConfig()
|
|
886
|
+
});
|
|
887
|
+
files.push({
|
|
888
|
+
path: ".env.example",
|
|
889
|
+
content: generateEnvExample(options)
|
|
890
|
+
});
|
|
891
|
+
files.push({
|
|
892
|
+
path: "CLAUDE.md",
|
|
893
|
+
content: generateClaudeMd(options)
|
|
894
|
+
});
|
|
895
|
+
if (options.server === "express") {
|
|
896
|
+
files.push({
|
|
897
|
+
path: "src/index.ts",
|
|
898
|
+
content: generateExpressIndex(options)
|
|
899
|
+
});
|
|
900
|
+
} else {
|
|
901
|
+
files.push({
|
|
902
|
+
path: "src/index.ts",
|
|
903
|
+
content: generateByoServerIndex(options)
|
|
904
|
+
});
|
|
905
|
+
files.push({
|
|
906
|
+
path: "src/server.ts",
|
|
907
|
+
content: generateByoServerPlaceholder()
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
if (options.database === "kysely") {
|
|
911
|
+
files.push({
|
|
912
|
+
path: "src/db.ts",
|
|
913
|
+
content: generateKyselySetup()
|
|
914
|
+
});
|
|
915
|
+
files.push({
|
|
916
|
+
path: "src/types/db.ts",
|
|
917
|
+
content: generateDbTypes()
|
|
918
|
+
});
|
|
919
|
+
files.push({
|
|
920
|
+
path: "src/migrations/index.ts",
|
|
921
|
+
content: generateMigrationIndex()
|
|
922
|
+
});
|
|
923
|
+
files.push({
|
|
924
|
+
path: "src/migrations/001_create_ping.ts",
|
|
925
|
+
content: generatePingMigration()
|
|
926
|
+
});
|
|
927
|
+
} else {
|
|
928
|
+
files.push({
|
|
929
|
+
path: "src/db.ts",
|
|
930
|
+
content: generateByoDatabasePlaceholder()
|
|
931
|
+
});
|
|
932
|
+
files.push({
|
|
933
|
+
path: "src/types/db.ts",
|
|
934
|
+
content: "// TODO: Define your database types here\nexport interface DB {}\n"
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (options.validator === "zod") {
|
|
938
|
+
files.push({
|
|
939
|
+
path: "src/features/ping/validators/ping.validators.ts",
|
|
940
|
+
content: generateZodValidators()
|
|
941
|
+
});
|
|
942
|
+
} else {
|
|
943
|
+
files.push({
|
|
944
|
+
path: "src/features/ping/validators/ping.validators.ts",
|
|
945
|
+
content: generateByoValidatorPlaceholder()
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
files.push({
|
|
949
|
+
path: "src/features/ping/routes/ping.route.ts",
|
|
950
|
+
content: generatePingRoute(options)
|
|
951
|
+
});
|
|
952
|
+
files.push({
|
|
953
|
+
path: "src/features/ping/services/ping.service.ts",
|
|
954
|
+
content: generatePingService(options)
|
|
955
|
+
});
|
|
956
|
+
files.push({
|
|
957
|
+
path: "src/features/ping/repo/ping.repo.ts",
|
|
958
|
+
content: generatePingRepo(options)
|
|
959
|
+
});
|
|
960
|
+
return files;
|
|
961
|
+
}
|
|
962
|
+
function writeFiles(projectPath, files) {
|
|
963
|
+
for (const file of files) {
|
|
964
|
+
const fullPath = path.join(projectPath, file.path);
|
|
965
|
+
const dir = path.dirname(fullPath);
|
|
966
|
+
if (!fs.existsSync(dir)) {
|
|
967
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
968
|
+
}
|
|
969
|
+
fs.writeFileSync(fullPath, file.content, "utf-8");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/commands/create.ts
|
|
974
|
+
async function createCommand(projectName) {
|
|
975
|
+
const options = await promptForOptions(projectName);
|
|
976
|
+
if (!options) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const projectPath = options.name === "." ? process.cwd() : path2.resolve(process.cwd(), options.name);
|
|
980
|
+
if (options.name !== ".") {
|
|
981
|
+
if (fs2.existsSync(projectPath)) {
|
|
982
|
+
const files = fs2.readdirSync(projectPath);
|
|
983
|
+
if (files.length > 0) {
|
|
984
|
+
p2.cancel(`Directory "${options.name}" already exists and is not empty.`);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const s = p2.spinner();
|
|
990
|
+
s.start("Creating project files...");
|
|
991
|
+
try {
|
|
992
|
+
const files = generateFiles(options);
|
|
993
|
+
writeFiles(projectPath, files);
|
|
994
|
+
s.stop("Project files created!");
|
|
995
|
+
p2.note(
|
|
996
|
+
`cd ${options.name === "." ? "." : options.name}
|
|
997
|
+
pnpm install
|
|
998
|
+
pnpm dev`,
|
|
999
|
+
"Next steps"
|
|
1000
|
+
);
|
|
1001
|
+
const adapters = [];
|
|
1002
|
+
if (options.server === "express") adapters.push("@fossyl/express");
|
|
1003
|
+
if (options.validator === "zod") adapters.push("@fossyl/zod");
|
|
1004
|
+
if (options.database === "kysely") adapters.push("@fossyl/kysely");
|
|
1005
|
+
if (adapters.length > 0) {
|
|
1006
|
+
p2.log.info(`Using fossyl adapters: ${adapters.join(", ")}`);
|
|
1007
|
+
}
|
|
1008
|
+
if (options.server === "byo" || options.validator === "byo" || options.database === "byo") {
|
|
1009
|
+
p2.log.warn("Check TODO comments in generated files for BYO setup instructions.");
|
|
1010
|
+
}
|
|
1011
|
+
p2.outro("Happy coding!");
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
s.stop("Failed to create project files.");
|
|
1014
|
+
throw error;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/index.ts
|
|
1019
|
+
var { values, positionals } = (0, import_node_util.parseArgs)({
|
|
1020
|
+
options: {
|
|
1021
|
+
create: { type: "boolean" },
|
|
1022
|
+
help: { type: "boolean", short: "h" },
|
|
1023
|
+
version: { type: "boolean", short: "v" }
|
|
1024
|
+
},
|
|
1025
|
+
allowPositionals: true
|
|
1026
|
+
});
|
|
1027
|
+
function showHelp() {
|
|
1028
|
+
console.log(`
|
|
1029
|
+
fossyl - CLI for scaffolding fossyl projects
|
|
1030
|
+
|
|
1031
|
+
Usage:
|
|
1032
|
+
npx fossyl --create <project-name> Create a new fossyl project
|
|
1033
|
+
npx fossyl --help Show this help message
|
|
1034
|
+
npx fossyl --version Show version
|
|
1035
|
+
|
|
1036
|
+
Examples:
|
|
1037
|
+
npx fossyl --create my-api Create a new project named "my-api"
|
|
1038
|
+
npx fossyl --create . Create a new project in the current directory
|
|
1039
|
+
`);
|
|
1040
|
+
}
|
|
1041
|
+
function showVersion() {
|
|
1042
|
+
console.log("fossyl v0.9.0");
|
|
1043
|
+
}
|
|
1044
|
+
async function main() {
|
|
1045
|
+
if (values.version) {
|
|
1046
|
+
showVersion();
|
|
1047
|
+
} else if (values.create) {
|
|
1048
|
+
await createCommand(positionals[0]);
|
|
1049
|
+
} else if (values.help) {
|
|
1050
|
+
showHelp();
|
|
1051
|
+
} else {
|
|
1052
|
+
showHelp();
|
|
1053
|
+
}
|
|
98
1054
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
createRouter
|
|
1055
|
+
main().catch((error) => {
|
|
1056
|
+
console.error("Error:", error.message);
|
|
1057
|
+
process.exit(1);
|
|
103
1058
|
});
|