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 +3 -2
- package/package.json +1 -1
- package/src/args.ts +8 -2
- package/src/cli.ts +3 -2
- package/src/features.ts +188 -41
- package/src/index.ts +14 -1
- package/src/module-cli.ts +79 -0
- package/src/prompts.ts +8 -0
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
|
-
| `
|
|
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,
|
|
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
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,
|
|
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,
|
|
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
|
-
| "
|
|
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: "
|
|
32
|
-
label: "
|
|
33
|
-
description: "Prisma 7
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
173
|
-
|
|
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
|
|
187
|
-
if (!
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
508
|
+
if (selection.postgres) {
|
|
439
509
|
lines.push(
|
|
440
|
-
"
|
|
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(
|
|
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.
|
|
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
|
-
|
|
597
|
+
/^COPY docker\/app\/docker-entrypoint\.sh .*$\n/m,
|
|
522
598
|
"",
|
|
523
599
|
);
|
|
524
600
|
content = content.replace(
|
|
525
|
-
|
|
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
|
|
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
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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);
|