fossyl 0.9.0 → 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/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';
@@ -442,7 +513,53 @@ export const migrations = createMigrationProvider({
442
513
  });
443
514
  `;
444
515
  }
445
- function generatePingMigration() {
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
+ }
446
563
  return `import { sql } from 'kysely';
447
564
  import { defineMigration } from '@fossyl/kysely';
448
565
 
@@ -873,6 +990,98 @@ export async function remove(id: string): Promise<void> {
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';
@@ -418,7 +489,53 @@ export const migrations = createMigrationProvider({
418
489
  });
419
490
  `;
420
491
  }
421
- function generatePingMigration() {
492
+ function generatePingMigration(dialect = "postgres") {
493
+ if (dialect === "sqlite") {
494
+ return `import { sql } from 'kysely';
495
+ import { defineMigration } from '@fossyl/kysely';
496
+
497
+ export const migration = defineMigration({
498
+ async up(db) {
499
+ await db.schema
500
+ .createTable('ping')
501
+ .addColumn('id', 'text', (col) => col.primaryKey())
502
+ .addColumn('message', 'text', (col) => col.notNull())
503
+ .addColumn('created_by', 'text', (col) => col.notNull())
504
+ .addColumn('created_at', 'text', (col) =>
505
+ col.notNull().defaultTo(sql\`(datetime('now'))\`)
506
+ )
507
+ .execute();
508
+ },
509
+
510
+ async down(db) {
511
+ await db.schema.dropTable('ping').execute();
512
+ },
513
+ });
514
+ `;
515
+ }
516
+ if (dialect === "mysql") {
517
+ return `import { sql } from 'kysely';
518
+ import { defineMigration } from '@fossyl/kysely';
519
+
520
+ export const migration = defineMigration({
521
+ async up(db) {
522
+ await db.schema
523
+ .createTable('ping')
524
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
525
+ .addColumn('message', 'varchar(255)', (col) => col.notNull())
526
+ .addColumn('created_by', 'varchar(255)', (col) => col.notNull())
527
+ .addColumn('created_at', 'timestamp', (col) =>
528
+ col.notNull().defaultTo(sql\`CURRENT_TIMESTAMP\`)
529
+ )
530
+ .execute();
531
+ },
532
+
533
+ async down(db) {
534
+ await db.schema.dropTable('ping').execute();
535
+ },
536
+ });
537
+ `;
538
+ }
422
539
  return `import { sql } from 'kysely';
423
540
  import { defineMigration } from '@fossyl/kysely';
424
541
 
@@ -849,6 +966,98 @@ export async function remove(id: string): Promise<void> {
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.10.0",
4
4
  "description": "CLI for scaffolding fossyl projects",
5
5
  "bin": {
6
6
  "fossyl": "./bin/fossyl.js"