fossyl 0.9.0 → 0.11.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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # fossyl
2
+
3
+ CLI for scaffolding fossyl projects.
4
+
5
+ For AI-assisted development, see [CLAUDE.md](./CLAUDE.md). Full documentation at [github.com/YoyoSaur/fossyl](https://github.com/YoyoSaur/fossyl).
package/dist/index.js CHANGED
@@ -76,7 +76,30 @@ async function promptForOptions(projectName) {
76
76
  p.cancel("Operation cancelled.");
77
77
  return null;
78
78
  }
79
- return { name, server, validator, database };
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;
92
+ }
93
+ }
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 };
80
103
  }
81
104
 
82
105
  // src/scaffold.ts
@@ -105,8 +128,15 @@ function generatePackageJson(options) {
105
128
  if (options.database === "kysely") {
106
129
  dependencies["@fossyl/kysely"] = "^0.9.0";
107
130
  dependencies["kysely"] = "^0.27.0";
108
- dependencies["pg"] = "^8.13.0";
109
- devDependencies["@types/pg"] = "^8.11.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";
136
+ } else {
137
+ dependencies["pg"] = "^8.13.0";
138
+ devDependencies["@types/pg"] = "^8.11.0";
139
+ }
110
140
  }
111
141
  const pkg = {
112
142
  name: options.name === "." ? "my-fossyl-api" : options.name,
@@ -147,8 +177,17 @@ PORT=3000
147
177
  if (options.database === "kysely") {
148
178
  content += `
149
179
  # Database
150
- DATABASE_URL=postgres://user:password@localhost:5432/mydb
151
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
+ `;
187
+ } else {
188
+ content += `DATABASE_URL=postgres://user:password@localhost:5432/mydb
189
+ `;
190
+ }
152
191
  }
153
192
  return content;
154
193
  }
@@ -394,7 +433,39 @@ export function startServer(routes: Route[], port: number): void {
394
433
  }
395
434
 
396
435
  // src/templates/database/kysely.ts
397
- function generateKyselySetup() {
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
+ }
398
469
  return `import { Kysely, PostgresDialect } from 'kysely';
399
470
  import { Pool } from 'pg';
400
471
  import type { DB } from './types/db';
@@ -417,7 +488,7 @@ function generateDbTypes() {
417
488
 
418
489
  // Ping table types
419
490
  export interface PingTable {
420
- id: Generated<string>;
491
+ id: Generated<number>;
421
492
  message: string;
422
493
  created_by: string;
423
494
  created_at: Generated<Date>;
@@ -442,17 +513,61 @@ export const migrations = createMigrationProvider({
442
513
  });
443
514
  `;
444
515
  }
