fossyl 0.1.5 → 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.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 __getOwnPropSymbols = Object.getOwnPropertySymbols;
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
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 index_exports = {};
39
- __export(index_exports, {
40
- authWrapper: () => authWrapper,
41
- createRouter: () => createRouter
42
- });
43
- module.exports = __toCommonJS(index_exports);
44
-
45
- // src/router/router.ts
46
- function createEndpoint(path) {
47
- function get(config) {
48
- if ("authenticator" in config) {
49
- return __spreadProps(__spreadValues({}, config), { type: "authenticated", path, method: "GET" });
50
- } else {
51
- return __spreadProps(__spreadValues({}, config), { type: "open", path, method: "GET" });
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
- function put(config) {
55
- if ("authenticator" in config) {
56
- return __spreadProps(__spreadValues({}, config), { type: "full", path, method: "PUT" });
57
- } else {
58
- return __spreadProps(__spreadValues({}, config), { type: "validated", path, method: "PUT" });
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
- function post(config) {
62
- if ("authenticator" in config) {
63
- return __spreadProps(__spreadValues({}, config), { type: "full", path, method: "POST" });
64
- } else {
65
- return __spreadProps(__spreadValues({}, config), { type: "validated", path, method: "POST" });
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
- function del(config) {
69
- if ("authenticator" in config) {
70
- return __spreadProps(__spreadValues({}, config), {
71
- type: "authenticated",
72
- path,
73
- method: "DELETE"
74
- });
75
- } else {
76
- return __spreadProps(__spreadValues({}, config), { type: "open", path, method: "DELETE" });
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
- get,
81
- post,
82
- put,
83
- delete: del
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 createRouter(_) {
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
- createEndpoint: (path) => createEndpoint(path),
89
- createSubrouter: (path) => createRouter(path)
605
+ limit: Number(parsed.limit) || 10,
606
+ offset: Number(parsed.offset) || 0,
90
607
  };
608
+ };
609
+ `;
91
610
  }
92
611
 
93
- // src/router/types/routes.types.ts
94
- function authWrapper(auth) {
95
- return __spreadProps(__spreadValues({}, auth), {
96
- [authBrand]: "Auth"
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
- // Annotate the CommonJS export names for ESM import in node:
100
- 0 && (module.exports = {
101
- authWrapper,
102
- createRouter
1055
+ main().catch((error) => {
1056
+ console.error("Error:", error.message);
1057
+ process.exit(1);
103
1058
  });