kavoru 0.8.12 → 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/cli.ts +4 -2
- package/src/features.ts +120 -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/cli.ts
CHANGED
|
@@ -155,10 +155,12 @@ export async function runCli(options: CliOptions): Promise<void> {
|
|
|
155
155
|
console.log(" bun install");
|
|
156
156
|
}
|
|
157
157
|
if (featureSelection.cli) {
|
|
158
|
+
console.log(" ./kavoru module <name> # Git Bash / macOS / Linux");
|
|
159
|
+
console.log(" .\\kavoru.cmd module <name> # Windows cmd / PowerShell");
|
|
158
160
|
if (!options.install) {
|
|
159
|
-
console.log(" bun run link-cli
|
|
161
|
+
console.log(" bun run link-cli # bare kavoru on PATH (~/.bun/bin)");
|
|
160
162
|
}
|
|
161
|
-
console.log(" kavoru module <name>
|
|
163
|
+
console.log(" bunx kavoru@latest module <name> # works without PATH setup");
|
|
162
164
|
}
|
|
163
165
|
console.log(" bunx kavoru@latest <dir> # scaffold another project");
|
|
164
166
|
console.log(" bun run dev");
|
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
|
}
|
|
@@ -498,8 +533,13 @@ async function patchPackageJson(
|
|
|
498
533
|
|
|
499
534
|
if (!selection.cli) {
|
|
500
535
|
delete pkg.bin;
|
|
536
|
+
if (pkg.scripts?.postinstall === "bun scripts/link-cli.ts") {
|
|
537
|
+
delete pkg.scripts.postinstall;
|
|
538
|
+
}
|
|
501
539
|
} else {
|
|
502
540
|
pkg.bin = { kavoru: "./bin/kavoru.js" };
|
|
541
|
+
pkg.scripts ??= {};
|
|
542
|
+
pkg.scripts.postinstall = "bun scripts/link-cli.ts";
|
|
503
543
|
}
|
|
504
544
|
|
|
505
545
|
await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
@@ -561,6 +601,19 @@ export function buildEnvExample(
|
|
|
561
601
|
);
|
|
562
602
|
}
|
|
563
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
|
+
|
|
564
617
|
if (selection.resend) {
|
|
565
618
|
lines.push(
|
|
566
619
|
"# Resend (disabled when RESEND_API_KEY is unset; always disabled in test)",
|
|
@@ -613,9 +666,25 @@ async function patchDockerfile(
|
|
|
613
666
|
);
|
|
614
667
|
}
|
|
615
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
|
+
|
|
616
678
|
await writeText(projectDir, relativePath, content);
|
|
617
679
|
}
|
|
618
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
|
+
|
|
619
688
|
const DOCKER_KAFKA_ENV = `# KRaft broker config (Confluent cp-kafka 7.6.1)
|
|
620
689
|
CLUSTER_ID=MkU3OEVBNTcwNTJENDM2Qk
|
|
621
690
|
KAFKA_NODE_ID=0
|
|
@@ -660,6 +729,12 @@ function buildDockerAppEnv(
|
|
|
660
729
|
if (selection.kafka) {
|
|
661
730
|
lines.push("KAFKA_BROKERS=kafka:9092");
|
|
662
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
|
+
}
|
|
663
738
|
if (selection.otel) {
|
|
664
739
|
lines.push("OTEL_EXPORTER_OTLP_ENDPOINT=http://otel:4318/v1/traces");
|
|
665
740
|
}
|
|
@@ -679,6 +754,10 @@ function buildAppDependsOn(selection: FeatureSelection): string {
|
|
|
679
754
|
deps.push(` kafka:
|
|
680
755
|
condition: service_started`);
|
|
681
756
|
}
|
|
757
|
+
if (selection.redis) {
|
|
758
|
+
deps.push(` redis:
|
|
759
|
+
condition: service_healthy`);
|
|
760
|
+
}
|
|
682
761
|
if (deps.length === 0) return "";
|
|
683
762
|
return ` depends_on:
|
|
684
763
|
${deps.join("\n")}
|
|
@@ -727,6 +806,31 @@ function generateDockerCompose(selection: FeatureSelection): string {
|
|
|
727
806
|
`
|
|
728
807
|
: "";
|
|
729
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
|
+
|
|
730
834
|
const otelService = selection.otel
|
|
731
835
|
? `
|
|
732
836
|
otel:
|
|
@@ -787,7 +891,7 @@ ${appDependsOn} healthcheck:
|
|
|
787
891
|
timeout: 300s
|
|
788
892
|
retries: 1
|
|
789
893
|
start_period: 90s
|
|
790
|
-
${postgresService}${kafkaService}${otelService}${spotlightService}
|
|
894
|
+
${postgresService}${kafkaService}${redisService}${otelService}${spotlightService}
|
|
791
895
|
networks:
|
|
792
896
|
app_network:
|
|
793
897
|
driver: bridge
|
|
@@ -810,6 +914,9 @@ async function patchDockerCompose(
|
|
|
810
914
|
if (!selection.kafka) {
|
|
811
915
|
await removePaths(projectDir, ["docker/kafka"]);
|
|
812
916
|
}
|
|
917
|
+
if (!selection.redis) {
|
|
918
|
+
await removePaths(projectDir, ["docker/redis"]);
|
|
919
|
+
}
|
|
813
920
|
if (!selection.otel) {
|
|
814
921
|
await removePaths(projectDir, ["docker/otel"]);
|
|
815
922
|
}
|
|
@@ -832,6 +939,13 @@ async function patchDockerCompose(
|
|
|
832
939
|
if (selection.kafka) {
|
|
833
940
|
await writeText(projectDir, "docker/kafka/.env", DOCKER_KAFKA_ENV);
|
|
834
941
|
}
|
|
942
|
+
if (selection.redis) {
|
|
943
|
+
await writeText(
|
|
944
|
+
projectDir,
|
|
945
|
+
"docker/redis/.env",
|
|
946
|
+
buildDockerRedisEnv(packageName),
|
|
947
|
+
);
|
|
948
|
+
}
|
|
835
949
|
if (selection.otel) {
|
|
836
950
|
await writeText(projectDir, "docker/otel/.env", DOCKER_OTEL_ENV);
|
|
837
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
|
+
}
|