kavoru 0.2.0 → 0.4.0
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 +8 -1
- package/package.json +1 -1
- package/src/constants.ts +3 -1
- package/src/features.ts +24 -14
- package/src/prompts.ts +152 -45
package/README.md
CHANGED
|
@@ -14,6 +14,13 @@ bun run dev
|
|
|
14
14
|
|
|
15
15
|
Equivalent to `bunx --bun kavoru` (Bun runs the `kavoru` binary from the npm package).
|
|
16
16
|
|
|
17
|
+
**Stale UI after upgrade?** Bun caches `bunx` installs under `%TEMP%\bunx-*-kavoru@latest` and does not auto-refresh when a new version is published. Clear it or pin a version:
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
Remove-Item -Recurse -Force "$env:TEMP\bunx-*-kavoru*"
|
|
21
|
+
bunx kavoru@0.3.0 my-api
|
|
22
|
+
```
|
|
23
|
+
|
|
17
24
|
### Options
|
|
18
25
|
|
|
19
26
|
| Flag | Description |
|
|
@@ -44,7 +51,7 @@ During setup you can pick which integrations to scaffold. Core is always include
|
|
|
44
51
|
| `cron` | Cron jobs |
|
|
45
52
|
| `docker` | Dockerfile + Compose |
|
|
46
53
|
|
|
47
|
-
Interactive mode (TTY) shows a
|
|
54
|
+
Interactive mode (TTY) shows a checkbox menu (↑↓ move, Space toggle, Enter confirm). Non-interactive runs use the full stack unless you pass flags.
|
|
48
55
|
|
|
49
56
|
### Examples
|
|
50
57
|
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
package/src/features.ts
CHANGED
|
@@ -504,9 +504,14 @@ async function patchDockerfile(projectDir: string, selection: FeatureSelection)
|
|
|
504
504
|
let content = current;
|
|
505
505
|
if (!selection.prisma) {
|
|
506
506
|
content = content.replace(/^\s*COPY prisma\.config\.ts \.\/.*\n/m, "");
|
|
507
|
-
content = content.replace(
|
|
508
|
-
|
|
509
|
-
|
|
507
|
+
content = content.replace(
|
|
508
|
+
/^\s*# generate only needs the schema on disk, not a live database\n/m,
|
|
509
|
+
"",
|
|
510
|
+
);
|
|
511
|
+
content = content.replace(
|
|
512
|
+
/^\s*RUN if \[ -f src\/infra\/prisma\/schemas\/schema\.prisma \]; then bunx prisma generate; fi\n/m,
|
|
513
|
+
"",
|
|
514
|
+
);
|
|
510
515
|
}
|
|
511
516
|
|
|
512
517
|
await writeText(projectDir, relativePath, content);
|
|
@@ -525,19 +530,24 @@ function generateDockerCompose(selection: FeatureSelection): string {
|
|
|
525
530
|
const kafkaService = selection.kafka
|
|
526
531
|
? `
|
|
527
532
|
kafka:
|
|
528
|
-
image:
|
|
533
|
+
image: confluentinc/cp-kafka:7.6.1
|
|
534
|
+
hostname: kafka
|
|
529
535
|
ports:
|
|
530
536
|
- "9094:9094"
|
|
531
537
|
environment:
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
538
|
+
CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
|
|
539
|
+
KAFKA_NODE_ID: "0"
|
|
540
|
+
KAFKA_PROCESS_ROLES: broker,controller
|
|
541
|
+
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
|
|
542
|
+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094
|
|
543
|
+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
|
|
544
|
+
KAFKA_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
|
|
545
|
+
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
|
546
|
+
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
|
547
|
+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
548
|
+
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
|
549
|
+
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
|
550
|
+
KAFKA_LOG_DIRS: /tmp/kraft-combined-logs
|
|
541
551
|
networks:
|
|
542
552
|
- app_network
|
|
543
553
|
restart: unless-stopped
|
|
@@ -547,7 +557,7 @@ function generateDockerCompose(selection: FeatureSelection): string {
|
|
|
547
557
|
const jaegerService = selection.otel
|
|
548
558
|
? `
|
|
549
559
|
jaeger:
|
|
550
|
-
image: jaegertracing/all-in-one:1.62
|
|
560
|
+
image: jaegertracing/all-in-one:1.62.0
|
|
551
561
|
ports:
|
|
552
562
|
- "16686:16686"
|
|
553
563
|
- "4318:4318"
|
package/src/prompts.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
1
|
+
import { stdin, stdout } from "node:process";
|
|
3
2
|
import {
|
|
4
3
|
ALL_FEATURES,
|
|
5
4
|
FEATURES,
|
|
@@ -9,70 +8,178 @@ import {
|
|
|
9
8
|
formatFeatureSelection,
|
|
10
9
|
} from "./features";
|
|
11
10
|
|
|
11
|
+
const ESC = "\x1b";
|
|
12
|
+
const dim = `${ESC}[2m`;
|
|
13
|
+
const reset = `${ESC}[0m`;
|
|
14
|
+
const cyan = `${ESC}[36m`;
|
|
15
|
+
|
|
12
16
|
function cloneSelection(selection: FeatureSelection): FeatureSelection {
|
|
13
17
|
return { ...selection };
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
type KeyAction =
|
|
21
|
+
| "up"
|
|
22
|
+
| "down"
|
|
23
|
+
| "toggle"
|
|
24
|
+
| "confirm"
|
|
25
|
+
| "all"
|
|
26
|
+
| "minimal"
|
|
27
|
+
| "interrupt";
|
|
28
|
+
|
|
29
|
+
const KEY_UP = ESC + "[A";
|
|
30
|
+
const KEY_DOWN = ESC + "[B";
|
|
31
|
+
const KEY_UP_ALT = ESC + "OA";
|
|
32
|
+
const KEY_DOWN_ALT = ESC + "OB";
|
|
33
|
+
|
|
34
|
+
function parseKeyInput(data: string): KeyAction | null {
|
|
35
|
+
if (data === "\u0003") return "interrupt";
|
|
36
|
+
if (data === "\r" || data === "\n") return "confirm";
|
|
37
|
+
if (data === " ") return "toggle";
|
|
38
|
+
if (data === "a" || data === "A") return "all";
|
|
39
|
+
if (data === "m" || data === "M") return "minimal";
|
|
40
|
+
if (data === KEY_UP || data === KEY_UP_ALT) return "up";
|
|
41
|
+
if (data === KEY_DOWN || data === KEY_DOWN_ALT) return "down";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createKeyReader(onKey: (action: KeyAction) => void) {
|
|
46
|
+
let pending = "";
|
|
47
|
+
|
|
48
|
+
const onData = (chunk: string) => {
|
|
49
|
+
pending += chunk;
|
|
50
|
+
|
|
51
|
+
while (pending.length > 0) {
|
|
52
|
+
if (pending === ESC) return;
|
|
53
|
+
|
|
54
|
+
if (pending.startsWith(ESC)) {
|
|
55
|
+
if (pending.length < 3) return;
|
|
56
|
+
|
|
57
|
+
const action = parseKeyInput(pending.slice(0, 3));
|
|
58
|
+
if (action) {
|
|
59
|
+
pending = pending.slice(3);
|
|
60
|
+
onKey(action);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (pending.startsWith(ESC + "O") && pending.length < 3) return;
|
|
65
|
+
|
|
66
|
+
pending = pending.slice(1);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const action = parseKeyInput(pending[0] ?? "");
|
|
71
|
+
pending = pending.slice(1);
|
|
72
|
+
if (action) onKey(action);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return onData;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderCheckboxMenu(
|
|
80
|
+
selection: FeatureSelection,
|
|
81
|
+
activeIndex: number,
|
|
82
|
+
lineCount: number,
|
|
83
|
+
): number {
|
|
84
|
+
const lines: string[] = [
|
|
85
|
+
`${cyan}◆${reset} Select optional features ${dim}(↑↓ move · Space toggle · Enter confirm)${reset}`,
|
|
86
|
+
`${dim} a = all · m = minimal${reset}`,
|
|
87
|
+
"",
|
|
88
|
+
];
|
|
23
89
|
|
|
24
90
|
FEATURES.forEach((feature, index) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
91
|
+
const isActive = index === activeIndex;
|
|
92
|
+
const pointer = isActive ? `${cyan}❯${reset}` : " ";
|
|
93
|
+
const mark = selection[feature.id] ? "x" : " ";
|
|
94
|
+
const label = isActive ? `${cyan}${feature.label}${reset}` : feature.label;
|
|
95
|
+
lines.push(
|
|
96
|
+
` ${pointer} [${mark}] ${label.padEnd(22)} ${dim}${feature.description}${reset}`,
|
|
28
97
|
);
|
|
29
98
|
});
|
|
30
99
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
100
|
+
lines.push("", ` ${dim}Selected: ${formatFeatureSelection(selection)}${reset}`);
|
|
101
|
+
|
|
102
|
+
if (lineCount > 0) {
|
|
103
|
+
stdout.write(`${ESC}[${lineCount}A`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
stdout.write(`${ESC}[2K${ESC}[0G${line}\n`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return lines.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function restoreTerminal(onData: (chunk: string) => void) {
|
|
114
|
+
stdin.off("data", onData);
|
|
115
|
+
if (stdin.isTTY) {
|
|
116
|
+
stdin.setRawMode(false);
|
|
117
|
+
}
|
|
118
|
+
stdin.pause();
|
|
119
|
+
stdout.write(`${ESC}[?25h`);
|
|
34
120
|
}
|
|
35
121
|
|
|
36
122
|
export async function promptFeatureSelection(
|
|
37
123
|
initial: FeatureSelection = ALL_FEATURES,
|
|
38
124
|
): Promise<FeatureSelection> {
|
|
39
|
-
if (!
|
|
125
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
40
126
|
return cloneSelection(initial);
|
|
41
127
|
}
|
|
42
128
|
|
|
43
129
|
const selection = cloneSelection(initial);
|
|
44
|
-
|
|
130
|
+
let activeIndex = 0;
|
|
131
|
+
let lineCount = 0;
|
|
45
132
|
|
|
46
|
-
|
|
47
|
-
while (true) {
|
|
48
|
-
printFeatureMenu(selection);
|
|
49
|
-
const answer = (await rl.question("Toggle feature: ")).trim().toLowerCase();
|
|
133
|
+
stdout.write(`${ESC}[?25l`);
|
|
50
134
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Object.assign(selection, ALL_FEATURES);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
135
|
+
if (stdin.isTTY) {
|
|
136
|
+
stdin.setRawMode(true);
|
|
137
|
+
}
|
|
138
|
+
stdin.resume();
|
|
139
|
+
stdin.setEncoding("utf8");
|
|
59
140
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const onKey = (action: KeyAction) => {
|
|
143
|
+
switch (action) {
|
|
144
|
+
case "up":
|
|
145
|
+
activeIndex =
|
|
146
|
+
(activeIndex - 1 + FEATURES.length) % FEATURES.length;
|
|
147
|
+
lineCount = renderCheckboxMenu(selection, activeIndex, lineCount);
|
|
148
|
+
break;
|
|
149
|
+
case "down":
|
|
150
|
+
activeIndex = (activeIndex + 1) % FEATURES.length;
|
|
151
|
+
lineCount = renderCheckboxMenu(selection, activeIndex, lineCount);
|
|
152
|
+
break;
|
|
153
|
+
case "toggle": {
|
|
154
|
+
const feature = FEATURES[activeIndex];
|
|
155
|
+
if (!feature) break;
|
|
156
|
+
selection[feature.id as FeatureId] = !selection[feature.id as FeatureId];
|
|
157
|
+
lineCount = renderCheckboxMenu(selection, activeIndex, lineCount);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "all":
|
|
161
|
+
Object.assign(selection, ALL_FEATURES);
|
|
162
|
+
lineCount = renderCheckboxMenu(selection, activeIndex, lineCount);
|
|
163
|
+
break;
|
|
164
|
+
case "minimal":
|
|
165
|
+
Object.assign(selection, MINIMAL_FEATURES);
|
|
166
|
+
lineCount = renderCheckboxMenu(selection, activeIndex, lineCount);
|
|
167
|
+
break;
|
|
168
|
+
case "confirm":
|
|
169
|
+
restoreTerminal(onData);
|
|
170
|
+
stdout.write("\n");
|
|
171
|
+
resolve(selection);
|
|
172
|
+
break;
|
|
173
|
+
case "interrupt":
|
|
174
|
+
restoreTerminal(onData);
|
|
175
|
+
stdout.write("\n");
|
|
176
|
+
reject(new Error("Feature selection cancelled."));
|
|
177
|
+
break;
|
|
63
178
|
}
|
|
179
|
+
};
|
|
64
180
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const feature = FEATURES[index - 1];
|
|
72
|
-
if (!feature) continue;
|
|
73
|
-
selection[feature.id as FeatureId] = !selection[feature.id as FeatureId];
|
|
74
|
-
}
|
|
75
|
-
} finally {
|
|
76
|
-
rl.close();
|
|
77
|
-
}
|
|
181
|
+
const onData = createKeyReader(onKey);
|
|
182
|
+
stdin.on("data", onData);
|
|
183
|
+
lineCount = renderCheckboxMenu(selection, activeIndex, 0);
|
|
184
|
+
});
|
|
78
185
|
}
|