kavoru 0.8.8 → 0.8.10

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 CHANGED
@@ -47,7 +47,7 @@ During setup you can pick which integrations to scaffold. Core is always include
47
47
  | ID | Feature |
48
48
  | ----------- | ---------------------- |
49
49
  | `auth` | JWT authentication |
50
- | `prisma` | Prisma + PostgreSQL |
50
+ | `postgres` | PostgreSQL + Prisma (includes Docker Postgres) |
51
51
  | `otel` | OpenTelemetry |
52
52
  | `sentry` | Sentry + Spotlight |
53
53
  | `kafka` | Kafka producer/consumer|
@@ -55,6 +55,7 @@ During setup you can pick which integrations to scaffold. Core is always include
55
55
  | `resend` | Resend email |
56
56
  | `cron` | Cron jobs |
57
57
  | `docker` | Dockerfile + Compose |
58
+ | `cli` | Project CLI (`kavoru module`, bin, scaffolds) |
58
59
 
59
60
  Interactive mode (TTY) shows a checkbox menu (↑↓ move, Space toggle, Enter confirm). Non-interactive runs use the full stack unless you pass flags.
60
61
 
@@ -71,7 +72,7 @@ bunx kavoru@latest .
71
72
  bunx kavoru@latest my-api --minimal
72
73
 
73
74
  # Pick specific features
74
- bunx kavoru@latest my-api --features auth,prisma,otel,sentry
75
+ bunx kavoru@latest my-api --features auth,postgres,otel,sentry
75
76
 
76
77
  # Full stack minus Kafka and Docker
77
78
  bunx kavoru@latest my-api --no-features kafka,docker
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kavoru",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "Scaffold a new Kavoru (Elysia + Bun) backend from the official template",
5
5
  "type": "module",
