@dastyaresocial/ds-cs 0.2.0 → 0.2.1
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 +18 -5
- package/package.json +1 -1
- package/src/setup.mjs +2 -2
- package/src/utils/prompt.mjs +195 -118
package/README.md
CHANGED
|
@@ -23,20 +23,24 @@ Run it with no flags and `ds-cs` asks, in this order:
|
|
|
23
23
|
Global (every project) your user directory — OpenCode, Claude Code
|
|
24
24
|
|
|
25
25
|
? Which agents should read these skills?
|
|
26
|
-
type to filter · space
|
|
26
|
+
type to filter · space toggle · a all · enter confirm
|
|
27
|
+
❯ ◉ Select all
|
|
27
28
|
◉ OpenCode .agents/skills
|
|
28
29
|
◉ Cursor .agents/skills
|
|
29
30
|
◉ Codex .agents/skills
|
|
30
31
|
◉ Claude Code .claude/skills
|
|
31
32
|
○ Windsurf .windsurf/skills
|
|
32
|
-
…
|
|
33
|
+
… 8 more
|
|
34
|
+
|
|
35
|
+
(15 selected: opencode, claude-code, cursor, codex, …)
|
|
33
36
|
|
|
34
37
|
? How should the files be shared between agents?
|
|
35
38
|
❯ Symlink one copy, updates land everywhere
|
|
36
39
|
Copy independent files per agent
|
|
37
40
|
|
|
38
41
|
? Which skills would you like to install?
|
|
39
|
-
type to filter · space
|
|
42
|
+
type to filter · space toggle · a all · enter install
|
|
43
|
+
❯ ◉ Select all
|
|
40
44
|
○ add-component skills/dev/add-component/
|
|
41
45
|
○ add-orpc-endpoint skills/dev/add-orpc-endpoint/
|
|
42
46
|
…
|
|
@@ -45,6 +49,10 @@ Run it with no flags and `ds-cs` asks, in this order:
|
|
|
45
49
|
Scope first, because it decides which directories exist; then which agents read
|
|
46
50
|
the skills; then how those agents share one copy; then what to install.
|
|
47
51
|
|
|
52
|
+
Both multi-selects open on a **Select all** row — space on it, or `a` anywhere,
|
|
53
|
+
takes everything; pressing it again clears the selection. That is what you want
|
|
54
|
+
on a first run and never again.
|
|
55
|
+
|
|
48
56
|
**Your last choices are remembered** in `~/.config/ds-cs/preferences.json`, so
|
|
49
57
|
the next run preselects them and you can just hit enter three times. Delete the
|
|
50
58
|
file to reset. It lives in your user directory, not the repo — scope and agent
|
|
@@ -183,10 +191,15 @@ npm run dev -- --help
|
|
|
183
191
|
The prompts are hand-rolled keypress handling, which cannot be exercised without
|
|
184
192
|
a terminal — every automated check would otherwise stop at `isInteractive()` and
|
|
185
193
|
test the non-interactive path instead. `test/pty_harness.py` runs the real
|
|
186
|
-
binary under a pty, sends keystrokes, and prints the
|
|
194
|
+
binary under a pty, sends keystrokes, and prints the final screen.
|
|
195
|
+
|
|
196
|
+
It splits the raw stream into repaint frames rather than counting occurrences of
|
|
197
|
+
a menu in it. A prompt redraws by emitting cursor-up and erase sequences, so the
|
|
198
|
+
byte stream contains every frame ever drawn; counting them there would report a
|
|
199
|
+
correctly-repainting prompt as one that duplicates its menu:
|
|
187
200
|
|
|
188
201
|
```bash
|
|
189
|
-
python3 test/pty_harness.py "ENTER,ENTER,ENTER,s,t,o,SPACE,ENTER" \
|
|
202
|
+
python3 test/pty_harness.py "ENTER,SPACE,ENTER,ENTER,s,t,o,SPACE,ENTER" \
|
|
190
203
|
node bin/ds-cs.mjs add
|
|
191
204
|
```
|
|
192
205
|
|
package/package.json
CHANGED
package/src/setup.mjs
CHANGED
|
@@ -106,7 +106,7 @@ export async function resolveInstallTarget(flags, { projectRoot }) {
|
|
|
106
106
|
message: "Which agents should read these skills?",
|
|
107
107
|
choices: agentChoices(detected),
|
|
108
108
|
initialSelected: seed,
|
|
109
|
-
hint: "type to filter · space
|
|
109
|
+
hint: "type to filter · space toggle · a all · enter confirm",
|
|
110
110
|
required: true,
|
|
111
111
|
});
|
|
112
112
|
agents = picked.map((choice) => choice.value);
|
|
@@ -175,7 +175,7 @@ export async function pickSkills(skills) {
|
|
|
175
175
|
description: skill.skillPath.replace(/SKILL\.md$/, ""),
|
|
176
176
|
keywords: `${skill.name} ${skill.skillPath} ${skill.description ?? ""}`,
|
|
177
177
|
})),
|
|
178
|
-
hint: "type to filter · space
|
|
178
|
+
hint: "type to filter · space toggle · a all · enter install",
|
|
179
179
|
required: true,
|
|
180
180
|
});
|
|
181
181
|
return picked.map((choice) => choice.value);
|
package/src/utils/prompt.mjs
CHANGED
|
@@ -8,22 +8,47 @@ import { bold, cyan, dim, yellow } from "./log.mjs";
|
|
|
8
8
|
* Interactive prompts.
|
|
9
9
|
*
|
|
10
10
|
* Hand-rolled rather than pulled from a prompt library, because the CLI ships
|
|
11
|
-
* with zero runtime dependencies.
|
|
12
|
-
* for an installer: search/filter over a list, multi-select with pre-selection,
|
|
13
|
-
* and keyboard navigation — plus the guarantee that a non-interactive shell
|
|
14
|
-
* never blocks on a keypress.
|
|
11
|
+
* with zero runtime dependencies.
|
|
15
12
|
*/
|
|
16
13
|
|
|
17
14
|
const KEY = {
|
|
18
15
|
up: "up",
|
|
19
16
|
down: "down",
|
|
20
17
|
space: "space",
|
|
21
|
-
enter: "
|
|
18
|
+
enter: "enter",
|
|
22
19
|
escape: "escape",
|
|
23
20
|
ctrlC: "c",
|
|
24
21
|
backspace: "backspace",
|
|
25
22
|
};
|
|
26
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A repainting region of the terminal.
|
|
26
|
+
*
|
|
27
|
+
* The previous implementation moved the cursor up, cleared a single line, and
|
|
28
|
+
* wrote the menu again — which appended a fresh copy on every keypress and
|
|
29
|
+
* scrolled the real menu off screen. Repainting has to be stateless: each frame
|
|
30
|
+
* is measured, the last one is fully erased, and the new one is written at a
|
|
31
|
+
* known position.
|
|
32
|
+
*/
|
|
33
|
+
function createScreen(write) {
|
|
34
|
+
let lastHeight = 0;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
paint(lines) {
|
|
38
|
+
if (lastHeight > 0) write(`\x1B[${lastHeight}A`);
|
|
39
|
+
write("\x1B[0J"); // erase from the cursor to the end of the screen
|
|
40
|
+
for (const line of lines) write(`${line}\n`);
|
|
41
|
+
lastHeight = lines.length;
|
|
42
|
+
},
|
|
43
|
+
erase() {
|
|
44
|
+
if (lastHeight === 0) return;
|
|
45
|
+
write(`\x1B[${lastHeight}A`);
|
|
46
|
+
write("\x1B[0J");
|
|
47
|
+
lastHeight = 0;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
function readKeys() {
|
|
28
53
|
readline.emitKeypressEvents(stdin);
|
|
29
54
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
@@ -37,9 +62,11 @@ function keyOf(key) {
|
|
|
37
62
|
if (!key) return null;
|
|
38
63
|
switch (key.name) {
|
|
39
64
|
case "up":
|
|
65
|
+
return KEY.up;
|
|
40
66
|
case "k":
|
|
41
67
|
return KEY.up;
|
|
42
68
|
case "down":
|
|
69
|
+
return KEY.down;
|
|
43
70
|
case "j":
|
|
44
71
|
return KEY.down;
|
|
45
72
|
case "space":
|
|
@@ -68,34 +95,36 @@ export function isInteractive() {
|
|
|
68
95
|
return Boolean(stdin.isTTY && stdout.isTTY) && process.env.CI !== "true";
|
|
69
96
|
}
|
|
70
97
|
|
|
71
|
-
function visibleWidth(text) {
|
|
72
|
-
return stripAnsi(text).length;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
98
|
function stripAnsi(text) {
|
|
76
99
|
return text.replace(/\u001B\[[0-9;]*m/g, "");
|
|
77
100
|
}
|
|
78
101
|
|
|
102
|
+
function visibleWidth(text) {
|
|
103
|
+
return stripAnsi(text).length;
|
|
104
|
+
}
|
|
105
|
+
|
|
79
106
|
function pad(text, width) {
|
|
80
107
|
const gap = width - visibleWidth(text);
|
|
81
108
|
return gap > 0 ? text + " ".repeat(gap) : text;
|
|
82
109
|
}
|
|
83
110
|
|
|
111
|
+
function termWidth() {
|
|
112
|
+
return stdout.columns || 80;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function termRows() {
|
|
116
|
+
return stdout.rows || 24;
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
/**
|
|
85
120
|
* Search-and-multi-select over a list of choices.
|
|
86
121
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
122
|
+
* The prompt an installer lives or dies by: typing filters, space toggles,
|
|
123
|
+
* enter confirms. A leading "Select all" row — and the `a` shortcut — make the
|
|
124
|
+
* whole registry selectable without fifteen space presses, which is what you
|
|
125
|
+
* want on a first run and never again.
|
|
91
126
|
*/
|
|
92
|
-
export function multiselect({
|
|
93
|
-
message,
|
|
94
|
-
choices,
|
|
95
|
-
initialSelected = [],
|
|
96
|
-
hint,
|
|
97
|
-
required = false,
|
|
98
|
-
}) {
|
|
127
|
+
export function multiselect({ message, choices, initialSelected = [], hint, required = false }) {
|
|
99
128
|
if (!isInteractive()) {
|
|
100
129
|
throw new CliError(`"${message}" needs an interactive terminal`, {
|
|
101
130
|
hint: "Pass the choice explicitly instead, e.g. --skill <name> or --agent <name>.",
|
|
@@ -103,136 +132,179 @@ export function multiselect({
|
|
|
103
132
|
}
|
|
104
133
|
if (choices.length === 0) return [];
|
|
105
134
|
|
|
106
|
-
const
|
|
135
|
+
const ALL = Symbol("select-all");
|
|
136
|
+
const selected = new Set(
|
|
137
|
+
initialSelected.filter((value) => choices.some((choice) => choice.value === value))
|
|
138
|
+
);
|
|
139
|
+
|
|
107
140
|
let cursor = 0;
|
|
108
141
|
let filter = "";
|
|
109
142
|
let offset = 0;
|
|
143
|
+
let status = "";
|
|
110
144
|
|
|
111
|
-
const stop = readKeys();
|
|
112
145
|
const write = (line) => stdout.write(line);
|
|
113
|
-
const
|
|
146
|
+
const screen = createScreen(write);
|
|
147
|
+
const stop = readKeys();
|
|
148
|
+
const pageSize = Math.min(8, Math.max(3, termRows() - 9));
|
|
114
149
|
|
|
115
|
-
const
|
|
150
|
+
const visibleChoices = () =>
|
|
116
151
|
filter === ""
|
|
117
152
|
? choices
|
|
118
153
|
: choices.filter((choice) =>
|
|
119
|
-
// `keywords`
|
|
120
|
-
//
|
|
154
|
+
// `keywords` carries extra searchable text — a skill's full
|
|
155
|
+
// description, say — without widening what the list displays.
|
|
121
156
|
(choice.keywords ?? `${choice.label} ${choice.description ?? ""}`)
|
|
122
157
|
.toLowerCase()
|
|
123
158
|
.includes(filter.toLowerCase())
|
|
124
159
|
);
|
|
125
160
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
const rows = () => [
|
|
162
|
+
{ value: ALL, label: "Select all" },
|
|
163
|
+
...visibleChoices().map((choice) => ({ value: choice.value, label: choice.label, choice })),
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const paint = () => {
|
|
167
|
+
const all = rows();
|
|
168
|
+
if (cursor >= all.length) cursor = all.length - 1;
|
|
169
|
+
if (cursor < 0) cursor = 0;
|
|
129
170
|
if (cursor < offset) offset = cursor;
|
|
130
171
|
if (cursor >= offset + pageSize) offset = cursor - pageSize + 1;
|
|
131
172
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const window = rows.slice(offset, offset + pageSize);
|
|
140
|
-
const labelWidth = Math.max(
|
|
173
|
+
const window = all.slice(offset, offset + pageSize);
|
|
174
|
+
const labelWidth = Math.min(
|
|
175
|
+
28,
|
|
176
|
+
Math.max(0, ...all.map((row) => visibleWidth(row.label)))
|
|
177
|
+
);
|
|
178
|
+
const pathWidth = Math.max(
|
|
141
179
|
0,
|
|
142
|
-
...
|
|
180
|
+
...all.map((row) => visibleWidth(row.choice?.description ?? ""))
|
|
143
181
|
);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
const lines = [];
|
|
183
|
+
lines.push(`${bold("?")} ${bold(message)}`);
|
|
184
|
+
if (hint) lines.push(` ${dim(hint)}`);
|
|
185
|
+
lines.push(filter === "" ? "" : ` ${dim("filter:")} ${filter}${cyan("▌")}`);
|
|
186
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
187
|
+
|
|
188
|
+
for (const row of window) {
|
|
189
|
+
const index = offset + window.indexOf(row);
|
|
147
190
|
const isCursor = index === cursor;
|
|
148
|
-
|
|
191
|
+
|
|
192
|
+
if (row.value === ALL) {
|
|
193
|
+
const every = visibleChoices().length > 0 && visibleChoices().every((c) => selected.has(c.value));
|
|
194
|
+
const marker = every ? cyan("◉") : dim("○");
|
|
195
|
+
lines.push(
|
|
196
|
+
` ${isCursor ? cyan("❯") : " "} ${marker} ${isCursor ? bold("Select all") : "Select all"}`
|
|
197
|
+
);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const isSelected = selected.has(row.value);
|
|
149
202
|
const marker = isSelected ? cyan("◉") : dim("○");
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
203
|
+
const label = isCursor ? bold(pad(row.label, labelWidth)) : pad(row.label, labelWidth);
|
|
204
|
+
const path = row.choice?.description ? dim(` ${row.choice.description}`) : "";
|
|
205
|
+
lines.push(
|
|
206
|
+
` ${isCursor ? cyan("❯") : " "} ${marker} ${label}${path}`.slice(0, termWidth()).padEnd(
|
|
207
|
+
Math.min(termWidth(), labelWidth + pathWidth + 6)
|
|
208
|
+
)
|
|
209
|
+
);
|
|
154
210
|
}
|
|
155
211
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
212
|
+
if (all.length > window.length) {
|
|
213
|
+
lines.push(dim(` … ${all.length - window.length} more`));
|
|
214
|
+
}
|
|
215
|
+
lines.push("");
|
|
216
|
+
lines.push(
|
|
217
|
+
status ||
|
|
218
|
+
dim(
|
|
219
|
+
selected.size === 0
|
|
220
|
+
? "(none selected)"
|
|
221
|
+
: `(${selected.size} selected: ${[...selected].slice(0, 4).join(", ")}${selected.size > 4 ? ", …" : ""})`
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
screen.paint(lines);
|
|
159
225
|
};
|
|
160
226
|
|
|
161
|
-
const
|
|
227
|
+
const finish = (error) => {
|
|
162
228
|
stop();
|
|
163
|
-
|
|
164
|
-
|
|
229
|
+
screen.erase();
|
|
230
|
+
stdout.write("\x1B[?25h");
|
|
231
|
+
if (error) throw error;
|
|
165
232
|
};
|
|
166
233
|
|
|
167
|
-
stdout.write("\x1B[?25l"); // hide
|
|
234
|
+
stdout.write("\x1B[?25l"); // hide the cursor while navigating
|
|
235
|
+
paint();
|
|
168
236
|
|
|
169
|
-
return new Promise((resolve
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
237
|
+
return new Promise((resolve) => {
|
|
238
|
+
const toggleAll = () => {
|
|
239
|
+
const shown = visibleChoices();
|
|
240
|
+
const everySelected = shown.every((choice) => selected.has(choice.value));
|
|
241
|
+
if (everySelected) for (const choice of shown) selected.delete(choice.value);
|
|
242
|
+
else for (const choice of shown) selected.add(choice.value);
|
|
243
|
+
};
|
|
176
244
|
|
|
177
245
|
const onKeypress = (str, key) => {
|
|
178
246
|
const name = keyOf(key);
|
|
247
|
+
const all = rows();
|
|
179
248
|
|
|
180
249
|
if (name === KEY.ctrlC) {
|
|
181
|
-
|
|
182
|
-
cleanup();
|
|
250
|
+
finish();
|
|
183
251
|
process.exit(130);
|
|
184
252
|
}
|
|
185
|
-
|
|
186
253
|
if (name === KEY.escape) {
|
|
187
|
-
|
|
188
|
-
reject(new CliError("Aborted."));
|
|
254
|
+
finish(new CliError("Aborted."));
|
|
189
255
|
return;
|
|
190
256
|
}
|
|
191
|
-
|
|
192
257
|
if (name === KEY.enter) {
|
|
193
258
|
if (required && selected.size === 0) {
|
|
194
|
-
|
|
259
|
+
status = yellow("! select at least one (enter to confirm)");
|
|
260
|
+
paint();
|
|
195
261
|
return;
|
|
196
262
|
}
|
|
197
|
-
|
|
263
|
+
finish();
|
|
198
264
|
resolve(choices.filter((choice) => selected.has(choice.value)));
|
|
199
265
|
return;
|
|
200
266
|
}
|
|
201
|
-
|
|
202
267
|
if (name === KEY.up || name === KEY.down) {
|
|
203
|
-
const rows = visible();
|
|
204
|
-
if (rows.length === 0) return;
|
|
205
268
|
cursor = name === KEY.up ? cursor - 1 : cursor + 1;
|
|
206
|
-
if (cursor < 0) cursor =
|
|
207
|
-
if (cursor >=
|
|
208
|
-
|
|
269
|
+
if (cursor < 0) cursor = all.length - 1;
|
|
270
|
+
if (cursor >= all.length) cursor = 0;
|
|
271
|
+
status = "";
|
|
272
|
+
paint();
|
|
209
273
|
return;
|
|
210
274
|
}
|
|
211
|
-
|
|
212
275
|
if (name === KEY.space) {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
276
|
+
const row = all[cursor];
|
|
277
|
+
if (row) {
|
|
278
|
+
if (row.value === ALL) toggleAll();
|
|
279
|
+
else if (selected.has(row.value)) selected.delete(row.value);
|
|
280
|
+
else selected.add(row.value);
|
|
281
|
+
}
|
|
282
|
+
status = "";
|
|
283
|
+
paint();
|
|
219
284
|
return;
|
|
220
285
|
}
|
|
221
|
-
|
|
222
286
|
if (name === KEY.backspace) {
|
|
223
287
|
filter = filter.slice(0, -1);
|
|
224
288
|
cursor = 0;
|
|
225
289
|
offset = 0;
|
|
226
|
-
|
|
290
|
+
status = "";
|
|
291
|
+
paint();
|
|
227
292
|
return;
|
|
228
293
|
}
|
|
229
294
|
|
|
230
|
-
//
|
|
295
|
+
// `a` toggles everything; anything else is filter input.
|
|
296
|
+
if (str === "a" && !key.ctrl && !key.meta) {
|
|
297
|
+
toggleAll();
|
|
298
|
+
status = "";
|
|
299
|
+
paint();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
231
302
|
if (str && !key.ctrl && !key.meta && visibleWidth(str) > 0) {
|
|
232
303
|
filter += str;
|
|
233
304
|
cursor = 0;
|
|
234
305
|
offset = 0;
|
|
235
|
-
|
|
306
|
+
status = "";
|
|
307
|
+
paint();
|
|
236
308
|
}
|
|
237
309
|
};
|
|
238
310
|
|
|
@@ -241,8 +313,8 @@ export function multiselect({
|
|
|
241
313
|
});
|
|
242
314
|
}
|
|
243
315
|
|
|
244
|
-
/** Single-select, same
|
|
245
|
-
export function select({ message, choices, initialIndex = 0 }) {
|
|
316
|
+
/** Single-select, same repaint model with enter choosing the cursor row. */
|
|
317
|
+
export function select({ message, choices, initialIndex = 0, hint }) {
|
|
246
318
|
if (choices.length === 0) return [];
|
|
247
319
|
|
|
248
320
|
if (!isInteractive()) {
|
|
@@ -251,60 +323,67 @@ export function select({ message, choices, initialIndex = 0 }) {
|
|
|
251
323
|
});
|
|
252
324
|
}
|
|
253
325
|
|
|
254
|
-
let cursor = Math.min(initialIndex, choices.length - 1);
|
|
326
|
+
let cursor = Math.min(Math.max(0, initialIndex), choices.length - 1);
|
|
255
327
|
let offset = 0;
|
|
256
|
-
|
|
328
|
+
|
|
257
329
|
const write = (line) => stdout.write(line);
|
|
258
|
-
const
|
|
330
|
+
const screen = createScreen(write);
|
|
331
|
+
const stop = readKeys();
|
|
332
|
+
const pageSize = Math.min(8, Math.max(3, termRows() - 8));
|
|
259
333
|
|
|
260
|
-
const
|
|
334
|
+
const paint = () => {
|
|
261
335
|
if (cursor >= choices.length) cursor = choices.length - 1;
|
|
262
336
|
if (cursor < offset) offset = cursor;
|
|
263
337
|
if (cursor >= offset + pageSize) offset = cursor - pageSize + 1;
|
|
264
338
|
|
|
265
|
-
write("\x1B[2K\r");
|
|
266
|
-
write(`${bold("?")} ${bold(message)}\n`);
|
|
267
|
-
|
|
268
339
|
const window = choices.slice(offset, offset + pageSize);
|
|
269
|
-
const labelWidth = Math.max(0, ...choices.map((choice) => visibleWidth(choice.label)));
|
|
340
|
+
const labelWidth = Math.min(28, Math.max(0, ...choices.map((choice) => visibleWidth(choice.label))));
|
|
341
|
+
|
|
342
|
+
const lines = [`${bold("?")} ${bold(message)}`];
|
|
343
|
+
if (hint) lines.push(` ${dim(hint)}`);
|
|
270
344
|
|
|
271
345
|
for (const choice of window) {
|
|
272
346
|
const index = offset + window.indexOf(choice);
|
|
273
|
-
const
|
|
274
|
-
const label =
|
|
347
|
+
const isCursor = index === cursor;
|
|
348
|
+
const label = isCursor ? bold(pad(choice.label, labelWidth)) : pad(choice.label, labelWidth);
|
|
275
349
|
const description = choice.description ? dim(` ${choice.description}`) : "";
|
|
276
|
-
|
|
350
|
+
lines.push(` ${isCursor ? cyan("❯") : " "} ${label}${description}`);
|
|
277
351
|
}
|
|
278
352
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
353
|
+
if (choices.length > window.length) {
|
|
354
|
+
lines.push(dim(` … ${choices.length - window.length} more`));
|
|
355
|
+
}
|
|
356
|
+
lines.push("");
|
|
357
|
+
lines.push(dim(hint ? "" : "↑/↓ move · enter select"));
|
|
358
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
283
359
|
|
|
284
|
-
|
|
285
|
-
stop();
|
|
286
|
-
write("\x1B[?25h");
|
|
360
|
+
screen.paint(lines);
|
|
287
361
|
};
|
|
288
362
|
|
|
289
|
-
stdout.write("\x1B[?25l");
|
|
363
|
+
stdout.write("\x1B[?25l");
|
|
364
|
+
paint();
|
|
290
365
|
|
|
291
366
|
return new Promise((resolve, reject) => {
|
|
292
|
-
render();
|
|
293
|
-
|
|
294
367
|
const onKeypress = (str, key) => {
|
|
295
368
|
const name = keyOf(key);
|
|
296
369
|
|
|
297
370
|
if (name === KEY.ctrlC) {
|
|
298
|
-
|
|
371
|
+
stop();
|
|
372
|
+
screen.erase();
|
|
373
|
+
stdout.write("\x1B[?25h");
|
|
299
374
|
process.exit(130);
|
|
300
375
|
}
|
|
301
376
|
if (name === KEY.escape) {
|
|
302
|
-
|
|
377
|
+
stop();
|
|
378
|
+
screen.erase();
|
|
379
|
+
stdout.write("\x1B[?25h");
|
|
303
380
|
reject(new CliError("Aborted."));
|
|
304
381
|
return;
|
|
305
382
|
}
|
|
306
383
|
if (name === KEY.enter) {
|
|
307
|
-
|
|
384
|
+
stop();
|
|
385
|
+
screen.erase();
|
|
386
|
+
stdout.write("\x1B[?25h");
|
|
308
387
|
resolve([choices[cursor]]);
|
|
309
388
|
return;
|
|
310
389
|
}
|
|
@@ -312,7 +391,7 @@ export function select({ message, choices, initialIndex = 0 }) {
|
|
|
312
391
|
cursor = name === KEY.up ? cursor - 1 : cursor + 1;
|
|
313
392
|
if (cursor < 0) cursor = choices.length - 1;
|
|
314
393
|
if (cursor >= choices.length) cursor = 0;
|
|
315
|
-
|
|
394
|
+
paint();
|
|
316
395
|
}
|
|
317
396
|
};
|
|
318
397
|
|
|
@@ -328,8 +407,7 @@ export function select({ message, choices, initialIndex = 0 }) {
|
|
|
328
407
|
export async function confirm(message, { defaultYes = true } = {}) {
|
|
329
408
|
if (!isInteractive()) return defaultYes;
|
|
330
409
|
|
|
331
|
-
const
|
|
332
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
410
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
333
411
|
try {
|
|
334
412
|
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
335
413
|
const answer = (await rl.question(`${bold("?")} ${message} ${dim(suffix)} `)).trim().toLowerCase();
|
|
@@ -342,8 +420,7 @@ export async function confirm(message, { defaultYes = true } = {}) {
|
|
|
342
420
|
|
|
343
421
|
export async function promptText(message, fallback = "") {
|
|
344
422
|
if (!isInteractive()) return fallback;
|
|
345
|
-
const
|
|
346
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
423
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
347
424
|
try {
|
|
348
425
|
const suffix = fallback ? dim(` (${fallback})`) : "";
|
|
349
426
|
const answer = (await rl.question(`${bold("?")} ${message}${suffix} `)).trim();
|
|
@@ -353,4 +430,4 @@ export async function promptText(message, fallback = "") {
|
|
|
353
430
|
}
|
|
354
431
|
}
|
|
355
432
|
|
|
356
|
-
export { select as selectOne, multiselect as selectMany };
|
|
433
|
+
export { select as selectOne, multiselect as selectMany };
|