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