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 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 toggle menu after the project name. Non-interactive runs use the full stack unless you pass flags.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kavoru",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Scaffold a new Kavoru (Elysia + Bun) backend from the official template",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.ts CHANGED
@@ -1,4 +1,6 @@
1
- export const PACKAGE_VERSION = "0.1.0";
1
+ import pkg from "../package.json";
2
+
3
+ export const PACKAGE_VERSION = pkg.version;
2
4
 
3
5
  export const TEMPLATE_REPO = "mertthesamael/Kavoru";
4
6
  export const TEMPLATE_BRANCH = "master";
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(/^\s*ENV DATABASE_URL=\$\{DATABASE_URL\}\n/m, "");
508
- content = content.replace(/^\s*RUN bunx prisma db pull\n/m, "");
509
- content = content.replace(/^\s*RUN bunx prisma generate\n/m, "");
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: bitnami/kafka:3.9
533
+ image: confluentinc/cp-kafka:7.6.1
534
+ hostname: kafka
529
535
  ports:
530
536
  - "9094:9094"
531
537
  environment:
532
- KAFKA_CFG_NODE_ID: "0"
533
- KAFKA_CFG_PROCESS_ROLES: controller,broker
534
- KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094
535
- KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094
536
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT
537
- KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
538
- KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
539
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME: PLAINTEXT
540
- ALLOW_PLAINTEXT_LISTENER: "yes"
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 * as readline from "node:readline/promises";
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
- function printFeatureMenu(selection: FeatureSelection) {
17
- console.log();
18
- console.log("Select optional features for your project:");
19
- console.log(
20
- " Type a number to toggle · a = all · m = minimal · Enter = continue",
21
- );
22
- console.log();
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 checked = selection[feature.id] ? "x" : " ";
26
- console.log(
27
- ` [${checked}] ${index + 1}. ${feature.label.padEnd(22)} ${feature.description}`,
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
- console.log();
32
- console.log(` Selected: ${formatFeatureSelection(selection)}`);
33
- console.log();
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 (!process.stdin.isTTY || !process.stdout.isTTY) {
125
+ if (!stdin.isTTY || !stdout.isTTY) {
40
126
  return cloneSelection(initial);
41
127
  }
42
128
 
43
129
  const selection = cloneSelection(initial);
44
- const rl = readline.createInterface({ input, output });
130
+ let activeIndex = 0;
131
+ let lineCount = 0;
45
132
 
46
- try {
47
- while (true) {
48
- printFeatureMenu(selection);
49
- const answer = (await rl.question("Toggle feature: ")).trim().toLowerCase();
133
+ stdout.write(`${ESC}[?25l`);
50
134
 
51
- if (!answer) {
52
- return selection;
53
- }
54
-
55
- if (answer === "a" || answer === "all") {
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
- if (answer === "m" || answer === "minimal") {
61
- Object.assign(selection, MINIMAL_FEATURES);
62
- continue;
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
- const index = Number.parseInt(answer, 10);
66
- if (Number.isNaN(index) || index < 1 || index > FEATURES.length) {
67
- console.log("Enter a number between 1 and 9, a, m, or press Enter.");
68
- continue;
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
  }