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