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