kavoru 0.8.8 → 0.8.9
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 +2 -2
- package/package.json +1 -1
- package/src/args.ts +3 -2
- package/src/cli.ts +3 -2
- package/src/features.ts +167 -40
- 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|
|
|
@@ -71,7 +71,7 @@ bunx kavoru@latest .
|
|
|
71
71
|
bunx kavoru@latest my-api --minimal
|
|
72
72
|
|
|
73
73
|
# Pick specific features
|
|
74
|
-
bunx kavoru@latest my-api --features auth,
|
|
74
|
+
bunx kavoru@latest my-api --features auth,postgres,otel,sentry
|
|
75
75
|
|
|
76
76
|
# Full stack minus Kafka and Docker
|
|
77
77
|
bunx kavoru@latest my-api --no-features kafka,docker
|
package/package.json
CHANGED
package/src/args.ts
CHANGED
|
@@ -33,12 +33,13 @@ Options:
|
|
|
33
33
|
--no-features <list> Comma-separated features to exclude
|
|
34
34
|
|
|
35
35
|
Features:
|
|
36
|
-
auth,
|
|
36
|
+
auth, postgres, otel, sentry, kafka, websocket, resend, cron, docker
|
|
37
|
+
(prisma is accepted as an alias for postgres)
|
|
37
38
|
|
|
38
39
|
Examples:
|
|
39
40
|
bunx kavoru@latest my-api
|
|
40
41
|
bunx kavoru@latest my-api --minimal
|
|
41
|
-
bunx kavoru@latest my-api --features auth,
|
|
42
|
+
bunx kavoru@latest my-api --features auth,postgres,otel
|
|
42
43
|
bunx kavoru@latest my-api --no-features kafka,docker,resend
|
|
43
44
|
`;
|
|
44
45
|
|
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,7 +4,7 @@ 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"
|
|
@@ -13,6 +13,10 @@ export type FeatureId =
|
|
|
13
13
|
| "cron"
|
|
14
14
|
| "docker";
|
|
15
15
|
|
|
16
|
+
const FEATURE_ALIASES: Record<string, FeatureId> = {
|
|
17
|
+
prisma: "postgres",
|
|
18
|
+
};
|
|
19
|
+
|
|
16
20
|
export type FeatureSelection = Record<FeatureId, boolean>;
|
|
17
21
|
|
|
18
22
|
export type FeatureDef = {
|
|
@@ -28,9 +32,9 @@ export const FEATURES: FeatureDef[] = [
|
|
|
28
32
|
description: "Bearer auth, sign-in route, protected routes",
|
|
29
33
|
},
|
|
30
34
|
{
|
|
31
|
-
id: "
|
|
32
|
-
label: "
|
|
33
|
-
description: "Prisma 7
|
|
35
|
+
id: "postgres",
|
|
36
|
+
label: "PostgreSQL",
|
|
37
|
+
description: "Docker Postgres, Prisma 7, migrations, and seed",
|
|
34
38
|
},
|
|
35
39
|
{
|
|
36
40
|
id: "otel",
|
|
@@ -88,7 +92,7 @@ const FEATURE_PATHS: Record<FeatureId, string[]> = {
|
|
|
88
92
|
"src/constants/jwt.ts",
|
|
89
93
|
"src/models/schemas/signin.ts",
|
|
90
94
|
],
|
|
91
|
-
|
|
95
|
+
postgres: ["prisma.config.ts", "src/infra/prisma"],
|
|
92
96
|
otel: ["src/infra/telemetry"],
|
|
93
97
|
sentry: [
|
|
94
98
|
"src/infra/sentry",
|
|
@@ -115,7 +119,7 @@ const FEATURE_DEPENDENCIES: Partial<
|
|
|
115
119
|
Record<FeatureId, { dependencies?: string[]; devDependencies?: string[] }>
|
|
116
120
|
> = {
|
|
117
121
|
auth: { dependencies: ["@elysiajs/bearer", "@elysiajs/jwt"] },
|
|
118
|
-
|
|
122
|
+
postgres: {
|
|
119
123
|
dependencies: ["@prisma/adapter-pg", "@prisma/client"],
|
|
120
124
|
devDependencies: ["prisma"],
|
|
121
125
|
},
|
|
@@ -136,9 +140,42 @@ const FEATURE_DEPENDENCIES: Partial<
|
|
|
136
140
|
const FEATURE_SCRIPTS: Partial<Record<FeatureId, string[]>> = {
|
|
137
141
|
otel: ["otel:view", "otel:tui"],
|
|
138
142
|
sentry: ["sentry:spotlight"],
|
|
139
|
-
|
|
143
|
+
postgres: ["seed"],
|
|
140
144
|
};
|
|
141
145
|
|
|
146
|
+
function resolveFeatureId(raw: string): FeatureId | null {
|
|
147
|
+
const id = FEATURE_ALIASES[raw] ?? raw;
|
|
148
|
+
return FEATURE_IDS.includes(id as FeatureId) ? (id as FeatureId) : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function toPostgresName(packageName: string): string {
|
|
152
|
+
const normalized = packageName
|
|
153
|
+
.replace(/-/g, "_")
|
|
154
|
+
.replace(/[^a-z0-9_]/gi, "_")
|
|
155
|
+
.replace(/_+/g, "_")
|
|
156
|
+
.replace(/^_|_$/g, "");
|
|
157
|
+
return normalized || "app";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function buildDatabaseUrl(
|
|
161
|
+
packageName: string,
|
|
162
|
+
host: string,
|
|
163
|
+
port = 5432,
|
|
164
|
+
): string {
|
|
165
|
+
const name = toPostgresName(packageName);
|
|
166
|
+
return `postgresql://${name}:${name}@${host}:${port}/${name}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function normalizeFeatureSelection(
|
|
170
|
+
selection: FeatureSelection,
|
|
171
|
+
): FeatureSelection {
|
|
172
|
+
const next = { ...selection };
|
|
173
|
+
if (next.postgres) {
|
|
174
|
+
next.docker = true;
|
|
175
|
+
}
|
|
176
|
+
return next;
|
|
177
|
+
}
|
|
178
|
+
|
|
142
179
|
function enabledFeatures(selection: FeatureSelection): FeatureId[] {
|
|
143
180
|
return FEATURE_IDS.filter((id) => selection[id]);
|
|
144
181
|
}
|
|
@@ -159,9 +196,7 @@ export function parseFeatureIncludeList(input: string): FeatureSelection {
|
|
|
159
196
|
.map((part) => part.trim())
|
|
160
197
|
.filter(Boolean);
|
|
161
198
|
|
|
162
|
-
const unknown = requested.filter(
|
|
163
|
-
(id) => !FEATURE_IDS.includes(id as FeatureId),
|
|
164
|
-
);
|
|
199
|
+
const unknown = requested.filter((part) => resolveFeatureId(part) === null);
|
|
165
200
|
if (unknown.length > 0) {
|
|
166
201
|
throw new Error(
|
|
167
202
|
`Unknown feature(s): ${unknown.join(", ")}. Valid: ${FEATURE_IDS.join(", ")}`,
|
|
@@ -169,8 +204,12 @@ export function parseFeatureIncludeList(input: string): FeatureSelection {
|
|
|
169
204
|
}
|
|
170
205
|
|
|
171
206
|
const selection = { ...MINIMAL_FEATURES };
|
|
172
|
-
for (const
|
|
173
|
-
|
|
207
|
+
for (const part of requested) {
|
|
208
|
+
const id = resolveFeatureId(part);
|
|
209
|
+
if (id) selection[id] = true;
|
|
210
|
+
}
|
|
211
|
+
if (selection.postgres) {
|
|
212
|
+
selection.docker = true;
|
|
174
213
|
}
|
|
175
214
|
return selection;
|
|
176
215
|
}
|
|
@@ -183,13 +222,14 @@ export function parseFeatureExcludeList(
|
|
|
183
222
|
const unknown: string[] = [];
|
|
184
223
|
|
|
185
224
|
for (const raw of excluded) {
|
|
186
|
-
const
|
|
187
|
-
if (!
|
|
188
|
-
|
|
189
|
-
|
|
225
|
+
const part = raw.trim().toLowerCase();
|
|
226
|
+
if (!part) continue;
|
|
227
|
+
const id = resolveFeatureId(part);
|
|
228
|
+
if (!id) {
|
|
229
|
+
unknown.push(part);
|
|
190
230
|
continue;
|
|
191
231
|
}
|
|
192
|
-
selection[id
|
|
232
|
+
selection[id] = false;
|
|
193
233
|
}
|
|
194
234
|
|
|
195
235
|
if (unknown.length > 0) {
|
|
@@ -198,6 +238,10 @@ export function parseFeatureExcludeList(
|
|
|
198
238
|
);
|
|
199
239
|
}
|
|
200
240
|
|
|
241
|
+
if (!selection.docker) {
|
|
242
|
+
selection.postgres = false;
|
|
243
|
+
}
|
|
244
|
+
|
|
201
245
|
return selection;
|
|
202
246
|
}
|
|
203
247
|
|
|
@@ -349,11 +393,17 @@ export function buildEntryIndex(selection: FeatureSelection): string {
|
|
|
349
393
|
return [...imports, ...body].join("\n");
|
|
350
394
|
}
|
|
351
395
|
|
|
352
|
-
async function patchEntryIndex(
|
|
396
|
+
async function patchEntryIndex(
|
|
397
|
+
projectDir: string,
|
|
398
|
+
selection: FeatureSelection,
|
|
399
|
+
) {
|
|
353
400
|
await writeText(projectDir, "src/index.ts", buildEntryIndex(selection));
|
|
354
401
|
}
|
|
355
402
|
|
|
356
|
-
async function patchServerIndex(
|
|
403
|
+
async function patchServerIndex(
|
|
404
|
+
projectDir: string,
|
|
405
|
+
selection: FeatureSelection,
|
|
406
|
+
) {
|
|
357
407
|
const relativePath = "src/server/index.ts";
|
|
358
408
|
const current = await readText(projectDir, relativePath);
|
|
359
409
|
if (!current) return;
|
|
@@ -422,7 +472,7 @@ async function patchPackageJson(
|
|
|
422
472
|
}
|
|
423
473
|
}
|
|
424
474
|
|
|
425
|
-
if (!selection.
|
|
475
|
+
if (!selection.postgres && pkg.scripts) {
|
|
426
476
|
pkg.scripts.start = "bun run src/index.ts";
|
|
427
477
|
}
|
|
428
478
|
|
|
@@ -435,9 +485,11 @@ export function buildEnvExample(
|
|
|
435
485
|
): string {
|
|
436
486
|
const lines = ["NODE_ENV=development", "PORT=3131", ""];
|
|
437
487
|
|
|
438
|
-
if (selection.
|
|
488
|
+
if (selection.postgres) {
|
|
439
489
|
lines.push(
|
|
440
|
-
"
|
|
490
|
+
"# Start database: docker compose up -d postgres",
|
|
491
|
+
"# Host dev uses published port; Docker app overrides host in docker/app/.env",
|
|
492
|
+
`DATABASE_URL=${buildDatabaseUrl(packageName, "localhost")}`,
|
|
441
493
|
"",
|
|
442
494
|
);
|
|
443
495
|
}
|
|
@@ -507,7 +559,10 @@ async function patchEnvExample(
|
|
|
507
559
|
await writeText(projectDir, ".env", content);
|
|
508
560
|
}
|
|
509
561
|
|
|
510
|
-
async function patchDockerfile(
|
|
562
|
+
async function patchDockerfile(
|
|
563
|
+
projectDir: string,
|
|
564
|
+
selection: FeatureSelection,
|
|
565
|
+
) {
|
|
511
566
|
if (!selection.docker) return;
|
|
512
567
|
|
|
513
568
|
const relativePath = "docker/app/Dockerfile";
|
|
@@ -515,14 +570,19 @@ async function patchDockerfile(projectDir: string, selection: FeatureSelection)
|
|
|
515
570
|
if (!current) return;
|
|
516
571
|
|
|
517
572
|
let content = current;
|
|
518
|
-
if (!selection.
|
|
573
|
+
if (!selection.postgres) {
|
|
519
574
|
content = content.replace(/^\s*COPY prisma\.config\.ts \.\/.*\n/m, "");
|
|
575
|
+
content = content.replace(/^\s*RUN bunx prisma generate\n/m, "");
|
|
576
|
+
content = content.replace(
|
|
577
|
+
/^COPY docker\/app\/docker-entrypoint\.sh .*$\n/m,
|
|
578
|
+
"",
|
|
579
|
+
);
|
|
520
580
|
content = content.replace(
|
|
521
|
-
|
|
581
|
+
/^RUN sed -i 's\/\\r\$\/\/' \/app\/docker-entrypoint\.sh && chmod \+x \/app\/docker-entrypoint\.sh\n/m,
|
|
522
582
|
"",
|
|
523
583
|
);
|
|
524
584
|
content = content.replace(
|
|
525
|
-
|
|
585
|
+
/^ENTRYPOINT \["\/bin\/sh", "\/app\/docker-entrypoint\.sh"\]\n/m,
|
|
526
586
|
"",
|
|
527
587
|
);
|
|
528
588
|
}
|
|
@@ -552,11 +612,25 @@ const DOCKER_OTEL_ENV =
|
|
|
552
612
|
const DOCKER_SPOTLIGHT_ENV =
|
|
553
613
|
"# Official Spotlight image; add overrides here if needed.\n";
|
|
554
614
|
|
|
555
|
-
function
|
|
615
|
+
function buildDockerPostgresEnv(packageName: string): string {
|
|
616
|
+
const name = toPostgresName(packageName);
|
|
617
|
+
return `POSTGRES_USER=${name}
|
|
618
|
+
POSTGRES_PASSWORD=${name}
|
|
619
|
+
POSTGRES_DB=${name}
|
|
620
|
+
`;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function buildDockerAppEnv(
|
|
624
|
+
selection: FeatureSelection,
|
|
625
|
+
packageName: string,
|
|
626
|
+
): string {
|
|
556
627
|
const lines = [
|
|
557
628
|
"# Docker-only overrides (loaded after root .env)",
|
|
558
629
|
"NODE_ENV=production",
|
|
559
630
|
];
|
|
631
|
+
if (selection.postgres) {
|
|
632
|
+
lines.push(`DATABASE_URL=${buildDatabaseUrl(packageName, "postgres")}`);
|
|
633
|
+
}
|
|
560
634
|
if (selection.kafka) {
|
|
561
635
|
lines.push("KAFKA_BROKERS=kafka:9092");
|
|
562
636
|
}
|
|
@@ -569,11 +643,45 @@ function buildDockerAppEnv(selection: FeatureSelection): string {
|
|
|
569
643
|
return `${lines.join("\n")}\n`;
|
|
570
644
|
}
|
|
571
645
|
|
|
646
|
+
function buildAppDependsOn(selection: FeatureSelection): string {
|
|
647
|
+
const deps: string[] = [];
|
|
648
|
+
if (selection.postgres) {
|
|
649
|
+
deps.push(` postgres:
|
|
650
|
+
condition: service_healthy`);
|
|
651
|
+
}
|
|
652
|
+
if (selection.kafka) {
|
|
653
|
+
deps.push(` kafka:
|
|
654
|
+
condition: service_started`);
|
|
655
|
+
}
|
|
656
|
+
if (deps.length === 0) return "";
|
|
657
|
+
return ` depends_on:
|
|
658
|
+
${deps.join("\n")}
|
|
659
|
+
`;
|
|
660
|
+
}
|
|
661
|
+
|
|
572
662
|
function generateDockerCompose(selection: FeatureSelection): string {
|
|
573
|
-
const appDependsOn = selection
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
663
|
+
const appDependsOn = buildAppDependsOn(selection);
|
|
664
|
+
|
|
665
|
+
const postgresService = selection.postgres
|
|
666
|
+
? `
|
|
667
|
+
postgres:
|
|
668
|
+
build:
|
|
669
|
+
context: docker/postgres
|
|
670
|
+
hostname: postgres
|
|
671
|
+
ports:
|
|
672
|
+
- "\${POSTGRES_PORT:-5432}:5432"
|
|
673
|
+
env_file:
|
|
674
|
+
- docker/postgres/.env
|
|
675
|
+
volumes:
|
|
676
|
+
- postgres_data:/var/lib/postgresql/data
|
|
677
|
+
networks:
|
|
678
|
+
- app_network
|
|
679
|
+
healthcheck:
|
|
680
|
+
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
|
681
|
+
interval: 5s
|
|
682
|
+
timeout: 5s
|
|
683
|
+
retries: 5
|
|
684
|
+
restart: unless-stopped
|
|
577
685
|
`
|
|
578
686
|
: "";
|
|
579
687
|
|
|
@@ -631,7 +739,7 @@ function generateDockerCompose(selection: FeatureSelection): string {
|
|
|
631
739
|
target: build
|
|
632
740
|
args:
|
|
633
741
|
PORT: \${PORT:-3131}
|
|
634
|
-
command:
|
|
742
|
+
command: ./server
|
|
635
743
|
volumes:
|
|
636
744
|
- ./src:/app/src
|
|
637
745
|
networks:
|
|
@@ -644,27 +752,35 @@ function generateDockerCompose(selection: FeatureSelection): string {
|
|
|
644
752
|
- "\${PORT:-3131}:\${PORT:-3131}"
|
|
645
753
|
restart: unless-stopped
|
|
646
754
|
env_file:
|
|
647
|
-
- .env
|
|
755
|
+
- path: .env
|
|
756
|
+
required: false
|
|
648
757
|
- docker/app/.env
|
|
649
758
|
${appDependsOn} healthcheck:
|
|
650
|
-
test: ["CMD", "curl", "-f", "http://localhost:\${PORT}/healthz"]
|
|
759
|
+
test: ["CMD", "curl", "-f", "http://localhost:\${PORT:-3131}/healthz"]
|
|
651
760
|
interval: 600s
|
|
652
761
|
timeout: 300s
|
|
653
762
|
retries: 1
|
|
654
|
-
start_period:
|
|
655
|
-
${kafkaService}${otelService}${spotlightService}
|
|
763
|
+
start_period: 90s
|
|
764
|
+
${postgresService}${kafkaService}${otelService}${spotlightService}
|
|
656
765
|
networks:
|
|
657
766
|
app_network:
|
|
658
767
|
driver: bridge
|
|
659
|
-
`;
|
|
768
|
+
${selection.postgres ? "\nvolumes:\n postgres_data:\n" : ""}`;
|
|
660
769
|
}
|
|
661
770
|
|
|
662
771
|
async function patchDockerCompose(
|
|
663
772
|
projectDir: string,
|
|
664
773
|
selection: FeatureSelection,
|
|
774
|
+
packageName: string,
|
|
665
775
|
) {
|
|
666
776
|
if (!selection.docker) return;
|
|
667
777
|
|
|
778
|
+
if (!selection.postgres) {
|
|
779
|
+
await removePaths(projectDir, [
|
|
780
|
+
"docker/postgres",
|
|
781
|
+
"docker/app/docker-entrypoint.sh",
|
|
782
|
+
]);
|
|
783
|
+
}
|
|
668
784
|
if (!selection.kafka) {
|
|
669
785
|
await removePaths(projectDir, ["docker/kafka"]);
|
|
670
786
|
}
|
|
@@ -678,8 +794,15 @@ async function patchDockerCompose(
|
|
|
678
794
|
await writeText(
|
|
679
795
|
projectDir,
|
|
680
796
|
"docker/app/.env",
|
|
681
|
-
buildDockerAppEnv(selection),
|
|
797
|
+
buildDockerAppEnv(selection, packageName),
|
|
682
798
|
);
|
|
799
|
+
if (selection.postgres) {
|
|
800
|
+
await writeText(
|
|
801
|
+
projectDir,
|
|
802
|
+
"docker/postgres/.env",
|
|
803
|
+
buildDockerPostgresEnv(packageName),
|
|
804
|
+
);
|
|
805
|
+
}
|
|
683
806
|
if (selection.kafka) {
|
|
684
807
|
await writeText(projectDir, "docker/kafka/.env", DOCKER_KAFKA_ENV);
|
|
685
808
|
}
|
|
@@ -689,7 +812,11 @@ async function patchDockerCompose(
|
|
|
689
812
|
if (selection.sentry) {
|
|
690
813
|
await writeText(projectDir, "docker/spotlight/.env", DOCKER_SPOTLIGHT_ENV);
|
|
691
814
|
}
|
|
692
|
-
await writeText(
|
|
815
|
+
await writeText(
|
|
816
|
+
projectDir,
|
|
817
|
+
"docker-compose.yaml",
|
|
818
|
+
generateDockerCompose(selection),
|
|
819
|
+
);
|
|
693
820
|
}
|
|
694
821
|
|
|
695
822
|
export async function applyFeatures(
|
|
@@ -710,7 +837,7 @@ export async function applyFeatures(
|
|
|
710
837
|
await patchPackageJson(projectDir, selection);
|
|
711
838
|
await patchEnvExample(projectDir, selection, packageName);
|
|
712
839
|
await patchDockerfile(projectDir, selection);
|
|
713
|
-
await patchDockerCompose(projectDir, selection);
|
|
840
|
+
await patchDockerCompose(projectDir, selection, packageName);
|
|
714
841
|
|
|
715
842
|
if (disabled.length > 0) {
|
|
716
843
|
log.success("Feature selection applied");
|
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);
|