445
- function generatePingMigration() {
446
- return `import { sql } from 'kysely';
516
+ function generatePingMigration(dialect = "postgres") {
517
+ if (dialect === "sqlite") {
518
+ return `import { sql } from 'kysely';
447
519
  import { defineMigration } from '@fossyl/kysely';
448
520
 
449
521
  export const migration = defineMigration({
450
522
  async up(db) {
451
523
  await db.schema
452
524
  .createTable('ping')
453
- .addColumn('id', 'uuid', (col) =>
454
- col.primaryKey().defaultTo(sql\`gen_random_uuid()\`)
525
+ .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
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'))\`)
455
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', 'integer', (col) => col.primaryKey().autoIncrement())
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', 'serial', (col) => col.primaryKey())
456
571
  .addColumn('message', 'varchar(255)', (col) => col.notNull())
457
572
  .addColumn('created_by', 'varchar(255)', (col) => col.notNull())
458
573
  .addColumn('created_at', 'timestamp', (col) =>
@@ -708,7 +823,7 @@ function generatePingService(_options) {
708
823
  return `import * as pingRepo from '../repo/ping.repo';
709
824
 
710
825
  export interface PingData {
711
- id: string;
826
+ id: number;
712
827
  message: string;
713
828
  created_by: string;
714
829
  created_at: Date;
@@ -784,7 +899,7 @@ export async function findById(id: string): Promise<Ping | undefined> {
784
899
  const db = getTransaction<DB>();
785
900
  return db
786
901
  .selectFrom('ping')
787
- .where('id', '=', id)
902
+ .where('id', '=', Number(id))
788
903
  .selectAll()
789
904
  .executeTakeFirst();
790
905
  }
@@ -803,14 +918,14 @@ export async function update(id: string, data: PingUpdate): Promise<Ping> {
803
918
  return db
804
919
  .updateTable('ping')
805
920
  .set(data)
806
- .where('id', '=', id)
921
+ .where('id', '=', Number(id))
807
922
  .returningAll()
808
923
  .executeTakeFirstOrThrow();
809
924
  }
810
925
 
811
926
  export async function remove(id: string): Promise<void> {
812
927
  const db = getTransaction<DB>();
813
- await db.deleteFrom('ping').where('id', '=', id).execute();
928
+ await db.deleteFrom('ping').where('id', '=', Number(id)).execute();
814
929
  }
815
930
  `;
816
931
  }
@@ -823,14 +938,15 @@ function generateByoPingRepo() {
823
938
  */
824
939
 
825
940
  export interface Ping {
826
- id: string;
941
+ id: number;
827
942
  message: string;
828
943
  created_by: string;
829
944
  created_at: Date;
830
945
  }
831
946
 
832
947
  // In-memory store for demo purposes - replace with actual database
833
- const pings: Map<string, Ping> = new Map();
948
+ const pings: Map<number, Ping> = new Map();
949
+ let nextId = 1;
834
950
 
835
951
  export async function findAll(limit: number, offset: number): Promise<Ping[]> {
836
952
  // TODO: Replace with actual database query
@@ -840,13 +956,13 @@ export async function findAll(limit: number, offset: number): Promise<Ping[]> {
840
956
 
841
957
  export async function findById(id: string): Promise<Ping | undefined> {
842
958
  // TODO: Replace with actual database query
843
- return pings.get(id);
959
+ return pings.get(Number(id));
844
960
  }
845
961
 
846
962
  export async function create(data: { message: string; created_by: string }): Promise<Ping> {
847
963
  // TODO: Replace with actual database insert
848
964
  const ping: Ping = {
849
- id: crypto.randomUUID(),
965
+ id: nextId++,
850
966
  message: data.message,
851
967
  created_by: data.created_by,
852
968
  created_at: new Date(),
@@ -857,22 +973,115 @@ export async function create(data: { message: string; created_by: string }): Pro
857
973
 
858
974
  export async function update(id: string, data: { message?: string }): Promise<Ping> {
859
975
  // TODO: Replace with actual database update
860
- const existing = pings.get(id);
976
+ const numId = Number(id);
977
+ const existing = pings.get(numId);
861
978
  if (!existing) {
862
979
  throw new Error('Not found');
863
980
  }
864
981
  const updated = { ...existing, ...data };
865
- pings.set(id, updated);
982
+ pings.set(numId, updated);
866
983
  return updated;
867
984
  }
868
985
 
869
986
  export async function remove(id: string): Promise<void> {
870
987
  // TODO: Replace with actual database delete
871
- pings.delete(id);
988
+ pings.delete(Number(id));
872
989
  }
873
990
  `;
874
991
  }
875
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
+ `;
1083
+ }
1084
+
876
1085
  // src/scaffold.ts
877
1086
  function generateFiles(options) {
878
1087
  const files = [];
@@ -910,7 +1119,7 @@ function generateFiles(options) {
910
1119
  if (options.database === "kysely") {
911
1120
  files.push({
912
1121
  path: "src/db.ts",
913
- content: generateKyselySetup()
1122
+ content: generateKyselySetup(options.dialect)
914
1123
  });
915
1124
  files.push({
916
1125
  path: "src/types/db.ts",
@@ -922,7 +1131,7 @@ function generateFiles(options) {
922
1131
  });
923
1132
  files.push({
924
1133
  path: "src/migrations/001_create_ping.ts",
925
- content: generatePingMigration()
1134
+ content: generatePingMigration(options.dialect)
926
1135
  });
927
1136
  } else {
928
1137
  files.push({
@@ -957,6 +1166,20 @@ function generateFiles(options) {
957
1166
  path: "src/features/ping/repo/ping.repo.ts",
958
1167
  content: generatePingRepo(options)
959
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
+ }
960
1183
  return files;
961
1184
  }
962
1185
  function writeFiles(projectPath, files) {
package/dist/index.mjs CHANGED
@@ -52,7 +52,30 @@ async function promptForOptions(projectName) {
52
52
  p.cancel("Operation cancelled.");
53
53
  return null;
54
54
  }
55
- return { name, server, validator, database };
55
+ let dialect;
56
+ if (database === "kysely") {
57
+ dialect = await p.select({
58
+ message: "Database dialect:",
59
+ options: [
60
+ { value: "sqlite", label: "SQLite", hint: "recommended - great for per-customer databases" },
61
+ { value: "postgres", label: "PostgreSQL" },
62
+ { value: "mysql", label: "MySQL" }
63
+ ]
64
+ });
65
+ if (p.isCancel(dialect)) {
66
+ p.cancel("Operation cancelled.");
67
+ return null;
68
+ }
69
+ }
70
+ const docker = await p.confirm({
71
+ message: "Include Docker setup?",
72
+ initialValue: true
73
+ });
74
+ if (p.isCancel(docker)) {
75
+ p.cancel("Operation cancelled.");
76
+ return null;
77
+ }
78
+ return { name, server, validator, database, dialect, docker };
56
79
  }
57
80
 
58
81
  // src/scaffold.ts
@@ -81,8 +104,15 @@ function generatePackageJson(options) {
81
104
  if (options.database === "kysely") {
82
105
  dependencies["@fossyl/kysely"] = "^0.9.0";
83
106
  dependencies["kysely"] = "^0.27.0";
84
- dependencies["pg"] = "^8.13.0";
85
- devDependencies["@types/pg"] = "^8.11.0";
107
+ if (options.dialect === "sqlite") {
108
+ dependencies["better-sqlite3"] = "^11.0.0";
109
+ devDependencies["@types/better-sqlite3"] = "^7.6.0";
110
+ } else if (options.dialect === "mysql") {
111
+ dependencies["mysql2"] = "^3.11.0";
112
+ } else {
113
+ dependencies["pg"] = "^8.13.0";
114
+ devDependencies["@types/pg"] = "^8.11.0";
115
+ }
86
116
  }
87
117
  const pkg = {
88
118
  name: options.name === "." ? "my-fossyl-api" : options.name,
@@ -123,8 +153,17 @@ PORT=3000
123
153
  if (options.database === "kysely") {
124
154
  content += `
125
155
  # Database
126
- DATABASE_URL=postgres://user:password@localhost:5432/mydb
127
156
  `;
157
+ if (options.dialect === "sqlite") {
158
+ content += `DATABASE_PATH=./data/app.db
159
+ `;
160
+ } else if (options.dialect === "mysql") {
161
+ content += `DATABASE_URL=mysql://user:password@localhost:3306/mydb
162
+ `;
163
+ } else {
164
+ content += `DATABASE_URL=postgres://user:password@localhost:5432/mydb
165
+ `;
166
+ }
128
167
  }
129
168
  return content;
130
169
  }
@@ -370,7 +409,39 @@ export function startServer(routes: Route[], port: number): void {
370
409
  }
371
410
 
372
411
  // src/templates/database/kysely.ts
373
- function generateKyselySetup() {
412
+ function generateKyselySetup(dialect = "postgres") {
413
+ if (dialect === "sqlite") {
414
+ return `import Database from 'better-sqlite3';
415
+ import { Kysely, SqliteDialect } from 'kysely';
416
+ import type { DB } from './types/db';
417
+
418
+ const databasePath = process.env.DATABASE_PATH || './data/app.db';
419
+
420
+ export const db = new Kysely<DB>({
421
+ dialect: new SqliteDialect({
422
+ database: new Database(databasePath),
423
+ }),
424
+ });
425
+ `;
426
+ }
427
+ if (dialect === "mysql") {
428
+ return `import { createPool } from 'mysql2';
429
+ import { Kysely, MysqlDialect } from 'kysely';
430
+ import type { DB } from './types/db';
431
+
432
+ const connectionString = process.env.DATABASE_URL;
433
+
434
+ if (!connectionString) {
435
+ throw new Error('DATABASE_URL environment variable is required');
436
+ }
437
+
438
+ export const db = new Kysely<DB>({
439
+ dialect: new MysqlDialect({
440
+ pool: createPool(connectionString),
441
+ }),
442
+ });
443
+ `;
444
+ }
374
445
  return `import { Kysely, PostgresDialect } from 'kysely';
375
446
  import { Pool } from 'pg';
376
447
  import type { DB } from './types/db';
@@ -393,7 +464,7 @@ function generateDbTypes() {
393
464
 
394
465
  // Ping table types
395
466
  export interface PingTable {
396
- id: Generated<string>;
467
+ id: Generated<number>;
397
468
  message: string;
398
469
  created_by: string;
399
470
  created_at: Generated<Date>;
@@ -418,17 +489,61 @@ export const migrations = createMigrationProvider({
418
489
  });
419
490
  `;
420
491
  }
421
- function generatePingMigration() {
422
- return `import { sql } from 'kysely';
492
+ function generatePingMigration(dialect = "postgres") {
493
+ if (dialect === "sqlite") {
494
+ return `import { sql } from 'kysely';
423
495
  import { defineMigration } from '@fossyl/kysely';
424
496
 
425
497
  export const migration = defineMigration({
426
498
  async up(db) {
427
499
  await db.schema
428
500
  .createTable('ping')
429
- .addColumn('id', 'uuid', (col) =>
430
- col.primaryKey().defaultTo(sql\`gen_random_uuid()\`)
501
+ .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
502
+ .addColumn('message', 'text', (col) => col.notNull())
503
+ .addColumn('created_by', 'text', (col) => col.notNull())
504
+ .addColumn('created_at', 'text', (col) =>
505
+ col.notNull().defaultTo(sql\`(datetime('now'))\`)
431
506
  )
507
+ .execute();
508
+ },
509
+
510
+ async down(db) {
511
+ await db.schema.dropTable('ping').execute();
512
+ },
513
+ });
514
+ `;
515
+ }
516
+ if (dialect === "mysql") {
517
+ return `import { sql } from 'kysely';
518
+ import { defineMigration } from '@fossyl/kysely';
519
+
520
+ export const migration = defineMigration({
521
+ async up(db) {
522
+ await db.schema
523
+ .createTable('ping')
524
+ .addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
525
+ .addColumn('message', 'varchar(255)', (col) => col.notNull())
526
+ .addColumn('created_by', 'varchar(255)', (col) => col.notNull())
527
+ .addColumn('created_at', 'timestamp', (col) =>
528
+ col.notNull().defaultTo(sql\`CURRENT_TIMESTAMP\`)
529
+ )
530
+ .execute();
531
+ },
532
+
533
+ async down(db) {
534
+ await db.schema.dropTable('ping').execute();
535
+ },
536
+ });
537
+ `;
538
+ }
539
+ return `import { sql } from 'kysely';
540
+ import { defineMigration } from '@fossyl/kysely';
541
+
542
+ export const migration = defineMigration({
543
+ async up(db) {
544
+ await db.schema
545
+ .createTable('ping')
546
+ .addColumn('id', 'serial', (col) => col.primaryKey())
432
547
  .addColumn('message', 'varchar(255)', (col) => col.notNull())
433
548
  .addColumn('created_by', 'varchar(255)', (col) => col.notNull())
434
549
  .addColumn('created_at', 'timestamp', (col) =>
@@ -684,7 +799,7 @@ function generatePingService(_options) {
684
799
  return `import * as pingRepo from '../repo/ping.repo';
685
800
 
686
801
  export interface PingData {
687
- id: string;
802
+ id: number;
688
803
  message: string;
689
804
  created_by: string;
690
805
  created_at: Date;
@@ -760,7 +875,7 @@ export async function findById(id: string): Promise<Ping | undefined> {
760
875
  const db = getTransaction<DB>();
761
876
  return db
762
877
  .selectFrom('ping')
763
- .where('id', '=', id)
878
+ .where('id', '=', Number(id))
764
879
  .selectAll()
765
880
  .executeTakeFirst();
766
881
  }
@@ -779,14 +894,14 @@ export async function update(id: string, data: PingUpdate): Promise<Ping> {
779
894
  return db
780
895
  .updateTable('ping')
781
896
  .set(data)
782
- .where('id', '=', id)
897
+ .where('id', '=', Number(id))
783
898
  .returningAll()
784
899
  .executeTakeFirstOrThrow();
785
900
  }
786
901
 
787
902
  export async function remove(id: string): Promise<void> {
788
903
  const db = getTransaction<DB>();
789
- await db.deleteFrom('ping').where('id', '=', id).execute();
904
+ await db.deleteFrom('ping').where('id', '=', Number(id)).execute();
790
905
  }
791
906
  `;
792
907
  }
@@ -799,14 +914,15 @@ function generateByoPingRepo() {
799
914
  */
800
915
 
801
916
  export interface Ping {
802
- id: string;
917
+ id: number;
803
918
  message: string;
804
919
  created_by: string;
805
920
  created_at: Date;
806
921
  }
807
922
 
808
923
  // In-memory store for demo purposes - replace with actual database
809
- const pings: Map<string, Ping> = new Map();
924
+ const pings: Map<number, Ping> = new Map();
925
+ let nextId = 1;
810
926
 
811
927
  export async function findAll(limit: number, offset: number): Promise<Ping[]> {
812
928
  // TODO: Replace with actual database query
@@ -816,13 +932,13 @@ export async function findAll(limit: number, offset: number): Promise<Ping[]> {
816
932
 
817
933
  export async function findById(id: string): Promise<Ping | undefined> {
818
934
  // TODO: Replace with actual database query
819
- return pings.get(id);
935
+ return pings.get(Number(id));
820
936
  }
821
937
 
822
938
  export async function create(data: { message: string; created_by: string }): Promise<Ping> {
823
939
  // TODO: Replace with actual database insert
824
940
  const ping: Ping = {
825
- id: crypto.randomUUID(),
941
+ id: nextId++,
826
942
  message: data.message,
827
943
  created_by: data.created_by,
828
944
  created_at: new Date(),
@@ -833,22 +949,115 @@ export async function create(data: { message: string; created_by: string }): Pro
833
949
 
834
950
  export async function update(id: string, data: { message?: string }): Promise<Ping> {
835
951
  // TODO: Replace with actual database update
836
- const existing = pings.get(id);
952
+ const numId = Number(id);
953
+ const existing = pings.get(numId);
837
954
  if (!existing) {
838
955
  throw new Error('Not found');
839
956
  }
840
957
  const updated = { ...existing, ...data };
841
- pings.set(id, updated);
958
+ pings.set(numId, updated);
842
959
  return updated;
843
960
  }
844
961
 
845
962
  export async function remove(id: string): Promise<void> {
846
963
  // TODO: Replace with actual database delete
847
- pings.delete(id);
964
+ pings.delete(Number(id));
848
965
  }
849
966
  `;
850
967
  }
851
968
 
969
+ // src/templates/docker.ts
970
+ function generateDockerfile() {
971
+ return `FROM node:20-alpine
972
+ WORKDIR /app
973
+ COPY package*.json pnpm-lock.yaml* ./
974
+ RUN corepack enable && pnpm install --frozen-lockfile
975
+ COPY . .
976
+ RUN pnpm build
977
+ EXPOSE 3000
978
+ CMD ["node", "dist/index.js"]
979
+ `;
980
+ }
981
+ function generateDockerignore() {
982
+ return `node_modules
983
+ dist
984
+ *.log
985
+ .env
986
+ .git
987
+ `;
988
+ }
989
+ function generateDockerCompose(dialect) {
990
+ if (dialect === "sqlite") {
991
+ return `services:
992
+ app:
993
+ build: .
994
+ ports:
995
+ - "\${PORT:-3000}:3000"
996
+ volumes:
997
+ - ./data:/app/data
998
+ environment:
999
+ - DATABASE_PATH=/app/data/app.db
1000
+ `;
1001
+ }
1002
+ if (dialect === "mysql") {
1003
+ return `services:
1004
+ app:
1005
+ build: .
1006
+ ports:
1007
+ - "\${PORT:-3000}:3000"
1008
+ depends_on:
1009
+ db:
1010
+ condition: service_healthy
1011
+ environment:
1012
+ - DATABASE_URL=mysql://fossyl:fossyl@db:3306/fossyl
1013
+ db:
1014
+ image: mysql:8
1015
+ environment:
1016
+ MYSQL_ROOT_PASSWORD: root
1017
+ MYSQL_USER: fossyl
1018
+ MYSQL_PASSWORD: fossyl
1019
+ MYSQL_DATABASE: fossyl
1020
+ volumes:
1021
+ - mysqldata:/var/lib/mysql
1022
+ healthcheck:
1023
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
1024
+ interval: 5s
1025
+ timeout: 5s
1026
+ retries: 5
1027
+
1028
+ volumes:
1029
+ mysqldata:
1030
+ `;
1031
+ }
1032
+ return `services:
1033
+ app:
1034
+ build: .
1035
+ ports:
1036
+ - "\${PORT:-3000}:3000"
1037
+ depends_on:
1038
+ db:
1039
+ condition: service_healthy
1040
+ environment:
1041
+ - DATABASE_URL=postgres://fossyl:fossyl@db:5432/fossyl
1042
+ db:
1043
+ image: postgres:16-alpine
1044
+ environment:
1045
+ POSTGRES_USER: fossyl
1046
+ POSTGRES_PASSWORD: fossyl
1047
+ POSTGRES_DB: fossyl
1048
+ volumes:
1049
+ - pgdata:/var/lib/postgresql/data
1050
+ healthcheck:
1051
+ test: ["CMD-SHELL", "pg_isready -U fossyl"]
1052
+ interval: 5s
1053
+ timeout: 5s
1054
+ retries: 5
1055
+
1056
+ volumes:
1057
+ pgdata:
1058
+ `;
1059
+ }
1060
+
852
1061
  // src/scaffold.ts
853
1062
  function generateFiles(options) {
854
1063
  const files = [];
@@ -886,7 +1095,7 @@ function generateFiles(options) {
886
1095
  if (options.database === "kysely") {
887
1096
  files.push({
888
1097
  path: "src/db.ts",
889
- content: generateKyselySetup()
1098
+ content: generateKyselySetup(options.dialect)
890
1099
  });
891
1100
  files.push({
892
1101
  path: "src/types/db.ts",
@@ -898,7 +1107,7 @@ function generateFiles(options) {
898
1107
  });
899
1108
  files.push({
900
1109
  path: "src/migrations/001_create_ping.ts",
901
- content: generatePingMigration()
1110
+ content: generatePingMigration(options.dialect)
902
1111
  });
903
1112
  } else {
904
1113
  files.push({
@@ -933,6 +1142,20 @@ function generateFiles(options) {
933
1142
  path: "src/features/ping/repo/ping.repo.ts",
934
1143
  content: generatePingRepo(options)
935
1144
  });
1145
+ if (options.docker) {
1146
+ files.push({
1147
+ path: "Dockerfile",
1148
+ content: generateDockerfile()
1149
+ });
1150
+ files.push({
1151
+ path: ".dockerignore",
1152
+ content: generateDockerignore()
1153
+ });
1154
+ files.push({
1155
+ path: "docker-compose.yml",
1156
+ content: generateDockerCompose(options.dialect)
1157
+ });
1158
+ }
936
1159
  return files;
937
1160
  }
938
1161
  function writeFiles(projectPath, files) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fossyl",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI for scaffolding fossyl projects",
5
5
  "bin": {
6
6
  "fossyl": "./bin/fossyl.js"
@@ -25,7 +25,15 @@
25
25
  "cli",
26
26
  "scaffolding",
27
27
  "rest-api",
28
- "typescript"
28
+ "typescript",
29
+ "generator",
30
+ "project-generator",
31
+ "create-app",
32
+ "starter",
33
+ "boilerplate",
34
+ "api-generator",
35
+ "nodejs",
36
+ "backend"
29
37
  ],
30
38
  "author": "YoyoSaur",
31
39
  "license": "GPL-3.0",