kavoru 0.2.0 → 0.3.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 +1 -1
- package/package.json +1 -1
- package/src/prompts.ts +152 -45
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ During setup you can pick which integrations to scaffold. Core is always include
|
|
|
44
44
|
| `cron` | Cron jobs |
|
|
45
45
|
| `docker` | Dockerfile + Compose |
|
|
46
46
|
|
|
47
|
-
Interactive mode (TTY) shows a
|
|
47
|
+
Interactive mode (TTY) shows a checkbox menu (↑↓ move, Space toggle, Enter confirm). Non-interactive runs use the full stack unless you pass flags.
|
|
48
48
|
|
|
49
49
|
### Examples
|
|
50
50
|
|
package/package.json
CHANGED
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
|
}
|