kavoru 0.9.0 → 0.9.2
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 +1 -0
- package/package.json +1 -1
- package/src/args.ts +1 -1
- package/src/features.ts +115 -6
- package/src/index.ts +11 -1
- package/src/module-cli.ts +73 -16
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ During setup you can pick which integrations to scaffold. Core is always include
|
|
|
51
51
|
| `otel` | OpenTelemetry |
|
|
52
52
|
| `sentry` | Sentry + Spotlight |
|
|
53
53
|
| `kafka` | Kafka producer/consumer|
|
|
54
|
+
| `redis` | Redis cache + CRUD API |
|
|
54
55
|
| `websocket` | WebSocket realtime |
|
|
55
56
|
| `resend` | Resend email |
|
|
56
57
|
| `cron` | Cron jobs |
|
package/package.json
CHANGED
package/src/args.ts
CHANGED
|
@@ -37,7 +37,7 @@ Options:
|
|
|
37
37
|
--no-features <list> Comma-separated features to exclude
|
|
38
38
|
|
|
39
39
|
Features:
|
|
40
|
-
auth, postgres, otel, sentry, kafka, websocket, resend, cron, docker, cli
|
|
40
|
+
auth, postgres, otel, sentry, kafka, redis, websocket, resend, cron, docker, cli
|
|
41
41
|
(prisma is accepted as an alias for postgres; kavoru-cli for cli)
|
|
42
42
|
|
|
43
43
|
Examples:
|
package/src/features.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type FeatureId =
|
|
|
8
8
|
| "otel"
|
|
9
9
|
| "sentry"
|
|
10
10
|
| "kafka"
|
|
11
|
+
| "redis"
|
|
11
12
|
| "websocket"
|
|
12
13
|
| "resend"
|
|
13
14
|
| "cron"
|
|
@@ -53,6 +54,11 @@ export const FEATURES: FeatureDef[] = [
|
|
|
53
54
|
label: "Kafka",
|
|
54
55
|
description: "Producer, consumer, and example HTTP endpoints",
|
|
55
56
|
},
|
|
57
|
+
{
|
|
58
|
+
id: "redis",
|
|
59
|
+
label: "Redis",
|
|
60
|
+
description: "Cache client and CRUD HTTP endpoints",
|
|
61
|
+
},
|
|
56
62
|
{
|
|
57
63
|
id: "websocket",
|
|
58
64
|
label: "WebSockets",
|
|
@@ -112,6 +118,12 @@ const FEATURE_PATHS: Record<FeatureId, string[]> = {
|
|
|
112
118
|
"src/models/schemas/kafka.ts",
|
|
113
119
|
"__tests__/kafka.test.ts",
|
|
114
120
|
],
|
|
121
|
+
redis: [
|
|
122
|
+
"src/modules/redis",
|
|
123
|
+
"src/infra/redis",
|
|
124
|
+
"src/models/schemas/redis.ts",
|
|
125
|
+
"__tests__/redis.test.ts",
|
|
126
|
+
],
|
|
115
127
|
websocket: [
|
|
116
128
|
"src/modules/realtime",
|
|
117
129
|
"src/models/schemas/realtime.ts",
|
|
@@ -126,8 +138,10 @@ const FEATURE_PATHS: Record<FeatureId, string[]> = {
|
|
|
126
138
|
"kavoru.cmd",
|
|
127
139
|
"scripts/kavoru-cli.ts",
|
|
128
140
|
"scripts/generate-module.ts",
|
|
141
|
+
"scripts/generate-repository.ts",
|
|
129
142
|
"scripts/link-cli.ts",
|
|
130
143
|
"__tests__/generate-module.test.ts",
|
|
144
|
+
"__tests__/generate-repository.test.ts",
|
|
131
145
|
"__tests__/kavoru-cli.test.ts",
|
|
132
146
|
"__tests__/link-cli.test.ts",
|
|
133
147
|
],
|
|
@@ -151,6 +165,7 @@ const FEATURE_DEPENDENCIES: Partial<
|
|
|
151
165
|
},
|
|
152
166
|
sentry: { dependencies: ["@sentry/elysia"] },
|
|
153
167
|
kafka: { dependencies: ["kafkajs"] },
|
|
168
|
+
redis: { dependencies: ["ioredis"] },
|
|
154
169
|
resend: { dependencies: ["resend"] },
|
|
155
170
|
cron: { dependencies: ["@elysiajs/cron"] },
|
|
156
171
|
};
|
|
@@ -185,6 +200,14 @@ export function buildDatabaseUrl(
|
|
|
185
200
|
return `postgresql://${name}:${name}@${host}:${port}/${name}`;
|
|
186
201
|
}
|
|
187
202
|
|
|
203
|
+
export function buildRedisCredentials(packageName: string): {
|
|
204
|
+
username: string;
|
|
205
|
+
password: string;
|
|
206
|
+
} {
|
|
207
|
+
const name = toPostgresName(packageName);
|
|
208
|
+
return { username: name, password: name };
|
|
209
|
+
}
|
|
210
|
+
|
|
188
211
|
export function normalizeFeatureSelection(
|
|
189
212
|
selection: FeatureSelection,
|
|
190
213
|
): FeatureSelection {
|
|
@@ -340,6 +363,8 @@ async function patchModulesIndex(
|
|
|
340
363
|
}
|
|
341
364
|
|
|
342
365
|
export function buildEntryIndex(selection: FeatureSelection): string {
|
|
366
|
+
const needsAsyncStartup = selection.kafka || selection.redis;
|
|
367
|
+
|
|
343
368
|
const imports = [
|
|
344
369
|
selection.sentry
|
|
345
370
|
? 'import { initSentry, flushSentry } from "./infra/sentry";'
|
|
@@ -350,9 +375,12 @@ export function buildEntryIndex(selection: FeatureSelection): string {
|
|
|
350
375
|
selection.kafka
|
|
351
376
|
? 'import { startKafka, stopKafka } from "./infra/kafka";'
|
|
352
377
|
: null,
|
|
378
|
+
selection.redis
|
|
379
|
+
? 'import { connectRedis, stopRedis } from "./infra/redis";'
|
|
380
|
+
: null,
|
|
353
381
|
'import { HttpServer } from "./server/index";',
|
|
354
382
|
'import { logger } from "./common/logger";',
|
|
355
|
-
|
|
383
|
+
needsAsyncStartup ? 'import { InternalServerError } from "elysia";' : null,
|
|
356
384
|
].filter(Boolean) as string[];
|
|
357
385
|
|
|
358
386
|
const body: string[] = [];
|
|
@@ -366,14 +394,18 @@ export function buildEntryIndex(selection: FeatureSelection): string {
|
|
|
366
394
|
|
|
367
395
|
body.push("", "const server = new HttpServer();", "");
|
|
368
396
|
|
|
369
|
-
if (
|
|
397
|
+
if (needsAsyncStartup) {
|
|
398
|
+
const startupCalls: string[] = [];
|
|
399
|
+
if (selection.kafka) startupCalls.push(" await startKafka();");
|
|
400
|
+
if (selection.redis) startupCalls.push(" await connectRedis();");
|
|
401
|
+
|
|
370
402
|
body.push(
|
|
371
403
|
"void server.start().then(async () => {",
|
|
372
404
|
" try {",
|
|
373
|
-
|
|
405
|
+
...startupCalls,
|
|
374
406
|
" } catch (error) {",
|
|
375
|
-
' logger.error("Failed to start
|
|
376
|
-
' throw new InternalServerError("Failed to start
|
|
407
|
+
' logger.error("Failed to start infrastructure", { error });',
|
|
408
|
+
' throw new InternalServerError("Failed to start infrastructure");',
|
|
377
409
|
" }",
|
|
378
410
|
"});",
|
|
379
411
|
);
|
|
@@ -392,6 +424,9 @@ export function buildEntryIndex(selection: FeatureSelection): string {
|
|
|
392
424
|
if (selection.kafka) {
|
|
393
425
|
body.push(" await stopKafka();");
|
|
394
426
|
}
|
|
427
|
+
if (selection.redis) {
|
|
428
|
+
body.push(" await stopRedis();");
|
|
429
|
+
}
|
|
395
430
|
if (selection.sentry) {
|
|
396
431
|
body.push(" await flushSentry();");
|
|
397
432
|
}
|
|
@@ -566,6 +601,19 @@ export function buildEnvExample(
|
|
|
566
601
|
);
|
|
567
602
|
}
|
|
568
603
|
|
|
604
|
+
if (selection.redis) {
|
|
605
|
+
const { username, password } = buildRedisCredentials(packageName);
|
|
606
|
+
lines.push(
|
|
607
|
+
"# Redis (enabled by default in development; disabled in test)",
|
|
608
|
+
"# Start server: docker compose up -d redis",
|
|
609
|
+
"# REDIS_ENABLED=false",
|
|
610
|
+
"REDIS_URL=redis://localhost:6379",
|
|
611
|
+
`REDIS_USERNAME=${username}`,
|
|
612
|
+
`REDIS_PASSWORD=${password}`,
|
|
613
|
+
"",
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
569
617
|
if (selection.resend) {
|
|
570
618
|
lines.push(
|
|
571
619
|
"# Resend (disabled when RESEND_API_KEY is unset; always disabled in test)",
|
|
@@ -618,9 +666,25 @@ async function patchDockerfile(
|
|
|
618
666
|
);
|
|
619
667
|
}
|
|
620
668
|
|
|
669
|
+
if (!selection.cli) {
|
|
670
|
+
content = content.replace(/^COPY bin \.\/bin\n/m, "");
|
|
671
|
+
content = content.replace(
|
|
672
|
+
/^COPY scripts\/link-cli\.ts \.\/scripts\/link-cli\.ts\n/m,
|
|
673
|
+
"",
|
|
674
|
+
);
|
|
675
|
+
content = content.replace(/^ENV PATH="\/root\/\.bun\/bin:\$\{PATH\}"\n/m, "");
|
|
676
|
+
}
|
|
677
|
+
|
|
621
678
|
await writeText(projectDir, relativePath, content);
|
|
622
679
|
}
|
|
623
680
|
|
|
681
|
+
function buildDockerRedisEnv(packageName: string): string {
|
|
682
|
+
const { username, password } = buildRedisCredentials(packageName);
|
|
683
|
+
return `REDIS_USERNAME=${username}
|
|
684
|
+
REDIS_PASSWORD=${password}
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
|
|
624
688
|
const DOCKER_KAFKA_ENV = `# KRaft broker config (Confluent cp-kafka 7.6.1)
|
|
625
689
|
CLUSTER_ID=MkU3OEVBNTcwNTJENDM2Qk
|
|
626
690
|
KAFKA_NODE_ID=0
|
|
@@ -665,6 +729,12 @@ function buildDockerAppEnv(
|
|
|
665
729
|
if (selection.kafka) {
|
|
666
730
|
lines.push("KAFKA_BROKERS=kafka:9092");
|
|
667
731
|
}
|
|
732
|
+
if (selection.redis) {
|
|
733
|
+
const { username, password } = buildRedisCredentials(packageName);
|
|
734
|
+
lines.push("REDIS_URL=redis://redis:6379");
|
|
735
|
+
lines.push(`REDIS_USERNAME=${username}`);
|
|
736
|
+
lines.push(`REDIS_PASSWORD=${password}`);
|
|
737
|
+
}
|
|
668
738
|
if (selection.otel) {
|
|
669
739
|
lines.push("OTEL_EXPORTER_OTLP_ENDPOINT=http://otel:4318/v1/traces");
|
|
670
740
|
}
|
|
@@ -684,6 +754,10 @@ function buildAppDependsOn(selection: FeatureSelection): string {
|
|
|
684
754
|
deps.push(` kafka:
|
|
685
755
|
condition: service_started`);
|
|
686
756
|
}
|
|
757
|
+
if (selection.redis) {
|
|
758
|
+
deps.push(` redis:
|
|
759
|
+
condition: service_healthy`);
|
|
760
|
+
}
|
|
687
761
|
if (deps.length === 0) return "";
|
|
688
762
|
return ` depends_on:
|
|
689
763
|
${deps.join("\n")}
|
|
@@ -732,6 +806,31 @@ function generateDockerCompose(selection: FeatureSelection): string {
|
|
|
732
806
|
`
|
|
733
807
|
: "";
|
|
734
808
|
|
|
809
|
+
const redisService = selection.redis
|
|
810
|
+
? `
|
|
811
|
+
redis:
|
|
812
|
+
build:
|
|
813
|
+
context: docker/redis
|
|
814
|
+
hostname: redis
|
|
815
|
+
ports:
|
|
816
|
+
- "\${REDIS_PORT:-6379}:6379"
|
|
817
|
+
env_file:
|
|
818
|
+
- docker/redis/.env
|
|
819
|
+
networks:
|
|
820
|
+
- app_network
|
|
821
|
+
healthcheck:
|
|
822
|
+
test:
|
|
823
|
+
[
|
|
824
|
+
"CMD-SHELL",
|
|
825
|
+
"redis-cli --user $$REDIS_USERNAME -a $$REDIS_PASSWORD ping | grep -q PONG",
|
|
826
|
+
]
|
|
827
|
+
interval: 5s
|
|
828
|
+
timeout: 3s
|
|
829
|
+
retries: 5
|
|
830
|
+
restart: unless-stopped
|
|
831
|
+
`
|
|
832
|
+
: "";
|
|
833
|
+
|
|
735
834
|
const otelService = selection.otel
|
|
736
835
|
? `
|
|
737
836
|
otel:
|
|
@@ -792,7 +891,7 @@ ${appDependsOn} healthcheck:
|
|
|
792
891
|
timeout: 300s
|
|
793
892
|
retries: 1
|
|
794
893
|
start_period: 90s
|
|
795
|
-
${postgresService}${kafkaService}${otelService}${spotlightService}
|
|
894
|
+
${postgresService}${kafkaService}${redisService}${otelService}${spotlightService}
|
|
796
895
|
networks:
|
|
797
896
|
app_network:
|
|
798
897
|
driver: bridge
|
|
@@ -815,6 +914,9 @@ async function patchDockerCompose(
|
|
|
815
914
|
if (!selection.kafka) {
|
|
816
915
|
await removePaths(projectDir, ["docker/kafka"]);
|
|
817
916
|
}
|
|
917
|
+
if (!selection.redis) {
|
|
918
|
+
await removePaths(projectDir, ["docker/redis"]);
|
|
919
|
+
}
|
|
818
920
|
if (!selection.otel) {
|
|
819
921
|
await removePaths(projectDir, ["docker/otel"]);
|
|
820
922
|
}
|
|
@@ -837,6 +939,13 @@ async function patchDockerCompose(
|
|
|
837
939
|
if (selection.kafka) {
|
|
838
940
|
await writeText(projectDir, "docker/kafka/.env", DOCKER_KAFKA_ENV);
|
|
839
941
|
}
|
|
942
|
+
if (selection.redis) {
|
|
943
|
+
await writeText(
|
|
944
|
+
projectDir,
|
|
945
|
+
"docker/redis/.env",
|
|
946
|
+
buildDockerRedisEnv(packageName),
|
|
947
|
+
);
|
|
948
|
+
}
|
|
840
949
|
if (selection.otel) {
|
|
841
950
|
await writeText(projectDir, "docker/otel/.env", DOCKER_OTEL_ENV);
|
|
842
951
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
|
+
import { printModuleHelp, printRepositoryHelp, runModuleCommand, runRepositoryCommand } from "./module-cli";
|
|
7
7
|
|
|
8
8
|
async function main(): Promise<void> {
|
|
9
9
|
try {
|
|
@@ -19,6 +19,16 @@ async function main(): Promise<void> {
|
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
if (argv[0] === "repository") {
|
|
23
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
24
|
+
printRepositoryHelp();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await runRepositoryCommand(argv.slice(1));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
const options = parseArgs(argv);
|
|
23
33
|
|
|
24
34
|
if (options.help) {
|
package/src/module-cli.ts
CHANGED
|
@@ -9,9 +9,15 @@ function findProjectRoot(cwd: string): string {
|
|
|
9
9
|
const packageJson = path.join(current, "package.json");
|
|
10
10
|
const localCli = path.join(current, "scripts/kavoru-cli.ts");
|
|
11
11
|
const moduleScript = path.join(current, "scripts/generate-module.ts");
|
|
12
|
+
const repositoryScript = path.join(
|
|
13
|
+
current,
|
|
14
|
+
"scripts/generate-repository.ts",
|
|
15
|
+
);
|
|
12
16
|
if (
|
|
13
17
|
existsSync(packageJson) &&
|
|
14
|
-
(existsSync(localCli) ||
|
|
18
|
+
(existsSync(localCli) ||
|
|
19
|
+
existsSync(moduleScript) ||
|
|
20
|
+
existsSync(repositoryScript))
|
|
15
21
|
) {
|
|
16
22
|
return current;
|
|
17
23
|
}
|
|
@@ -28,26 +34,24 @@ function findProjectRoot(cwd: string): string {
|
|
|
28
34
|
);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const projectDir = findProjectRoot(process.cwd());
|
|
37
|
+
async function runProjectScript(
|
|
38
|
+
projectDir: string,
|
|
39
|
+
command: "module" | "repository",
|
|
40
|
+
name: string,
|
|
41
|
+
force: boolean,
|
|
42
|
+
): Promise<void> {
|
|
40
43
|
const localCli = path.join(projectDir, "scripts/kavoru-cli.ts");
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const fallbackScript =
|
|
45
|
+
command === "module"
|
|
46
|
+
? path.join(projectDir, "scripts/generate-module.ts")
|
|
47
|
+
: path.join(projectDir, "scripts/generate-repository.ts");
|
|
48
|
+
|
|
49
|
+
const scriptPath = existsSync(localCli) ? localCli : fallbackScript;
|
|
44
50
|
const cmd = existsSync(localCli)
|
|
45
|
-
? ["bun", scriptPath,
|
|
51
|
+
? ["bun", scriptPath, command, name]
|
|
46
52
|
: ["bun", scriptPath, name];
|
|
47
53
|
if (force) cmd.push("--force");
|
|
48
54
|
|
|
49
|
-
log.info(`Generating module "${name}" in ${projectDir}`);
|
|
50
|
-
|
|
51
55
|
const proc = Bun.spawn(cmd, {
|
|
52
56
|
cwd: projectDir,
|
|
53
57
|
stdout: "inherit",
|
|
@@ -60,6 +64,39 @@ export async function runModuleCommand(argv: string[]): Promise<void> {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
export async function runModuleCommand(argv: string[]): Promise<void> {
|
|
68
|
+
const force = argv.includes("--force") || argv.includes("-f");
|
|
69
|
+
const name = argv.find((arg) => !arg.startsWith("-"));
|
|
70
|
+
|
|
71
|
+
if (!name) {
|
|
72
|
+
throw new Error("Usage: kavoru module <module-name> [--force]");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
76
|
+
log.info(`Generating module "${name}" in ${projectDir}`);
|
|
77
|
+
await runProjectScript(projectDir, "module", name, force);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runRepositoryCommand(argv: string[]): Promise<void> {
|
|
81
|
+
const force = argv.includes("--force") || argv.includes("-f");
|
|
82
|
+
const name = argv.find((arg) => !arg.startsWith("-"));
|
|
83
|
+
|
|
84
|
+
if (!name) {
|
|
85
|
+
throw new Error("Usage: kavoru repository <repository-name> [--force]");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const projectDir = findProjectRoot(process.cwd());
|
|
89
|
+
const prismaConfig = path.join(projectDir, "prisma.config.ts");
|
|
90
|
+
if (!existsSync(prismaConfig)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"PostgreSQL/Prisma is not enabled in this project. Scaffold with the postgres feature first.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
log.info(`Generating repository "${name}" in ${projectDir}`);
|
|
97
|
+
await runProjectScript(projectDir, "repository", name, force);
|
|
98
|
+
}
|
|
99
|
+
|
|
63
100
|
export function printModuleHelp(): void {
|
|
64
101
|
console.log(`\
|
|
65
102
|
Usage: kavoru module <module-name> [options]
|
|
@@ -77,3 +114,23 @@ Examples:
|
|
|
77
114
|
kavoru module user-profile --force
|
|
78
115
|
`);
|
|
79
116
|
}
|
|
117
|
+
|
|
118
|
+
export function printRepositoryHelp(): void {
|
|
119
|
+
console.log(`\
|
|
120
|
+
Usage: kavoru repository <repository-name> [options]
|
|
121
|
+
|
|
122
|
+
Generate Prisma model + repository (requires postgres/prisma feature):
|
|
123
|
+
src/infra/prisma/schemas/<name>.prisma
|
|
124
|
+
src/infra/prisma/repositories/<name>.ts
|
|
125
|
+
|
|
126
|
+
Runs bunx prisma generate when finished.
|
|
127
|
+
|
|
128
|
+
Options:
|
|
129
|
+
-f, --force Overwrite existing schema/repository files
|
|
130
|
+
-h, --help Show help
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
kavoru repository user
|
|
134
|
+
kavoru repository billing --force
|
|
135
|
+
`);
|
|
136
|
+
}
|