@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 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 to toggle · enter to confirm
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 to toggle · enter to install
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 transcript:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dastyaresocial/ds-cs",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for Dastyare Social — CS. Install DS-CS skills and third-party agent skills, wire up MCP, and authenticate to a channel.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 to toggle · enter to confirm",
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 to toggle · enter to install",
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);
@@ -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. That means implementing the parts that matter
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: "return",
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
- * This is the prompt an installer lives or dies by: typing filters, space
88
- * toggles, enter confirms, and the current selection is always visible. Keys
89
- * that are not commands are treated as filter input, so the user never has to
90
- * think about which mode they are in.
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 selected = new Set(initialSelected.filter((value) => choices.some((c) => c.value === value)));
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 pageSize = Math.min(10, Math.max(3, stdout.rows ? stdout.rows - 8 : 10));
146
+ const screen = createScreen(write);
147
+ const stop = readKeys();
148
+ const pageSize = Math.min(8, Math.max(3, termRows() - 9));
114
149
 
115
- const visible = () =>
150
+ const visibleChoices = () =>
116
151
  filter === ""
117
152
  ? choices
118
153
  : choices.filter((choice) =>
119
- // `keywords` lets a choice carry extra searchable text — a skill's
120
- // full description, say — without widening what the list shows.
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 render = (bottomLine) => {
127
- const rows = visible();
128
- if (cursor >= rows.length) cursor = Math.max(0, rows.length - 1);
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
- write("\x1B[2K\r");
133
- write(`${bold("?")} ${bold(message)}\n`);
134
-
135
- if (hint) write(` ${dim(hint)}\n`);
136
- if (filter) write(` ${dim("filter:")} ${filter}${cyan("▌")}\n`);
137
- write("\x1B[1A".repeat(filter ? 2 : hint ? 2 : 1));
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
- ...choices.map((choice) => visibleWidth(choice.label))
180
+ ...all.map((row) => visibleWidth(row.choice?.description ?? ""))
143
181
  );
144
-
145
- for (const choice of window) {
146
- const index = rows.indexOf(choice);
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
- const isSelected = selected.has(choice.value);
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 pointer = isCursor ? cyan("❯") : " ";
151
- const label = isCursor ? bold(pad(choice.label, labelWidth)) : pad(choice.label, labelWidth);
152
- const description = choice.description ? dim(` ${choice.description}`) : "";
153
- write(` ${pointer} ${marker} ${label}${description}\n`);
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
- write("\x1B[1A".repeat(window.length));
157
- write("\x1B[2K\r");
158
- write(`${bottomLine}\n`);
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 cleanup = (message) => {
227
+ const finish = (error) => {
162
228
  stop();
163
- write("\x1B[?25h"); // show cursor
164
- if (message) write(`${message}\n`);
229
+ screen.erase();
230
+ stdout.write("\x1B[?25h");
231
+ if (error) throw error;
165
232
  };
166
233
 
167
- stdout.write("\x1B[?25l"); // hide cursor for the duration of the prompt
234
+ stdout.write("\x1B[?25l"); // hide the cursor while navigating
235
+ paint();
168
236
 
169
- return new Promise((resolve, reject) => {
170
- const summary = () =>
171
- selected.size === 0
172
- ? `${dim("(none selected)")}`
173
- : dim(`(${selected.size} selected: ${[...selected].join(", ")})`);
174
-
175
- render(summary());
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
- write("\x1B[1A".repeat(1)); // clear summary row
182
- cleanup();
250
+ finish();
183
251
  process.exit(130);
184
252
  }
185
-
186
253
  if (name === KEY.escape) {
187
- cleanup();
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
- render(`${yellow("!")} select at least one (enter to confirm)`);
259
+ status = yellow("! select at least one (enter to confirm)");
260
+ paint();
195
261
  return;
196
262
  }
197
- cleanup();
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 = rows.length - 1;
207
- if (cursor >= rows.length) cursor = 0;
208
- render(summary());
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 rows = visible();
214
- const choice = rows[cursor];
215
- if (!choice) return;
216
- if (selected.has(choice.value)) selected.delete(choice.value);
217
- else selected.add(choice.value);
218
- render(summary());
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
- render(summary());
290
+ status = "";
291
+ paint();
227
292
  return;
228
293
  }
229
294
 
230
- // Anything else is filter input.
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
- render(summary());
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 interaction model with `enter` selecting the cursor. */
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
- const stop = readKeys();
328
+
257
329
  const write = (line) => stdout.write(line);
258
- const pageSize = Math.min(10, Math.max(3, stdout.rows ? stdout.rows - 8 : 10));
330
+ const screen = createScreen(write);
331
+ const stop = readKeys();
332
+ const pageSize = Math.min(8, Math.max(3, termRows() - 8));
259
333
 
260
- const render = () => {
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 pointer = index === cursor ? cyan("❯") : " ";
274
- const label = index === cursor ? bold(pad(choice.label, labelWidth)) : pad(choice.label, labelWidth);
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
- write(` ${pointer} ${label}${description}\n`);
350
+ lines.push(` ${isCursor ? cyan("❯") : " "} ${label}${description}`);
277
351
  }
278
352
 
279
- write("\x1B[1A".repeat(window.length));
280
- write("\x1B[2K\r");
281
- write(dim(" ↑/↓ move · enter select\n"));
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
- const cleanup = () => {
285
- stop();
286
- write("\x1B[?25h");
360
+ screen.paint(lines);
287
361
  };
288
362
 
289
- stdout.write("\x1B[?25l"); // hide cursor for the duration of the prompt
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
- cleanup();
371
+ stop();
372
+ screen.erase();
373
+ stdout.write("\x1B[?25h");
299
374
  process.exit(130);
300
375
  }
301
376
  if (name === KEY.escape) {
302
- cleanup();
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
- cleanup();
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
- render();
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 { createInterface } = readline;
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 { createInterface } = readline;
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 };