6
6
  "bin": {
package/src/args.ts CHANGED
@@ -15,9 +15,13 @@ export type CliOptions = {
15
15
 
16
16
  const HELP = `\
17
17
  Usage: kavoru [options] [directory]
18
+ kavoru module <module-name> [options]
18
19
 
19
20
  Create a new project from the Kavoru Elysia + Bun template.
20
21
 
22
+ Commands:
23
+ module <name> Generate src/modules/<name> (routes, service, types)
24
+
21
25
  Arguments:
22
26
  directory Project folder (use "." for current directory)
23
27
 
@@ -33,13 +37,15 @@ Options:
33
37
  --no-features <list> Comma-separated features to exclude
34
38
 
35
39
  Features:
36
- auth, prisma, otel, sentry, kafka, websocket, resend, cron, docker
40
+ auth, postgres, otel, sentry, kafka, websocket, resend, cron, docker, cli
41
+ (prisma is accepted as an alias for postgres; kavoru-cli for cli)
37
42
 
38
43
  Examples:
39
44
  bunx kavoru@latest my-api
40
45
  bunx kavoru@latest my-api --minimal
41
- bunx kavoru@latest my-api --features auth,prisma,otel
46
+ bunx kavoru@latest my-api --features auth,postgres,otel
42
47
  bunx kavoru@latest my-api --no-features kafka,docker,resend
48
+ bunx kavoru@latest module users
43
49
  `;
44
50
 
45
51
  export function parseArgs(argv: string[]): CliOptions {
package/src/cli.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  MINIMAL_FEATURES,
11
11
  applyFeatures,
12
12
  formatFeatureSelection,
13
+ normalizeFeatureSelection,
13
14
  parseFeatureExcludeList,
14
15
  parseFeatureIncludeList,
15
16
  type FeatureSelection,
@@ -72,7 +73,7 @@ export function resolveFeatureSelection(options: CliOptions): FeatureSelection {
72
73
  return parseFeatureExcludeList(options.noFeatures, ALL_FEATURES);
73
74
  }
74
75
 
75
- return { ...ALL_FEATURES };
76
+ return normalizeFeatureSelection({ ...ALL_FEATURES });
76
77
  }
77
78
 
78
79
  async function resolveFeatureSelectionInteractive(
@@ -88,7 +89,7 @@ async function resolveFeatureSelectionInteractive(
88
89
  return fromFlags;
89
90
  }
90
91
 
91
- return promptFeatureSelection(fromFlags);
92
+ return normalizeFeatureSelection(await promptFeatureSelection(fromFlags));
92
93
  }
93
94
 
94
95
  export async function runCli(options: CliOptions): Promise<void> {
package/src/features.ts CHANGED
@@ -4,14 +4,20 @@ import { log } from "./log";
4
4
 
5
5
  export type FeatureId =
6
6
  | "auth"
7
- | "prisma"
7
+ | "postgres"
8
8
  | "otel"
9
9
  | "sentry"
10
10
  | "kafka"
11
11
  | "websocket"
12
12
  | "resend"
13
13
  | "cron"
14
- | "docker";
14
+ | "docker"
15
+ | "cli";
16
+
17
+ const FEATURE_ALIASES: Record<string, FeatureId> = {
18
+ prisma: "postgres",
19
+ "kavoru-cli": "cli",
20
+ };
15
21
 
16
22
  export type FeatureSelection = Record<FeatureId, boolean>;
17
23
 
@@ -28,9 +34,9 @@ export const FEATURES: FeatureDef[] = [
28
34
  description: "Bearer auth, sign-in route, protected routes",
29
35
  },
30
36
  {
31
- id: "prisma",
32
- label: "Prisma + PostgreSQL",
33
- description: "Prisma 7 config, migrations, seed scripts",
37
+ id: "postgres",
38
+ label: "PostgreSQL",
39
+ description: "Docker Postgres, Prisma 7, migrations, and seed",
34
40
  },
35
41
  {
36
42
  id: "otel",
@@ -67,6 +73,11 @@ export const FEATURES: FeatureDef[] = [
67
73
  label: "Docker",
68
74
  description: "Dockerfile and Docker Compose stack",
69
75
  },
76
+ {
77
+ id: "cli",
78
+ label: "Project CLI",
79
+ description: "kavoru module command, bin, and module scaffolds",
80
+ },
70
81
  ];
71
82
 
72
83
  export const FEATURE_IDS = FEATURES.map((feature) => feature.id);
@@ -88,7 +99,7 @@ const FEATURE_PATHS: Record<FeatureId, string[]> = {
88
99
  "src/constants/jwt.ts",
89
100
  "src/models/schemas/signin.ts",
90
101
  ],
91
- prisma: ["prisma.config.ts", "src/infra/prisma"],
102
+ postgres: ["prisma.config.ts", "src/infra/prisma"],
92
103
  otel: ["src/infra/telemetry"],
93
104
  sentry: [
94
105
  "src/infra/sentry",
@@ -109,13 +120,20 @@ const FEATURE_PATHS: Record<FeatureId, string[]> = {
109
120
  resend: ["src/infra/resend"],
110
121
  cron: ["src/schedules"],
111
122
  docker: ["docker-compose.yaml", "docker"],
123
+ cli: [
124
+ "bin/kavoru.js",
125
+ "scripts/kavoru-cli.ts",
126
+ "scripts/generate-module.ts",
127
+ "__tests__/generate-module.test.ts",
128
+ "__tests__/kavoru-cli.test.ts",
129
+ ],
112
130
  };
113
131
 
114
132
  const FEATURE_DEPENDENCIES: Partial<
115
133
  Record<FeatureId, { dependencies?: string[]; devDependencies?: string[] }>
116
134
  > = {
117
135
  auth: { dependencies: ["@elysiajs/bearer", "@elysiajs/jwt"] },
118
- prisma: {
136
+ postgres: {
119
137
  dependencies: ["@prisma/adapter-pg", "@prisma/client"],
120
138
  devDependencies: ["prisma"],
121
139
  },
@@ -136,9 +154,43 @@ const FEATURE_DEPENDENCIES: Partial<
136
154
  const FEATURE_SCRIPTS: Partial<Record<FeatureId, string[]>> = {
137
155
  otel: ["otel:view", "otel:tui"],
138
156
  sentry: ["sentry:spotlight"],
139
- prisma: ["seed"],
157
+ postgres: ["seed"],
158
+ cli: ["kavoru", "module"],
140
159
  };
141
160
 
161
+ function resolveFeatureId(raw: string): FeatureId | null {
162
+ const id = FEATURE_ALIASES[raw] ?? raw;
163
+ return FEATURE_IDS.includes(id as FeatureId) ? (id as FeatureId) : null;
164
+ }
165
+
166
+ export function toPostgresName(packageName: string): string {
167
+ const normalized = packageName
168
+ .replace(/-/g, "_")
169
+ .replace(/[^a-z0-9_]/gi, "_")
170
+ .replace(/_+/g, "_")
171
+ .replace(/^_|_$/g, "");
172
+ return normalized || "app";
173
+ }
174
+
175
+ export function buildDatabaseUrl(
176
+ packageName: string,
177
+ host: string,
178
+ port = 5432,
179
+ ): string {
180
+ const name = toPostgresName(packageName);
181
+ return `postgresql://${name}:${name}@${host}:${port}/${name}`;
182
+ }
183
+
184
+ export function normalizeFeatureSelection(
185
+ selection: FeatureSelection,
186
+ ): FeatureSelection {
187
+ const next = { ...selection };
188
+ if (next.postgres) {
189
+ next.docker = true;
190
+ }
191
+ return next;
192
+ }
193
+
142
194
  function enabledFeatures(selection: FeatureSelection): FeatureId[] {
143
195
  return FEATURE_IDS.filter((id) => selection[id]);
144
196
  }
@@ -159,9 +211,7 @@ export function parseFeatureIncludeList(input: string): FeatureSelection {
159
211
  .map((part) => part.trim())
160
212
  .filter(Boolean);
161
213
 
162
- const unknown = requested.filter(
163
- (id) => !FEATURE_IDS.includes(id as FeatureId),
164
- );
214
+ const unknown = requested.filter((part) => resolveFeatureId(part) === null);
165
215
  if (unknown.length > 0) {
166
216
  throw new Error(
167
217
  `Unknown feature(s): ${unknown.join(", ")}. Valid: ${FEATURE_IDS.join(", ")}`,
@@ -169,8 +219,12 @@ export function parseFeatureIncludeList(input: string): FeatureSelection {
169
219
  }
170
220
 
171
221
  const selection = { ...MINIMAL_FEATURES };
172
- for (const id of requested) {
173
- selection[id as FeatureId] = true;
222
+ for (const part of requested) {
223
+ const id = resolveFeatureId(part);
224
+ if (id) selection[id] = true;
225
+ }
226
+ if (selection.postgres) {
227
+ selection.docker = true;
174
228
  }
175
229
  return selection;
176
230
  }
@@ -183,13 +237,14 @@ export function parseFeatureExcludeList(
183
237
  const unknown: string[] = [];
184
238
 
185
239
  for (const raw of excluded) {
186
- const id = raw.trim().toLowerCase();
187
- if (!id) continue;
188
- if (!FEATURE_IDS.includes(id as FeatureId)) {
189
- unknown.push(id);
240
+ const part = raw.trim().toLowerCase();
241
+ if (!part) continue;
242
+ const id = resolveFeatureId(part);
243
+ if (!id) {
244
+ unknown.push(part);
190
245
  continue;
191
246
  }
192
- selection[id as FeatureId] = false;
247
+ selection[id] = false;
193
248
  }
194
249
 
195
250
  if (unknown.length > 0) {
@@ -198,6 +253,10 @@ export function parseFeatureExcludeList(
198
253
  );
199
254
  }
200
255
 
256
+ if (!selection.docker) {
257
+ selection.postgres = false;
258
+ }
259
+
201
260
  return selection;
202
261
  }
203
262
 
@@ -349,11 +408,17 @@ export function buildEntryIndex(selection: FeatureSelection): string {
349
408
  return [...imports, ...body].join("\n");
350
409
  }
351
410
 
352
- async function patchEntryIndex(projectDir: string, selection: FeatureSelection) {
411
+ async function patchEntryIndex(
412
+ projectDir: string,
413
+ selection: FeatureSelection,
414
+ ) {
353
415
  await writeText(projectDir, "src/index.ts", buildEntryIndex(selection));
354
416
  }
355
417
 
356
- async function patchServerIndex(projectDir: string, selection: FeatureSelection) {
418
+ async function patchServerIndex(
419
+ projectDir: string,
420
+ selection: FeatureSelection,
421
+ ) {
357
422
  const relativePath = "src/server/index.ts";
358
423
  const current = await readText(projectDir, relativePath);
359
424
  if (!current) return;
@@ -396,6 +461,7 @@ async function patchPackageJson(
396
461
  if (!(await pkgFile.exists())) return;
397
462
 
398
463
  const pkg = (await pkgFile.json()) as {
464
+ bin?: Record<string, string>;
399
465
  dependencies?: Record<string, string>;
400
466
  devDependencies?: Record<string, string>;
401
467
  scripts?: Record<string, string>;
@@ -422,10 +488,14 @@ async function patchPackageJson(
422
488
  }
423
489
  }
424
490
 
425
- if (!selection.prisma && pkg.scripts) {
491
+ if (!selection.postgres && pkg.scripts) {
426
492
  pkg.scripts.start = "bun run src/index.ts";
427
493
  }
428
494
 
495
+ if (!selection.cli) {
496
+ delete pkg.bin;
497
+ }
498
+
429
499
  await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
430
500
  }
431
501
 
@@ -435,9 +505,11 @@ export function buildEnvExample(
435
505
  ): string {
436
506
  const lines = ["NODE_ENV=development", "PORT=3131", ""];
437
507
 
438
- if (selection.prisma) {
508
+ if (selection.postgres) {
439
509
  lines.push(
440
- "DATABASE_URL=postgresql://user:password@localhost:5432/your_database",
510
+ "# Start database: docker compose up -d postgres",
511
+ "# Host dev uses published port; Docker app overrides host in docker/app/.env",
512
+ `DATABASE_URL=${buildDatabaseUrl(packageName, "localhost")}`,
441
513
  "",
442
514
  );
443
515
  }
@@ -507,7 +579,10 @@ async function patchEnvExample(
507
579
  await writeText(projectDir, ".env", content);
508
580
  }
509
581
 
510
- async function patchDockerfile(projectDir: string, selection: FeatureSelection) {
582
+ async function patchDockerfile(
583
+ projectDir: string,
584
+ selection: FeatureSelection,
585
+ ) {
511
586
  if (!selection.docker) return;
512
587
 
513
588
  const relativePath = "docker/app/Dockerfile";
@@ -515,14 +590,19 @@ async function patchDockerfile(projectDir: string, selection: FeatureSelection)
515
590
  if (!current) return;
516
591
 
517
592
  let content = current;
518
- if (!selection.prisma) {
593
+ if (!selection.postgres) {
519
594
  content = content.replace(/^\s*COPY prisma\.config\.ts \.\/.*\n/m, "");
595
+ content = content.replace(/^\s*RUN bunx prisma generate\n/m, "");
520
596
  content = content.replace(
521
- /^\s*# generate only needs the schema on disk, not a live database\n/m,
597
+ /^COPY docker\/app\/docker-entrypoint\.sh .*$\n/m,
522
598
  "",
523
599
  );
524
600
  content = content.replace(
525
- /^\s*RUN if \[ -f src\/infra\/prisma\/schemas\/schema\.prisma \]; then bunx prisma generate; fi\n/m,
601
+ /^RUN sed -i 's\/\\r\$\/\/' \/app\/docker-entrypoint\.sh && chmod \+x \/app\/docker-entrypoint\.sh\n/m,
602
+ "",
603
+ );
604
+ content = content.replace(
605
+ /^ENTRYPOINT \["\/bin\/sh", "\/app\/docker-entrypoint\.sh"\]\n/m,
526
606
  "",
527
607
  );
528
608
  }
@@ -552,11 +632,25 @@ const DOCKER_OTEL_ENV =
552
632
  const DOCKER_SPOTLIGHT_ENV =
553
633
  "# Official Spotlight image; add overrides here if needed.\n";
554
634
 
555
- function buildDockerAppEnv(selection: FeatureSelection): string {
635
+ function buildDockerPostgresEnv(packageName: string): string {
636
+ const name = toPostgresName(packageName);
637
+ return `POSTGRES_USER=${name}
638
+ POSTGRES_PASSWORD=${name}
639
+ POSTGRES_DB=${name}
640
+ `;
641
+ }
642
+
643
+ function buildDockerAppEnv(
644
+ selection: FeatureSelection,
645
+ packageName: string,
646
+ ): string {
556
647
  const lines = [
557
648
  "# Docker-only overrides (loaded after root .env)",
558
649
  "NODE_ENV=production",
559
650
  ];
651
+ if (selection.postgres) {
652
+ lines.push(`DATABASE_URL=${buildDatabaseUrl(packageName, "postgres")}`);
653
+ }
560
654
  if (selection.kafka) {
561
655
  lines.push("KAFKA_BROKERS=kafka:9092");
562
656
  }
@@ -569,11 +663,45 @@ function buildDockerAppEnv(selection: FeatureSelection): string {
569
663
  return `${lines.join("\n")}\n`;
570
664
  }
571
665
 
666
+ function buildAppDependsOn(selection: FeatureSelection): string {
667
+ const deps: string[] = [];
668
+ if (selection.postgres) {
669
+ deps.push(` postgres:
670
+ condition: service_healthy`);
671
+ }
672
+ if (selection.kafka) {
673
+ deps.push(` kafka:
674
+ condition: service_started`);
675
+ }
676
+ if (deps.length === 0) return "";
677
+ return ` depends_on:
678
+ ${deps.join("\n")}
679
+ `;
680
+ }
681
+
572
682
  function generateDockerCompose(selection: FeatureSelection): string {
573
- const appDependsOn = selection.kafka
574
- ? ` depends_on:
575
- kafka:
576
- condition: service_started
683
+ const appDependsOn = buildAppDependsOn(selection);
684
+
685
+ const postgresService = selection.postgres
686
+ ? `
687
+ postgres:
688
+ build:
689
+ context: docker/postgres
690
+ hostname: postgres
691
+ ports:
692
+ - "\${POSTGRES_PORT:-5432}:5432"
693
+ env_file:
694
+ - docker/postgres/.env
695
+ volumes:
696
+ - postgres_data:/var/lib/postgresql/data
697
+ networks:
698
+ - app_network
699
+ healthcheck:
700
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
701
+ interval: 5s
702
+ timeout: 5s
703
+ retries: 5
704
+ restart: unless-stopped
577
705
  `
578
706
  : "";
579
707
 
@@ -631,7 +759,7 @@ function generateDockerCompose(selection: FeatureSelection): string {
631
759
  target: build
632
760
  args:
633
761
  PORT: \${PORT:-3131}
634
- command: /app/server
762
+ command: ./server
635
763
  volumes:
636
764
  - ./src:/app/src
637
765
  networks:
@@ -644,27 +772,35 @@ function generateDockerCompose(selection: FeatureSelection): string {
644
772
  - "\${PORT:-3131}:\${PORT:-3131}"
645
773
  restart: unless-stopped
646
774
  env_file:
647
- - .env
775
+ - path: .env
776
+ required: false
648
777
  - docker/app/.env
649
778
  ${appDependsOn} healthcheck:
650
- test: ["CMD", "curl", "-f", "http://localhost:\${PORT}/healthz"]
779
+ test: ["CMD", "curl", "-f", "http://localhost:\${PORT:-3131}/healthz"]
651
780
  interval: 600s
652
781
  timeout: 300s
653
782
  retries: 1
654
- start_period: 40s
655
- ${kafkaService}${otelService}${spotlightService}
783
+ start_period: 90s
784
+ ${postgresService}${kafkaService}${otelService}${spotlightService}
656
785
  networks:
657
786
  app_network:
658
787
  driver: bridge
659
- `;
788
+ ${selection.postgres ? "\nvolumes:\n postgres_data:\n" : ""}`;
660
789
  }
661
790
 
662
791
  async function patchDockerCompose(
663
792
  projectDir: string,
664
793
  selection: FeatureSelection,
794
+ packageName: string,
665
795
  ) {
666
796
  if (!selection.docker) return;
667
797
 
798
+ if (!selection.postgres) {
799
+ await removePaths(projectDir, [
800
+ "docker/postgres",
801
+ "docker/app/docker-entrypoint.sh",
802
+ ]);
803
+ }
668
804
  if (!selection.kafka) {
669
805
  await removePaths(projectDir, ["docker/kafka"]);
670
806
  }
@@ -678,8 +814,15 @@ async function patchDockerCompose(
678
814
  await writeText(
679
815
  projectDir,
680
816
  "docker/app/.env",
681
- buildDockerAppEnv(selection),
817
+ buildDockerAppEnv(selection, packageName),
682
818
  );
819
+ if (selection.postgres) {
820
+ await writeText(
821
+ projectDir,
822
+ "docker/postgres/.env",
823
+ buildDockerPostgresEnv(packageName),
824
+ );
825
+ }
683
826
  if (selection.kafka) {
684
827
  await writeText(projectDir, "docker/kafka/.env", DOCKER_KAFKA_ENV);
685
828
  }
@@ -689,7 +832,11 @@ async function patchDockerCompose(
689
832
  if (selection.sentry) {
690
833
  await writeText(projectDir, "docker/spotlight/.env", DOCKER_SPOTLIGHT_ENV);
691
834
  }
692
- await writeText(projectDir, "docker-compose.yaml", generateDockerCompose(selection));
835
+ await writeText(
836
+ projectDir,
837
+ "docker-compose.yaml",
838
+ generateDockerCompose(selection),
839
+ );
693
840
  }
694
841
 
695
842
  export async function applyFeatures(
@@ -710,7 +857,7 @@ export async function applyFeatures(
710
857
  await patchPackageJson(projectDir, selection);
711
858
  await patchEnvExample(projectDir, selection, packageName);
712
859
  await patchDockerfile(projectDir, selection);
713
- await patchDockerCompose(projectDir, selection);
860
+ await patchDockerCompose(projectDir, selection, packageName);
714
861
 
715
862
  if (disabled.length > 0) {
716
863
  log.success("Feature selection applied");
package/src/index.ts CHANGED
@@ -3,10 +3,23 @@
3
3
  import { parseArgs, printHelp, printVersion } from "./args";
4
4
  import { runCli } from "./cli";
5
5
  import { log } from "./log";
6
+ import { printModuleHelp, runModuleCommand } from "./module-cli";
6
7
 
7
8
  async function main(): Promise<void> {
8
9
  try {
9
- const options = parseArgs(process.argv.slice(2));
10
+ const argv = process.argv.slice(2);
11
+
12
+ if (argv[0] === "module") {
13
+ if (argv.includes("-h") || argv.includes("--help")) {
14
+ printModuleHelp();
15
+ return;
16
+ }
17
+
18
+ await runModuleCommand(argv.slice(1));
19
+ return;
20
+ }
21
+
22
+ const options = parseArgs(argv);
10
23
 
11
24
  if (options.help) {
12
25
  printHelp();
@@ -0,0 +1,79 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { log } from "./log";
4
+
5
+ function findProjectRoot(cwd: string): string {
6
+ let current = path.resolve(cwd);
7
+
8
+ while (true) {
9
+ const packageJson = path.join(current, "package.json");
10
+ const localCli = path.join(current, "scripts/kavoru-cli.ts");
11
+ const moduleScript = path.join(current, "scripts/generate-module.ts");
12
+ if (
13
+ existsSync(packageJson) &&
14
+ (existsSync(localCli) || existsSync(moduleScript))
15
+ ) {
16
+ return current;
17
+ }
18
+
19
+ const parent = path.dirname(current);
20
+ if (parent === current) {
21
+ break;
22
+ }
23
+ current = parent;
24
+ }
25
+
26
+ throw new Error(
27
+ "Could not find a Kavoru project with the Project CLI enabled. Scaffold with the cli feature or run from a project that includes scripts/kavoru-cli.ts.",
28
+ );
29
+ }
30
+
31
+ export async function runModuleCommand(argv: string[]): Promise<void> {
32
+ const force = argv.includes("--force") || argv.includes("-f");
33
+ const name = argv.find((arg) => !arg.startsWith("-"));
34
+
35
+ if (!name) {
36
+ throw new Error("Usage: kavoru module <module-name> [--force]");
37
+ }
38
+
39
+ const projectDir = findProjectRoot(process.cwd());
40
+ const localCli = path.join(projectDir, "scripts/kavoru-cli.ts");
41
+ const scriptPath = existsSync(localCli)
42
+ ? localCli
43
+ : path.join(projectDir, "scripts/generate-module.ts");
44
+ const cmd = existsSync(localCli)
45
+ ? ["bun", scriptPath, "module", name]
46
+ : ["bun", scriptPath, name];
47
+ if (force) cmd.push("--force");
48
+
49
+ log.info(`Generating module "${name}" in ${projectDir}`);
50
+
51
+ const proc = Bun.spawn(cmd, {
52
+ cwd: projectDir,
53
+ stdout: "inherit",
54
+ stderr: "inherit",
55
+ });
56
+
57
+ const exitCode = await proc.exited;
58
+ if (exitCode !== 0) {
59
+ process.exit(exitCode ?? 1);
60
+ }
61
+ }
62
+
63
+ export function printModuleHelp(): void {
64
+ console.log(`\
65
+ Usage: kavoru module <module-name> [options]
66
+
67
+ Generate a feature module under src/modules/<module-name>/ with:
68
+ routes.ts, service.ts, types.ts
69
+
70
+ Options:
71
+ -f, --force Overwrite an existing module folder
72
+ -h, --help Show help
73
+
74
+ Examples:
75
+ kavoru module users
76
+ kavoru module user-profile --force
77
+ bun run kavoru module billing
78
+ `);
79
+ }
package/src/prompts.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  ALL_FEATURES,
4
4
  FEATURES,
5
5
  MINIMAL_FEATURES,
6
+ normalizeFeatureSelection,
6
7
  type FeatureId,
7
8
  type FeatureSelection,
8
9
  formatFeatureSelection,
@@ -154,6 +155,12 @@ export async function promptFeatureSelection(
154
155
  const feature = FEATURES[activeIndex];
155
156
  if (!feature) break;
156
157
  selection[feature.id as FeatureId] = !selection[feature.id as FeatureId];
158
+ if (feature.id === "postgres" && selection.postgres) {
159
+ selection.docker = true;
160
+ }
161
+ if (feature.id === "docker" && !selection.docker) {
162
+ selection.postgres = false;
163
+ }
157
164
  lineCount = renderCheckboxMenu(selection, activeIndex, lineCount);
158
165
  break;
159
166
  }
@@ -166,6 +173,7 @@ export async function promptFeatureSelection(
166
173
  lineCount = renderCheckboxMenu(selection, activeIndex, lineCount);
167
174
  break;
168
175
  case "confirm":
176
+ Object.assign(selection, normalizeFeatureSelection(selection));
169
177
  restoreTerminal(onData);
170
178
  stdout.write("\n");
171
179
  resolve(selection);