caique 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +194 -6
- package/dist/ask.d.ts +62 -0
- package/dist/ask.js +148 -0
- package/dist/binding.d.ts +55 -0
- package/dist/binding.js +66 -0
- package/dist/decide.d.ts +68 -0
- package/dist/decide.js +69 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +10 -4
- package/dist/raw.d.ts +55 -0
- package/dist/raw.js +146 -0
- package/dist/spec.d.ts +43 -0
- package/dist/spec.js +31 -0
- package/dist/terminal.d.ts +20 -0
- package/dist/terminal.js +81 -0
- package/package.json +33 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ofri Peretz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/ofri-peretz/burgee/tree/main/packages/caique" target="blank">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/caique-lockup.svg" />
|
|
5
|
+
<img src="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/caique-lockup-light.svg" alt="caique" width="360" />
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
2
9
|
|
|
3
|
-
|
|
4
|
-
|
|
10
|
+
<p align="center">
|
|
11
|
+
Prompts that are flags first — so an agent answers before it is asked, and nothing ever hangs.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/caique"><img src="https://img.shields.io/npm/v/caique?style=flat-square&color=0a6b47" alt="npm version" /></a>
|
|
16
|
+
<img src="https://img.shields.io/badge/status-pre--release-a84c17?style=flat-square" alt="Status: pre-release" />
|
|
17
|
+
<img src="https://img.shields.io/badge/runtime%20dependencies-0-0a6b47?style=flat-square" alt="Zero runtime dependencies" />
|
|
18
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
**Pre-release.** The first working slice is here — `decide()`, below — and the rest follows
|
|
22
|
+
[`.sdlc/intents/caique/`](https://github.com/ofri-peretz/burgee/tree/main/.sdlc/intents/caique).
|
|
5
23
|
|
|
6
24
|
A **caique** (kah-EEK) is a small, loud, never-silent parrot — and this one always answers
|
|
7
25
|
back. It is also the light wooden boat of the Bosphorus and the Greek islands, the one that
|
|
@@ -9,6 +27,160 @@ runs between the ship and the shore carrying people and messages across the gap.
|
|
|
9
27
|
true of this package: it is the go-between that carries a question from a program to whoever
|
|
10
28
|
is calling, human or agent, and brings the answer back. **It never hangs.**
|
|
11
29
|
|
|
30
|
+
## What ships today
|
|
31
|
+
|
|
32
|
+
`decide()` — the rule that decides whether a person can be asked at all, and the reason
|
|
33
|
+
this package can promise it never hangs. It is pure: a value, a runtime slice and the run's
|
|
34
|
+
flags in, a verdict out.
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { decide } from 'caique/decide';
|
|
38
|
+
|
|
39
|
+
decide({
|
|
40
|
+
value: undefined, // nothing was passed
|
|
41
|
+
spec: { kind: 'text', message: 'Where should it go?' },
|
|
42
|
+
option: 'output-dir',
|
|
43
|
+
runtime: { env: process.env, isTTY: { stdin: process.stdin.isTTY } },
|
|
44
|
+
required: true,
|
|
45
|
+
});
|
|
46
|
+
// no terminal -> { action: 'error', code: 'USAGE',
|
|
47
|
+
// message: '--output-dir is required when there is no terminal',
|
|
48
|
+
// fix: 'pass --output-dir; it would have been asked as "Where should it go?"' }
|
|
49
|
+
// a terminal -> { action: 'prompt' }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The order of the rule is the argument, and it is enumerated rather than described: every
|
|
53
|
+
one of the 256 combinations of value x kind x TTY x CI x `--json` x `--yes` x
|
|
54
|
+
`--interactive` x required is generated and checked in `decide.test.ts`, against the rule
|
|
55
|
+
written a second time, independently. The case that would be a bug report if it broke is
|
|
56
|
+
called out by name: **no terminal and no value is never a prompt.**
|
|
57
|
+
|
|
58
|
+
`--interactive` reaches past "we would not have asked", never past "there is nobody to
|
|
59
|
+
ask" — and when there is nobody, the refusal says so, rather than looking like the flag was
|
|
60
|
+
ignored.
|
|
61
|
+
|
|
62
|
+
### Asking, once it is allowed
|
|
63
|
+
|
|
64
|
+
`ask()` is the six kinds — `text`, `confirm`, `select`, `multiselect`, `password`, `path` —
|
|
65
|
+
each written as a question and a line read back:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { ask } from 'caique/ask';
|
|
69
|
+
|
|
70
|
+
await ask(
|
|
71
|
+
{ kind: 'select', message: 'Which host?', choices: [{ value: 'ora' }, { value: 'log-update' }] },
|
|
72
|
+
{ reader, writer },
|
|
73
|
+
);
|
|
74
|
+
// Which host?
|
|
75
|
+
// 1) ora
|
|
76
|
+
// 2) log-update
|
|
77
|
+
// enter a number (1-2):
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Line mode is not the fallback, it is the floor.** No raw mode, no cursor movement, no
|
|
81
|
+
escape sequence, no redraw — so this *is* the accessible rendering rather than a second
|
|
82
|
+
implementation of it, and a screen reader gets the same bytes a terminal does. The raw-mode
|
|
83
|
+
renderer that arrows and highlights will sit on top and answer the same questions.
|
|
84
|
+
|
|
85
|
+
A stream that ends is a **cancellation**, not an empty answer: `Ctrl-D` and a closed pipe
|
|
86
|
+
both mean nobody is going to type, and reading that as `''` is how a program writes to a
|
|
87
|
+
path nobody chose. Invalid input is re-asked five times and then gives up, because a loop
|
|
88
|
+
against a stream that keeps answering wrongly is the same hang wearing a hat.
|
|
89
|
+
|
|
90
|
+
`projection(spec)` gives the question without the conversation, for a gallery, a `--help`
|
|
91
|
+
or a transcript in an issue.
|
|
92
|
+
|
|
93
|
+
### Wiring it to a CLI
|
|
94
|
+
|
|
95
|
+
`resolvePrompts()` is the pass a framework calls from its `preAction` hook, once the flags,
|
|
96
|
+
environment and config have had their turn:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { resolvePrompts } from 'caique/binding';
|
|
100
|
+
|
|
101
|
+
const { values, failure } = await resolvePrompts({
|
|
102
|
+
options, // { name: { required: true, prompt: { kind: 'text', message: 'Project name?' } } }
|
|
103
|
+
values, // what every other source resolved
|
|
104
|
+
runtime: { env: process.env, isTTY: { stdin: process.stdin.isTTY } },
|
|
105
|
+
flags: { json, yes, interactive },
|
|
106
|
+
io: { reader, writer },
|
|
107
|
+
});
|
|
108
|
+
if (failure) throw new CliError(failure.code, failure.message, { fix: failure.fix });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
It walks the options in **declaration order** — the order the help listed — asks only what
|
|
112
|
+
has to be asked, and **stops at the first refusal**, because a caller about to exit is
|
|
113
|
+
better served by one actionable message than six.
|
|
114
|
+
|
|
115
|
+
There is one binding, not one per host. What a framework supplies is a record of options,
|
|
116
|
+
the values so far and a runtime; none of that needs any particular framework's types, so
|
|
117
|
+
`caique` imports none of them.
|
|
118
|
+
|
|
119
|
+
### On a real terminal
|
|
120
|
+
|
|
121
|
+
`createIo()` is the reader and writer over actual streams — the only file in the package
|
|
122
|
+
that touches a terminal:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
import { createIo } from 'caique/terminal';
|
|
126
|
+
import { ask } from 'caique/ask';
|
|
127
|
+
|
|
128
|
+
const io = createIo({ input: process.stdin, output: process.stdout });
|
|
129
|
+
await ask({ kind: 'password', message: 'Token?' }, io);
|
|
130
|
+
io.close();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A `password` prompt is not echoed, and that lives here rather than in the widgets: this is
|
|
134
|
+
the only layer that knows what echo *is*, and no widget can leak a secret by writing it
|
|
135
|
+
back, because no widget writes what it read. The echo is suppressed for the duration of the
|
|
136
|
+
question rather than by turning the terminal's echo off — which would leave it off if the
|
|
137
|
+
process died mid-prompt.
|
|
138
|
+
|
|
139
|
+
### Arrow keys, where there is a terminal to take them
|
|
140
|
+
|
|
141
|
+
`askList()` draws `select` and `multiselect` with a moving highlight and repaints in place.
|
|
142
|
+
It answers the same question `ask()` does and returns the same value, so it is a swap and
|
|
143
|
+
not a second implementation:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
import { askList, canRender } from 'caique/raw';
|
|
147
|
+
import { createIo } from 'caique/terminal';
|
|
148
|
+
|
|
149
|
+
const io = { ...createIo({ input: process.stdin, output: process.stdout }), keys: process.stdin };
|
|
150
|
+
const spec = { kind: 'select', message: 'Which host?', choices: [{ value: 'ora' }, { value: 'chalk' }] };
|
|
151
|
+
const answer = canRender(io.keys) ? await askList(spec, io) : await ask(spec, io);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Line mode is the floor, not the fallback: this is decoration on top of it, and the suite
|
|
155
|
+
proves the two agree by running the same spec through both and comparing the answers.
|
|
156
|
+
`Ctrl-C` cancels — in raw mode it arrives as a byte rather than a signal — and the terminal
|
|
157
|
+
is put back the way it was found either way.
|
|
158
|
+
|
|
159
|
+
## Weight
|
|
160
|
+
|
|
161
|
+
Every subpath is a lock, not a convention, and the numbers below are asserted by
|
|
162
|
+
`weight.test.ts` against `dist/`, not estimated. **The ceiling is clack**: `@clack/prompts`
|
|
163
|
+
1.8.0 is **101,684 B across six packages** — itself, `@clack/core`, `fast-string-width`,
|
|
164
|
+
`fast-string-truncated-width`, `fast-wrap-ansi` and `sisteransi`.
|
|
165
|
+
|
|
166
|
+
| Subpath | Bytes | Reaches |
|
|
167
|
+
| :-- | --: | :-- |
|
|
168
|
+
| `caique` (everything) | 25,627 | no package at all |
|
|
169
|
+
| `caique/spec` | 1,571 | a leaf — declare prompts without loading a widget |
|
|
170
|
+
| `caique/decide` | 4,986 | the spec only |
|
|
171
|
+
| `caique/ask` | 8,564 | the six widgets, no terminal, no raw mode |
|
|
172
|
+
| `caique/raw` | 14,796 | line mode, which it sits on top of |
|
|
173
|
+
| `caique/binding` | 15,475 | the decision and the widgets |
|
|
174
|
+
| `caique/terminal` | 11,975 | the one file that touches a stream |
|
|
175
|
+
|
|
176
|
+
The whole package is **a quarter of the lightest incumbent**, and it reaches nothing:
|
|
177
|
+
`allow` is empty for every entry, asserted rather than claimed. Deciding *not* to ask costs
|
|
178
|
+
4,986 B and never loads the machinery of asking — which is the case an agent hits.
|
|
179
|
+
|
|
180
|
+
Measured 2026-09-09, the same way every bill in this family is: shipped code and data
|
|
181
|
+
(`.js`/`.mjs`/`.cjs` plus imported `.json`, `package.json` never counted), each competitor
|
|
182
|
+
counted whole across its own resolved tree.
|
|
183
|
+
|
|
12
184
|
## What it will be
|
|
13
185
|
|
|
14
186
|
- **Every prompt is a flag first.** A caller who passes the flag is never asked. An agent
|
|
@@ -20,6 +192,22 @@ is calling, human or agent, and brings the answer back. **It never hangs.**
|
|
|
20
192
|
- **Accessible mode** falls back to line input with no live redraw.
|
|
21
193
|
- **Drop-in paths** for inquirer and clack, graded by their own suites.
|
|
22
194
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
195
|
+
## Following along
|
|
196
|
+
|
|
197
|
+
The intent and design are committed before the code is, so you can read what it will be —
|
|
198
|
+
and argue with it — before it exists:
|
|
199
|
+
|
|
200
|
+
- [`.sdlc/intents/caique/`](https://github.com/ofri-peretz/burgee/tree/main/.sdlc/intents/caique)
|
|
201
|
+
— the intent and the design.
|
|
202
|
+
- [Open an issue](https://github.com/ofri-peretz/burgee/issues) if a prompt in your CLI
|
|
203
|
+
cannot be expressed as a flag. That case is the interesting one.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on
|
|
208
|
+
[burgee](https://www.npmjs.com/package/burgee) declares what it is,
|
|
209
|
+
[roundel](https://www.npmjs.com/package/roundel) carries its colours,
|
|
210
|
+
[flagstaff](https://www.npmjs.com/package/flagstaff) flies it, and caique answers back. Each
|
|
211
|
+
is an independent package; none requires the others.
|
|
212
|
+
|
|
213
|
+
MIT © Ofri Peretz — see [LICENSE](./LICENSE).
|
package/dist/ask.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The six widgets, in line mode (R5) — and line mode is not a fallback, it is the floor.
|
|
3
|
+
*
|
|
4
|
+
* Every widget here works by writing a question and reading a line. No raw mode, no cursor
|
|
5
|
+
* movement, no escape sequence, no redraw. That makes it the accessible mode by
|
|
6
|
+
* construction rather than by a second implementation kept in step by hand: a screen
|
|
7
|
+
* reader gets the same bytes a terminal does, and `select` is a numbered list because a
|
|
8
|
+
* numbered list is what a person can answer without seeing a highlight move.
|
|
9
|
+
*
|
|
10
|
+
* It is also what makes the widgets testable without a PTY. A `Reader` is one method that
|
|
11
|
+
* returns the next line, so the whole suite is strings in and strings out; the raw-mode
|
|
12
|
+
* renderer that arrows and highlights will come later sits *on top* of this and answers
|
|
13
|
+
* the same questions, which is how it stays honest.
|
|
14
|
+
*
|
|
15
|
+
* Nothing here reads `process`, and nothing decides *whether* to ask — that is `decide()`,
|
|
16
|
+
* which runs first and refuses when there is no one to ask.
|
|
17
|
+
*/
|
|
18
|
+
import { type BoundPrompt, type PromptSpec } from './spec.js';
|
|
19
|
+
export interface ReadOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Do not echo what is typed. Set for `password`, and honoured by whatever is reading —
|
|
22
|
+
* this module never sees a terminal, so it cannot hide anything itself, and a widget
|
|
23
|
+
* that never writes back what it read cannot leak it either.
|
|
24
|
+
*/
|
|
25
|
+
hidden?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** A line of input, or `undefined` when the stream ended — which is a cancellation. */
|
|
28
|
+
export interface Reader {
|
|
29
|
+
line(options?: ReadOptions): Promise<string | undefined>;
|
|
30
|
+
}
|
|
31
|
+
export interface Writer {
|
|
32
|
+
write(text: string): void;
|
|
33
|
+
}
|
|
34
|
+
export interface Io {
|
|
35
|
+
reader: Reader;
|
|
36
|
+
writer: Writer;
|
|
37
|
+
}
|
|
38
|
+
export type Answer = string | boolean | string[];
|
|
39
|
+
/** Answered, or cancelled — cancellation is a value here and a `CANCELLED` error above (R4). */
|
|
40
|
+
export type Asked = {
|
|
41
|
+
ok: true;
|
|
42
|
+
value: Answer;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
reason: 'cancelled';
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Ask one prompt and return its answer.
|
|
49
|
+
*
|
|
50
|
+
* A stream that ends is a cancellation, not an empty answer: `Ctrl-D` and a closed pipe
|
|
51
|
+
* both mean nobody is going to type, and treating that as `''` is how a program ends up
|
|
52
|
+
* writing to a path the user never chose. Invalid input is re-asked, bounded — after
|
|
53
|
+
* `MAX_ATTEMPTS` it gives up rather than looping, because a loop against a stream that
|
|
54
|
+
* keeps answering wrongly is the hang this package exists to prevent, wearing a hat.
|
|
55
|
+
*/
|
|
56
|
+
export declare function ask(prompt: BoundPrompt | PromptSpec, io: Io): Promise<Asked>;
|
|
57
|
+
/**
|
|
58
|
+
* What a widget would have written, without reading anything — the static projection (U3).
|
|
59
|
+
* The docs gallery, `--help` and a transcript in an issue all want the question without
|
|
60
|
+
* the conversation, and every other package in this family can answer that; so does this.
|
|
61
|
+
*/
|
|
62
|
+
export declare function projection(spec: PromptSpec): string;
|
package/dist/ask.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The six widgets, in line mode (R5) — and line mode is not a fallback, it is the floor.
|
|
3
|
+
*
|
|
4
|
+
* Every widget here works by writing a question and reading a line. No raw mode, no cursor
|
|
5
|
+
* movement, no escape sequence, no redraw. That makes it the accessible mode by
|
|
6
|
+
* construction rather than by a second implementation kept in step by hand: a screen
|
|
7
|
+
* reader gets the same bytes a terminal does, and `select` is a numbered list because a
|
|
8
|
+
* numbered list is what a person can answer without seeing a highlight move.
|
|
9
|
+
*
|
|
10
|
+
* It is also what makes the widgets testable without a PTY. A `Reader` is one method that
|
|
11
|
+
* returns the next line, so the whole suite is strings in and strings out; the raw-mode
|
|
12
|
+
* renderer that arrows and highlights will come later sits *on top* of this and answers
|
|
13
|
+
* the same questions, which is how it stays honest.
|
|
14
|
+
*
|
|
15
|
+
* Nothing here reads `process`, and nothing decides *whether* to ask — that is `decide()`,
|
|
16
|
+
* which runs first and refuses when there is no one to ask.
|
|
17
|
+
*/
|
|
18
|
+
import {} from './spec.js';
|
|
19
|
+
const CANCELLED = { ok: false, reason: 'cancelled' };
|
|
20
|
+
/** Re-asking forever on invalid input is a hang with extra steps. */
|
|
21
|
+
const MAX_ATTEMPTS = 5;
|
|
22
|
+
const DECIMAL = 10;
|
|
23
|
+
const YES = new Set(['y', 'yes', 'true', '1']);
|
|
24
|
+
const NO = new Set(['n', 'no', 'false', '0']);
|
|
25
|
+
/** `Overwrite it? (y/N)` — the default in capitals, the convention every CLI already uses. */
|
|
26
|
+
function confirmSuffix(initial) {
|
|
27
|
+
return initial === true ? ' (Y/n) ' : ' (y/N) ';
|
|
28
|
+
}
|
|
29
|
+
function textSuffix(initial) {
|
|
30
|
+
return typeof initial === 'string' && initial !== '' ? ` (${initial}) ` : ' ';
|
|
31
|
+
}
|
|
32
|
+
/** The numbered list R5 asks for. One-based, because a person is reading it. */
|
|
33
|
+
function listChoices(choices) {
|
|
34
|
+
return choices.map((choice, index) => ` ${index + 1}) ${choice.label ?? choice.value}${choice.hint === undefined ? '' : ` — ${choice.hint}`}`).join('\n');
|
|
35
|
+
}
|
|
36
|
+
/** A 1-based index into `choices`, or nothing when the answer names no choice. */
|
|
37
|
+
function pick(answer, choices) {
|
|
38
|
+
const index = Number.parseInt(answer, DECIMAL);
|
|
39
|
+
if (Number.isInteger(index) && index >= 1 && index <= choices.length)
|
|
40
|
+
return choices[index - 1];
|
|
41
|
+
// A person who types the label rather than its number has answered the question.
|
|
42
|
+
return choices.find((choice) => choice.value === answer || choice.label === answer);
|
|
43
|
+
}
|
|
44
|
+
function confirmAttempt(spec, label) {
|
|
45
|
+
return {
|
|
46
|
+
question: label + confirmSuffix(spec.initial),
|
|
47
|
+
parse: (line) => {
|
|
48
|
+
const answer = line.trim().toLowerCase();
|
|
49
|
+
if (answer === '')
|
|
50
|
+
return { ok: true, value: spec.initial === true };
|
|
51
|
+
if (YES.has(answer))
|
|
52
|
+
return { ok: true, value: true };
|
|
53
|
+
if (NO.has(answer))
|
|
54
|
+
return { ok: true, value: false };
|
|
55
|
+
return { ok: false, problem: 'answer y or n' };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function selectAttempt(choices, label) {
|
|
60
|
+
return {
|
|
61
|
+
question: `${label}\n${listChoices(choices)}\n enter a number (1-${choices.length}): `,
|
|
62
|
+
parse: (line) => {
|
|
63
|
+
const choice = pick(line.trim(), choices);
|
|
64
|
+
return choice === undefined ? { ok: false, problem: `enter a number from 1 to ${choices.length}` } : { ok: true, value: choice.value };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function multiselectAttempt(choices, label) {
|
|
69
|
+
return {
|
|
70
|
+
question: `${label}\n${listChoices(choices)}\n enter numbers separated by commas, or blank for none: `,
|
|
71
|
+
parse: (line) => {
|
|
72
|
+
const parts = line
|
|
73
|
+
.split(',')
|
|
74
|
+
.map((part) => part.trim())
|
|
75
|
+
.filter((part) => part !== '');
|
|
76
|
+
if (parts.length === 0)
|
|
77
|
+
return { ok: true, value: [] };
|
|
78
|
+
const picked = parts.map((part) => pick(part, choices));
|
|
79
|
+
const missing = parts.filter((_, index) => picked[index] === undefined);
|
|
80
|
+
if (missing.length > 0)
|
|
81
|
+
return { ok: false, problem: `${missing.join(', ')} ${missing.length === 1 ? 'is not a choice' : 'are not choices'}; use numbers from 1 to ${choices.length}` };
|
|
82
|
+
return { ok: true, value: picked.map((choice) => choice.value) };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* text, password and path all read a line. `password` differs only in that the caller must
|
|
88
|
+
* not echo it, which is the reader's business — this module never sees a terminal, so
|
|
89
|
+
* hiding the input cannot be got wrong here.
|
|
90
|
+
*/
|
|
91
|
+
function lineAttempt(spec, label) {
|
|
92
|
+
return {
|
|
93
|
+
question: label + textSuffix(spec.initial),
|
|
94
|
+
parse: (line) => {
|
|
95
|
+
const answer = line === '' && typeof spec.initial === 'string' ? spec.initial : line;
|
|
96
|
+
const problem = spec.validate?.(answer);
|
|
97
|
+
return problem === undefined ? { ok: true, value: answer } : { ok: false, problem };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function attemptFor(spec, label) {
|
|
102
|
+
const choices = spec.choices ?? [];
|
|
103
|
+
if (spec.kind === 'confirm')
|
|
104
|
+
return confirmAttempt(spec, label);
|
|
105
|
+
if (spec.kind === 'select')
|
|
106
|
+
return selectAttempt(choices, label);
|
|
107
|
+
if (spec.kind === 'multiselect')
|
|
108
|
+
return multiselectAttempt(choices, label);
|
|
109
|
+
return lineAttempt(spec, label);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Ask one prompt and return its answer.
|
|
113
|
+
*
|
|
114
|
+
* A stream that ends is a cancellation, not an empty answer: `Ctrl-D` and a closed pipe
|
|
115
|
+
* both mean nobody is going to type, and treating that as `''` is how a program ends up
|
|
116
|
+
* writing to a path the user never chose. Invalid input is re-asked, bounded — after
|
|
117
|
+
* `MAX_ATTEMPTS` it gives up rather than looping, because a loop against a stream that
|
|
118
|
+
* keeps answering wrongly is the hang this package exists to prevent, wearing a hat.
|
|
119
|
+
*/
|
|
120
|
+
export async function ask(prompt, io) {
|
|
121
|
+
const label = prompt.message;
|
|
122
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
|
|
123
|
+
const { question, parse } = attemptFor(prompt, label);
|
|
124
|
+
io.writer.write(question);
|
|
125
|
+
// A prompt is a conversation: the next question depends on the last answer, which is
|
|
126
|
+
// what sequential means. Promise.all() would ask all five at once and read none.
|
|
127
|
+
// eslint-disable-next-line reliability/no-await-in-loop -- see above
|
|
128
|
+
const line = await io.reader.line({ hidden: prompt.kind === 'password' });
|
|
129
|
+
if (line === undefined)
|
|
130
|
+
return CANCELLED;
|
|
131
|
+
const result = parse(line);
|
|
132
|
+
if (result.ok)
|
|
133
|
+
return { ok: true, value: result.value };
|
|
134
|
+
io.writer.write(` ${result.problem}\n`);
|
|
135
|
+
}
|
|
136
|
+
io.writer.write(` giving up after ${MAX_ATTEMPTS} attempts\n`);
|
|
137
|
+
return CANCELLED;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* What a widget would have written, without reading anything — the static projection (U3).
|
|
141
|
+
* The docs gallery, `--help` and a transcript in an issue all want the question without
|
|
142
|
+
* the conversation, and every other package in this family can answer that; so does this.
|
|
143
|
+
*/
|
|
144
|
+
export function projection(spec) {
|
|
145
|
+
const { question } = attemptFor(spec, spec.message);
|
|
146
|
+
return question.trimEnd();
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=ask.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a whole command's prompts in one pass (R2, R3) — the piece a framework calls
|
|
3
|
+
* from its `preAction` hook, after the environment and config layers have had their turn.
|
|
4
|
+
*
|
|
5
|
+
* The design sketched this as `caique/burgee`, one binding per host. It is one binding for
|
|
6
|
+
* all of them instead, and that is the better answer: what a host actually supplies is a
|
|
7
|
+
* record of options, the values parsed so far, and a runtime. None of that needs burgee's
|
|
8
|
+
* types, so nothing here imports them — which keeps the family's rule that no package
|
|
9
|
+
* requires another, and means `burgee`, `burgee/commander` and `burgee/yargs` share one
|
|
10
|
+
* implementation rather than three that drift.
|
|
11
|
+
*
|
|
12
|
+
* The order is the declaration order, because `--interactive` asks for several things at
|
|
13
|
+
* once and a person answering them needs the sequence to match the help they just read.
|
|
14
|
+
*/
|
|
15
|
+
import { type Io } from './ask.js';
|
|
16
|
+
import { type Flags, type Runtime } from './decide.js';
|
|
17
|
+
import { type PromptSpec } from './spec.js';
|
|
18
|
+
/** What a host tells us about one option. Structural: any framework's spec satisfies it. */
|
|
19
|
+
export interface PromptableOption {
|
|
20
|
+
required?: boolean;
|
|
21
|
+
prompt?: PromptSpec;
|
|
22
|
+
}
|
|
23
|
+
export interface ResolveInput<O extends PromptableOption = PromptableOption> {
|
|
24
|
+
/** The command's options by name, in declaration order — a record preserves it. */
|
|
25
|
+
options: Record<string, O>;
|
|
26
|
+
/** What the values are after every other source has been consulted. */
|
|
27
|
+
values: Record<string, unknown>;
|
|
28
|
+
runtime: Runtime;
|
|
29
|
+
flags?: Flags;
|
|
30
|
+
io: Io;
|
|
31
|
+
}
|
|
32
|
+
export interface ResolveFailure {
|
|
33
|
+
option: string;
|
|
34
|
+
code: 'USAGE' | 'CANCELLED';
|
|
35
|
+
message: string;
|
|
36
|
+
fix?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface Resolved {
|
|
39
|
+
/** The values with every answered prompt written in. The input is not mutated. */
|
|
40
|
+
values: Record<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* The first refusal, or nothing. First rather than all: the caller is about to exit, and
|
|
43
|
+
* a person told about six missing flags one of which they will answer interactively has
|
|
44
|
+
* been given a worse message than one told about the first.
|
|
45
|
+
*/
|
|
46
|
+
failure?: ResolveFailure;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Walk the command's options in order, asking only what has to be asked.
|
|
50
|
+
*
|
|
51
|
+
* Stops at the first refusal: the caller is about to exit, and a person told about six
|
|
52
|
+
* missing flags — one of which they would have answered interactively — has been given a
|
|
53
|
+
* worse message than one told about the first.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolvePrompts<O extends PromptableOption>({ options, values, runtime, flags, io }: ResolveInput<O>): Promise<Resolved>;
|
package/dist/binding.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a whole command's prompts in one pass (R2, R3) — the piece a framework calls
|
|
3
|
+
* from its `preAction` hook, after the environment and config layers have had their turn.
|
|
4
|
+
*
|
|
5
|
+
* The design sketched this as `caique/burgee`, one binding per host. It is one binding for
|
|
6
|
+
* all of them instead, and that is the better answer: what a host actually supplies is a
|
|
7
|
+
* record of options, the values parsed so far, and a runtime. None of that needs burgee's
|
|
8
|
+
* types, so nothing here imports them — which keeps the family's rule that no package
|
|
9
|
+
* requires another, and means `burgee`, `burgee/commander` and `burgee/yargs` share one
|
|
10
|
+
* implementation rather than three that drift.
|
|
11
|
+
*
|
|
12
|
+
* The order is the declaration order, because `--interactive` asks for several things at
|
|
13
|
+
* once and a person answering them needs the sequence to match the help they just read.
|
|
14
|
+
*/
|
|
15
|
+
import { ask } from './ask.js';
|
|
16
|
+
import { decide } from './decide.js';
|
|
17
|
+
import { problemWith } from './spec.js';
|
|
18
|
+
/**
|
|
19
|
+
* One option's verdict, before any terminal is consulted. A spec that cannot be drawn is
|
|
20
|
+
* caught here rather than in the widget: better to say so than to draw an empty list and
|
|
21
|
+
* wait, which is the hang this package exists to prevent.
|
|
22
|
+
*/
|
|
23
|
+
function verdictFor({ option, prompt, required, value, runtime, flags }) {
|
|
24
|
+
const malformed = problemWith(prompt);
|
|
25
|
+
if (malformed !== undefined)
|
|
26
|
+
return { option, code: 'USAGE', message: `--${option} has a prompt that cannot be drawn: ${malformed}`, fix: 'fix the prompt spec where the option is declared' };
|
|
27
|
+
const verdict = decide({ value, spec: prompt, option, runtime, ...(flags === undefined ? {} : { flags }), required });
|
|
28
|
+
if (verdict.action === 'error')
|
|
29
|
+
return { option, code: 'USAGE', message: verdict.message ?? '', ...(verdict.fix === undefined ? {} : { fix: verdict.fix }) };
|
|
30
|
+
return verdict;
|
|
31
|
+
}
|
|
32
|
+
const isFailure = (v) => !('action' in v);
|
|
33
|
+
/**
|
|
34
|
+
* Walk the command's options in order, asking only what has to be asked.
|
|
35
|
+
*
|
|
36
|
+
* Stops at the first refusal: the caller is about to exit, and a person told about six
|
|
37
|
+
* missing flags — one of which they would have answered interactively — has been given a
|
|
38
|
+
* worse message than one told about the first.
|
|
39
|
+
*/
|
|
40
|
+
export async function resolvePrompts({ options, values, runtime, flags, io }) {
|
|
41
|
+
const out = { ...values };
|
|
42
|
+
for (const [option, spec] of Object.entries(options)) {
|
|
43
|
+
const prompt = spec.prompt;
|
|
44
|
+
if (prompt === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
const verdict = verdictFor({ option, prompt, required: spec.required === true, value: out[option], runtime, flags });
|
|
47
|
+
if (isFailure(verdict))
|
|
48
|
+
return { values: out, failure: verdict };
|
|
49
|
+
if (verdict.action === 'skip')
|
|
50
|
+
continue;
|
|
51
|
+
if (verdict.action === 'answer') {
|
|
52
|
+
out[option] = verdict.value;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Asked one at a time on purpose: a person answers in sequence, and asking the second
|
|
56
|
+
// question before the first is answered would interleave two prompts on one terminal.
|
|
57
|
+
// eslint-disable-next-line reliability/no-await-in-loop -- see above
|
|
58
|
+
const answer = await ask(prompt, io);
|
|
59
|
+
if (!answer.ok) {
|
|
60
|
+
return { values: out, failure: { option, code: 'CANCELLED', message: `cancelled at --${option}`, fix: `pass --${option} to skip the question` } };
|
|
61
|
+
}
|
|
62
|
+
out[option] = answer.value;
|
|
63
|
+
}
|
|
64
|
+
return { values: out };
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=binding.js.map
|
package/dist/decide.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether to prompt at all (R2, R3, R6) — the one decision this package exists to get
|
|
3
|
+
* right, and the only one that has to be correct before a single character is drawn.
|
|
4
|
+
*
|
|
5
|
+
* **An agent asked to type is an agent that hangs.** Every prompt library assumes a person
|
|
6
|
+
* is there; when one is not, the process waits on a stdin that will never produce a line,
|
|
7
|
+
* and the caller sees a timeout with no output and no clue. That is the failure this
|
|
8
|
+
* function refuses to allow: with no terminal, a missing value is an *error naming the
|
|
9
|
+
* flag*, immediately, in words the caller can act on.
|
|
10
|
+
*
|
|
11
|
+
* It is pure — a value, a runtime slice and the flags in, a verdict out — so the whole
|
|
12
|
+
* truth table is a unit test rather than a PTY. `decide.test.ts` walks all of it.
|
|
13
|
+
*/
|
|
14
|
+
import { type PromptSpec } from './spec.js';
|
|
15
|
+
/** The slice of a runtime this needs. burgee's satisfies it; so does a literal in a test. */
|
|
16
|
+
export interface Runtime {
|
|
17
|
+
env: Record<string, string | undefined>;
|
|
18
|
+
isTTY: {
|
|
19
|
+
stdin: boolean;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** What the run was asked for, as the engine knows it — never sniffed from the process. */
|
|
23
|
+
export interface Flags {
|
|
24
|
+
/** `--json`: a machine is reading stdout, so no one is here to type (R6). */
|
|
25
|
+
json?: boolean;
|
|
26
|
+
/** `--yes`: every `confirm` is already answered true (R3). */
|
|
27
|
+
yes?: boolean;
|
|
28
|
+
/** `--interactive`: prompt for what is missing even where we would not have (R3). */
|
|
29
|
+
interactive?: boolean;
|
|
30
|
+
/** `--interactive=all`: prompt for every promptable option, not only the required ones. */
|
|
31
|
+
interactiveAll?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface Decision {
|
|
34
|
+
action: 'skip' | 'prompt' | 'answer' | 'error';
|
|
35
|
+
/** For `answer`: what to use without asking. Today only `--yes` produces one. */
|
|
36
|
+
value?: boolean;
|
|
37
|
+
/** For `error`: an E1 code the caller maps to its own error type. */
|
|
38
|
+
code?: 'USAGE';
|
|
39
|
+
message?: string;
|
|
40
|
+
/** For `error`: the one sentence that turns a refusal into a next step. */
|
|
41
|
+
fix?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface DecideInput {
|
|
44
|
+
/** Whatever the option resolved to before prompting, from any source. */
|
|
45
|
+
value: unknown;
|
|
46
|
+
spec: PromptSpec;
|
|
47
|
+
/** The option's long name, for the flag a refusal names. */
|
|
48
|
+
option: string;
|
|
49
|
+
runtime: Runtime;
|
|
50
|
+
flags?: Flags;
|
|
51
|
+
/** Whether the option must have a value for the command to run. */
|
|
52
|
+
required?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The rule, in order, and the order is the argument:
|
|
56
|
+
*
|
|
57
|
+
* 1. A value from any source wins. Prompting for something already answered is how a
|
|
58
|
+
* script that sets an env var still ends up waiting for input.
|
|
59
|
+
* 2. `--json` never prompts. It means "a machine is reading this", and there is no
|
|
60
|
+
* answer a machine can type.
|
|
61
|
+
* 3. `--yes` answers a `confirm`, and only a `confirm` — it is not a licence to invent
|
|
62
|
+
* a path or a password.
|
|
63
|
+
* 4. No terminal on stdin, or `CI` set, means nobody is there: refuse, naming the flag.
|
|
64
|
+
* 5. `--interactive` reaches past 4 only when there *is* a terminal; it is an override
|
|
65
|
+
* for "you would not have asked", never for "there is no one to ask".
|
|
66
|
+
* 6. Otherwise, if it is required or `--interactive` was asked for, prompt.
|
|
67
|
+
*/
|
|
68
|
+
export declare function decide({ value, spec, option, runtime, flags, required }: DecideInput): Decision;
|
package/dist/decide.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether to prompt at all (R2, R3, R6) — the one decision this package exists to get
|
|
3
|
+
* right, and the only one that has to be correct before a single character is drawn.
|
|
4
|
+
*
|
|
5
|
+
* **An agent asked to type is an agent that hangs.** Every prompt library assumes a person
|
|
6
|
+
* is there; when one is not, the process waits on a stdin that will never produce a line,
|
|
7
|
+
* and the caller sees a timeout with no output and no clue. That is the failure this
|
|
8
|
+
* function refuses to allow: with no terminal, a missing value is an *error naming the
|
|
9
|
+
* flag*, immediately, in words the caller can act on.
|
|
10
|
+
*
|
|
11
|
+
* It is pure — a value, a runtime slice and the flags in, a verdict out — so the whole
|
|
12
|
+
* truth table is a unit test rather than a PTY. `decide.test.ts` walks all of it.
|
|
13
|
+
*/
|
|
14
|
+
import { flagOf } from './spec.js';
|
|
15
|
+
const SKIP = { action: 'skip' };
|
|
16
|
+
const PROMPT = { action: 'prompt' };
|
|
17
|
+
/** Present and not empty — the same convention roundel's policy applies to every switch. */
|
|
18
|
+
const set = (value) => value !== undefined && value !== '';
|
|
19
|
+
/** A value from any source at all: a flag, an env var, a config file, a default. */
|
|
20
|
+
function alreadyAnswered(value) {
|
|
21
|
+
// `false` and `0` and `''` are answers. Only "nothing was supplied" is not.
|
|
22
|
+
return value !== undefined && value !== null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The rule, in order, and the order is the argument:
|
|
26
|
+
*
|
|
27
|
+
* 1. A value from any source wins. Prompting for something already answered is how a
|
|
28
|
+
* script that sets an env var still ends up waiting for input.
|
|
29
|
+
* 2. `--json` never prompts. It means "a machine is reading this", and there is no
|
|
30
|
+
* answer a machine can type.
|
|
31
|
+
* 3. `--yes` answers a `confirm`, and only a `confirm` — it is not a licence to invent
|
|
32
|
+
* a path or a password.
|
|
33
|
+
* 4. No terminal on stdin, or `CI` set, means nobody is there: refuse, naming the flag.
|
|
34
|
+
* 5. `--interactive` reaches past 4 only when there *is* a terminal; it is an override
|
|
35
|
+
* for "you would not have asked", never for "there is no one to ask".
|
|
36
|
+
* 6. Otherwise, if it is required or `--interactive` was asked for, prompt.
|
|
37
|
+
*/
|
|
38
|
+
export function decide({ value, spec, option, runtime, flags = {}, required = false }) {
|
|
39
|
+
if (alreadyAnswered(value))
|
|
40
|
+
return SKIP;
|
|
41
|
+
const flag = flagOf(option);
|
|
42
|
+
const interactive = flags.interactive === true || flags.interactiveAll === true;
|
|
43
|
+
if (flags.json === true) {
|
|
44
|
+
return {
|
|
45
|
+
action: 'error',
|
|
46
|
+
code: 'USAGE',
|
|
47
|
+
message: `${flag} is required under --json`,
|
|
48
|
+
fix: `pass ${flag}; --json means no one is here to answer "${spec.message}"`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (flags.yes === true && spec.kind === 'confirm')
|
|
52
|
+
return { action: 'answer', value: true };
|
|
53
|
+
const nobodyThere = !runtime.isTTY.stdin || set(runtime.env['CI']);
|
|
54
|
+
if (nobodyThere) {
|
|
55
|
+
// `--interactive` cannot conjure a person. Saying so is the difference between a
|
|
56
|
+
// useful refusal and a flag that looks like it did nothing.
|
|
57
|
+
const because = interactive ? ' (--interactive needs a terminal on stdin)' : '';
|
|
58
|
+
return {
|
|
59
|
+
action: 'error',
|
|
60
|
+
code: 'USAGE',
|
|
61
|
+
message: `${flag} is required when there is no terminal${because}`,
|
|
62
|
+
fix: `pass ${flag}; it would have been asked as "${spec.message}"`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (required || interactive)
|
|
66
|
+
return PROMPT;
|
|
67
|
+
return SKIP;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=decide.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* caique —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* caique — prompts that are flags first. The first working slice is `decide()`: the pure
|
|
3
|
+
* rule that decides whether a person can be asked at all, which is what keeps a CLI built
|
|
4
|
+
* on this from hanging when an agent runs it. Widgets and the two façades follow; see
|
|
5
|
+
* `.sdlc/intents/caique/`.
|
|
5
6
|
*/
|
|
6
|
-
export
|
|
7
|
+
export * from './ask.js';
|
|
8
|
+
export * from './binding.js';
|
|
9
|
+
export * from './decide.js';
|
|
10
|
+
export * from './raw.js';
|
|
11
|
+
export * from './spec.js';
|
|
12
|
+
export * from './terminal.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* caique —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* caique — prompts that are flags first. The first working slice is `decide()`: the pure
|
|
3
|
+
* rule that decides whether a person can be asked at all, which is what keeps a CLI built
|
|
4
|
+
* on this from hanging when an agent runs it. Widgets and the two façades follow; see
|
|
5
|
+
* `.sdlc/intents/caique/`.
|
|
5
6
|
*/
|
|
6
|
-
export
|
|
7
|
+
export * from './ask.js';
|
|
8
|
+
export * from './binding.js';
|
|
9
|
+
export * from './decide.js';
|
|
10
|
+
export * from './raw.js';
|
|
11
|
+
export * from './spec.js';
|
|
12
|
+
export * from './terminal.js';
|
|
7
13
|
//# sourceMappingURL=index.js.map
|
package/dist/raw.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The raw-mode renderer: arrow keys and a moving highlight for `select` and `multiselect`,
|
|
3
|
+
* on a terminal that can take them.
|
|
4
|
+
*
|
|
5
|
+
* **It answers the same questions `ask()` does, and returns the same answers.** That is the
|
|
6
|
+
* whole arrangement: line mode is the floor (R5), this sits on top, and a caller chooses
|
|
7
|
+
* between them by asking whether the terminal is one. Anything this can do that line mode
|
|
8
|
+
* cannot is decoration; anything line mode can do that this cannot would be a bug.
|
|
9
|
+
*
|
|
10
|
+
* **On the dependency the design named.** It said "spinner from flagstaff", and this does
|
|
11
|
+
* not import flagstaff. A prompt has no spinner — it is waiting for a person, not for work
|
|
12
|
+
* — and the repaint it needs is three escape sequences, written here. Importing flagstaff
|
|
13
|
+
* to get them would make the one package that talks to a human the only one in the family
|
|
14
|
+
* that requires a sibling, which is the rule `caique`'s own README states. If a prompt ever
|
|
15
|
+
* needs to show progress *while* it waits, that is a caller composing `hoist()` around
|
|
16
|
+
* `ask()`, not this file reaching for it.
|
|
17
|
+
*/
|
|
18
|
+
import { type Asked, type Io } from './ask.js';
|
|
19
|
+
import { type Choice, type PromptSpec } from './spec.js';
|
|
20
|
+
/** A stream that can be put into raw mode and read a key at a time. */
|
|
21
|
+
export interface KeyStream {
|
|
22
|
+
isTTY?: boolean;
|
|
23
|
+
setRawMode?(raw: boolean): unknown;
|
|
24
|
+
on(event: 'data', listener: (chunk: Buffer | string) => void): unknown;
|
|
25
|
+
off(event: 'data', listener: (chunk: Buffer | string) => void): unknown;
|
|
26
|
+
resume?(): unknown;
|
|
27
|
+
pause?(): unknown;
|
|
28
|
+
}
|
|
29
|
+
export type Key = 'up' | 'down' | 'space' | 'enter' | 'cancel' | 'other';
|
|
30
|
+
/**
|
|
31
|
+
* What a keypress means. Only the six that drive a list — everything else is `other`, and
|
|
32
|
+
* a widget that does not know what to do with a key does nothing, which is what a person
|
|
33
|
+
* expects from a key they pressed by accident.
|
|
34
|
+
*/
|
|
35
|
+
export declare function keyOf(data: string): Key;
|
|
36
|
+
export interface RawIo extends Io {
|
|
37
|
+
keys: KeyStream;
|
|
38
|
+
}
|
|
39
|
+
/** Whether this runtime can drive the raw renderer at all. */
|
|
40
|
+
export declare function canRender(keys: KeyStream): boolean;
|
|
41
|
+
interface ListState {
|
|
42
|
+
cursor: number;
|
|
43
|
+
selected: Set<number>;
|
|
44
|
+
}
|
|
45
|
+
/** One frame of the list. Exported so a test asserts the drawing rather than a screenshot. */
|
|
46
|
+
export declare function renderList(spec: PromptSpec, choices: Choice[], state: ListState, multi: boolean): string;
|
|
47
|
+
/**
|
|
48
|
+
* Drive a list prompt with the arrow keys, repainting in place.
|
|
49
|
+
*
|
|
50
|
+
* Returns the same `Asked` shape `ask()` does, so a caller can swap the two without
|
|
51
|
+
* knowing which ran — and cancels the same way, because `Ctrl-C` in raw mode is a byte and
|
|
52
|
+
* not a signal, and a person who presses it means to leave.
|
|
53
|
+
*/
|
|
54
|
+
export declare function askList(spec: PromptSpec, io: RawIo, multi?: boolean): Promise<Asked>;
|
|
55
|
+
export {};
|
package/dist/raw.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The raw-mode renderer: arrow keys and a moving highlight for `select` and `multiselect`,
|
|
3
|
+
* on a terminal that can take them.
|
|
4
|
+
*
|
|
5
|
+
* **It answers the same questions `ask()` does, and returns the same answers.** That is the
|
|
6
|
+
* whole arrangement: line mode is the floor (R5), this sits on top, and a caller chooses
|
|
7
|
+
* between them by asking whether the terminal is one. Anything this can do that line mode
|
|
8
|
+
* cannot is decoration; anything line mode can do that this cannot would be a bug.
|
|
9
|
+
*
|
|
10
|
+
* **On the dependency the design named.** It said "spinner from flagstaff", and this does
|
|
11
|
+
* not import flagstaff. A prompt has no spinner — it is waiting for a person, not for work
|
|
12
|
+
* — and the repaint it needs is three escape sequences, written here. Importing flagstaff
|
|
13
|
+
* to get them would make the one package that talks to a human the only one in the family
|
|
14
|
+
* that requires a sibling, which is the rule `caique`'s own README states. If a prompt ever
|
|
15
|
+
* needs to show progress *while* it waits, that is a caller composing `hoist()` around
|
|
16
|
+
* `ask()`, not this file reaching for it.
|
|
17
|
+
*/
|
|
18
|
+
import {} from './ask.js';
|
|
19
|
+
import {} from './spec.js';
|
|
20
|
+
const ESC = '\u001B';
|
|
21
|
+
const CSI = `${ESC}[`;
|
|
22
|
+
const HIDE_CURSOR = `${CSI}?25l`;
|
|
23
|
+
const SHOW_CURSOR = `${CSI}?25h`;
|
|
24
|
+
/** Column 1, up `n` lines, clear to the end of the screen — the only repaint this needs. */
|
|
25
|
+
const erase = (lines) => `${CSI}1G${lines > 1 ? `${CSI}${lines - 1}A` : ''}${CSI}0J`;
|
|
26
|
+
/**
|
|
27
|
+
* What a keypress means. Only the six that drive a list — everything else is `other`, and
|
|
28
|
+
* a widget that does not know what to do with a key does nothing, which is what a person
|
|
29
|
+
* expects from a key they pressed by accident.
|
|
30
|
+
*/
|
|
31
|
+
export function keyOf(data) {
|
|
32
|
+
if (data === `${CSI}A` || data === 'k')
|
|
33
|
+
return 'up';
|
|
34
|
+
if (data === `${CSI}B` || data === 'j')
|
|
35
|
+
return 'down';
|
|
36
|
+
if (data === ' ')
|
|
37
|
+
return 'space';
|
|
38
|
+
if (data === '\r' || data === '\n')
|
|
39
|
+
return 'enter';
|
|
40
|
+
// Ctrl-C and Ctrl-D. In raw mode the terminal delivers these as bytes rather than
|
|
41
|
+
// signals, so a widget that did not read them would leave a person unable to leave.
|
|
42
|
+
if (data === '\u0003' || data === '\u0004' || data === ESC)
|
|
43
|
+
return 'cancel';
|
|
44
|
+
return 'other';
|
|
45
|
+
}
|
|
46
|
+
/** Whether this runtime can drive the raw renderer at all. */
|
|
47
|
+
export function canRender(keys) {
|
|
48
|
+
return keys.isTTY === true && typeof keys.setRawMode === 'function';
|
|
49
|
+
}
|
|
50
|
+
const MARK = { on: '◉', off: '◯' };
|
|
51
|
+
const POINTER = '❯';
|
|
52
|
+
const CANCELLED = { ok: false, reason: 'cancelled' };
|
|
53
|
+
/** The `◉`/`◯` column, which only a multiselect has. Empty for a single select. */
|
|
54
|
+
function markFor(state, index, multi) {
|
|
55
|
+
if (!multi)
|
|
56
|
+
return '';
|
|
57
|
+
return `${state.selected.has(index) ? MARK.on : MARK.off} `;
|
|
58
|
+
}
|
|
59
|
+
/** One frame of the list. Exported so a test asserts the drawing rather than a screenshot. */
|
|
60
|
+
export function renderList(spec, choices, state, multi) {
|
|
61
|
+
const rows = choices.map((choice, index) => {
|
|
62
|
+
const pointer = index === state.cursor ? POINTER : ' ';
|
|
63
|
+
const hint = choice.hint === undefined ? '' : ` — ${choice.hint}`;
|
|
64
|
+
return `${pointer} ${markFor(state, index, multi)}${choice.label ?? choice.value}${hint}`;
|
|
65
|
+
});
|
|
66
|
+
return [spec.message, ...rows].join('\n');
|
|
67
|
+
}
|
|
68
|
+
const clamp = (index, length) => (index + length) % length;
|
|
69
|
+
/**
|
|
70
|
+
* Apply a navigation key, and say whether anything changed — a key with no meaning here
|
|
71
|
+
* changes nothing and repaints nothing, which is what a person expects from a key they
|
|
72
|
+
* pressed by accident.
|
|
73
|
+
*/
|
|
74
|
+
function moved(key, state, length, multi) {
|
|
75
|
+
if (key === 'up') {
|
|
76
|
+
state.cursor = clamp(state.cursor - 1, length);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (key === 'down') {
|
|
80
|
+
state.cursor = clamp(state.cursor + 1, length);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (key !== 'space' || !multi)
|
|
84
|
+
return false;
|
|
85
|
+
if (state.selected.has(state.cursor))
|
|
86
|
+
state.selected.delete(state.cursor);
|
|
87
|
+
else
|
|
88
|
+
state.selected.add(state.cursor);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/** What enter answers with. List order, not press order: a set of choices has no sequence. */
|
|
92
|
+
function chosen(choices, state, multi) {
|
|
93
|
+
if (!multi)
|
|
94
|
+
return choices[state.cursor]?.value ?? '';
|
|
95
|
+
return [...state.selected].sort((a, b) => a - b).map((index) => choices[index]?.value ?? '');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Drive a list prompt with the arrow keys, repainting in place.
|
|
99
|
+
*
|
|
100
|
+
* Returns the same `Asked` shape `ask()` does, so a caller can swap the two without
|
|
101
|
+
* knowing which ran — and cancels the same way, because `Ctrl-C` in raw mode is a byte and
|
|
102
|
+
* not a signal, and a person who presses it means to leave.
|
|
103
|
+
*/
|
|
104
|
+
export async function askList(spec, io, multi = false) {
|
|
105
|
+
const choices = spec.choices ?? [];
|
|
106
|
+
const state = { cursor: 0, selected: new Set() };
|
|
107
|
+
let painted = 0;
|
|
108
|
+
const paint = () => {
|
|
109
|
+
const frame = renderList(spec, choices, state, multi);
|
|
110
|
+
io.writer.write((painted === 0 ? HIDE_CURSOR : erase(painted)) + frame);
|
|
111
|
+
painted = frame.split('\n').length;
|
|
112
|
+
};
|
|
113
|
+
io.keys.setRawMode?.(true);
|
|
114
|
+
io.keys.resume?.();
|
|
115
|
+
paint();
|
|
116
|
+
try {
|
|
117
|
+
return await new Promise((resolve) => {
|
|
118
|
+
const onData = (chunk) => {
|
|
119
|
+
const key = keyOf(String(chunk));
|
|
120
|
+
if (key === 'cancel') {
|
|
121
|
+
done(CANCELLED);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (key === 'enter') {
|
|
125
|
+
done({ ok: true, value: chosen(choices, state, multi) });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (moved(key, state, choices.length, multi))
|
|
129
|
+
paint();
|
|
130
|
+
};
|
|
131
|
+
const done = (answer) => {
|
|
132
|
+
io.keys.off('data', onData);
|
|
133
|
+
// Leave the answered question on screen, the cursor back, and the terminal as it
|
|
134
|
+
// was found: a prompt that exits in raw mode leaves the shell unusable.
|
|
135
|
+
io.writer.write(`${erase(painted)}${renderList(spec, choices, state, multi)}\n${SHOW_CURSOR}`);
|
|
136
|
+
resolve(answer);
|
|
137
|
+
};
|
|
138
|
+
io.keys.on('data', onData);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
io.keys.setRawMode?.(false);
|
|
143
|
+
io.keys.pause?.();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=raw.js.map
|
package/dist/spec.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a prompt *is*, before anything draws one (R1).
|
|
3
|
+
*
|
|
4
|
+
* A `PromptSpec` hangs off an option, not off a call site, and that is the whole
|
|
5
|
+
* inversion: the option is the thing that exists in the manifest, in `--help`, in the MCP
|
|
6
|
+
* tool schema and on the command line, and the prompt is one more projection of it. A
|
|
7
|
+
* program written this way can be answered by a flag, by an environment variable, by a
|
|
8
|
+
* config file or by a person, and nothing in it has to know which happened.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here imports a runtime, a stream or a terminal. This file is data.
|
|
11
|
+
*/
|
|
12
|
+
/** The six kinds a prompt can be. Anything more is a wizard, which is out of scope. */
|
|
13
|
+
export type PromptKind = 'text' | 'confirm' | 'select' | 'multiselect' | 'password' | 'path';
|
|
14
|
+
/** One choice in a `select` or `multiselect`. `value` is what the option receives. */
|
|
15
|
+
export interface Choice {
|
|
16
|
+
value: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
hint?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PromptSpec {
|
|
21
|
+
kind: PromptKind;
|
|
22
|
+
/** What a person is asked. Also what an agent reads in the refusal when it cannot be asked. */
|
|
23
|
+
message: string;
|
|
24
|
+
/** Offered as the answer if the person just presses return. */
|
|
25
|
+
initial?: string | boolean | string[];
|
|
26
|
+
/** Required for `select` and `multiselect`; meaningless for the rest. */
|
|
27
|
+
choices?: Choice[];
|
|
28
|
+
/** Returns a message when the answer is unacceptable, or nothing when it is fine. */
|
|
29
|
+
validate?: (value: string) => string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** A prompt spec bound to the option it answers. */
|
|
32
|
+
export interface BoundPrompt extends PromptSpec {
|
|
33
|
+
/** The option's long name, without dashes — `output-dir`, not `--output-dir`. */
|
|
34
|
+
option: string;
|
|
35
|
+
}
|
|
36
|
+
/** `--output-dir`, which is what a refusal has to say to be actionable. */
|
|
37
|
+
export declare function flagOf(option: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* A `select` without choices, or a `confirm` with them, is a spec that cannot be drawn.
|
|
40
|
+
* Caught here rather than in the widget, so a program with a malformed prompt fails on the
|
|
41
|
+
* first run instead of the first time someone reaches that option.
|
|
42
|
+
*/
|
|
43
|
+
export declare function problemWith(spec: PromptSpec): string | undefined;
|
package/dist/spec.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a prompt *is*, before anything draws one (R1).
|
|
3
|
+
*
|
|
4
|
+
* A `PromptSpec` hangs off an option, not off a call site, and that is the whole
|
|
5
|
+
* inversion: the option is the thing that exists in the manifest, in `--help`, in the MCP
|
|
6
|
+
* tool schema and on the command line, and the prompt is one more projection of it. A
|
|
7
|
+
* program written this way can be answered by a flag, by an environment variable, by a
|
|
8
|
+
* config file or by a person, and nothing in it has to know which happened.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here imports a runtime, a stream or a terminal. This file is data.
|
|
11
|
+
*/
|
|
12
|
+
/** `--output-dir`, which is what a refusal has to say to be actionable. */
|
|
13
|
+
export function flagOf(option) {
|
|
14
|
+
return `--${option}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A `select` without choices, or a `confirm` with them, is a spec that cannot be drawn.
|
|
18
|
+
* Caught here rather than in the widget, so a program with a malformed prompt fails on the
|
|
19
|
+
* first run instead of the first time someone reaches that option.
|
|
20
|
+
*/
|
|
21
|
+
export function problemWith(spec) {
|
|
22
|
+
const needsChoices = spec.kind === 'select' || spec.kind === 'multiselect';
|
|
23
|
+
if (needsChoices && (spec.choices === undefined || spec.choices.length === 0))
|
|
24
|
+
return `a ${spec.kind} prompt needs a non-empty \`choices\` array`;
|
|
25
|
+
if (!needsChoices && spec.choices !== undefined)
|
|
26
|
+
return `a ${spec.kind} prompt has no use for \`choices\``;
|
|
27
|
+
if (spec.message.trim() === '')
|
|
28
|
+
return 'a prompt needs a message: it is what a person is asked, and what an agent is told when it cannot be';
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Io } from './ask.js';
|
|
2
|
+
/** The stream pair a terminal Io is built over: `process.stdin` and `process.stdout`. */
|
|
3
|
+
export interface Streams {
|
|
4
|
+
input: NodeJS.ReadableStream & {
|
|
5
|
+
isTTY?: boolean;
|
|
6
|
+
};
|
|
7
|
+
output: NodeJS.WritableStream & {
|
|
8
|
+
isTTY?: boolean;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A reader and writer over a real stream pair.
|
|
13
|
+
*
|
|
14
|
+
* The reader resolves `undefined` when the stream ends, which `ask()` reads as a
|
|
15
|
+
* cancellation — `Ctrl-D` and a closed pipe both arrive that way, and both mean nobody is
|
|
16
|
+
* going to type.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createIo(streams: Streams): Io & {
|
|
19
|
+
close: () => void;
|
|
20
|
+
};
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `Reader` and `Writer` over real streams — the one file in this package that touches a
|
|
3
|
+
* terminal, and the reason `ask()` can be used by a program rather than only by a test.
|
|
4
|
+
*
|
|
5
|
+
* Everything above this is strings in and strings out. That is deliberate: the widgets, the
|
|
6
|
+
* decision and the binding are all testable without a PTY, and this is the thin layer that
|
|
7
|
+
* has to be got right once. It reads lines through `node:readline`, which handles the
|
|
8
|
+
* line editing, the backspace and the `Ctrl-D` a person expects.
|
|
9
|
+
*
|
|
10
|
+
* **Hiding a password happens here**, because this is the only layer that knows what echo
|
|
11
|
+
* is. `ask()` says which prompts are hidden; nothing above has to remember to mute anything,
|
|
12
|
+
* and a widget cannot leak a secret by writing it, since no widget writes what it read.
|
|
13
|
+
*/
|
|
14
|
+
import { createInterface } from 'node:readline';
|
|
15
|
+
import {} from './ask.js';
|
|
16
|
+
/** Accepts a chunk and writes nothing: what a muted stream's `write` does. */
|
|
17
|
+
const swallow = () => true;
|
|
18
|
+
/**
|
|
19
|
+
* `readline` echoes what it reads. For a hidden answer the echo is suppressed by
|
|
20
|
+
* intercepting the interface's own output for the duration of the question — not by
|
|
21
|
+
* turning the terminal's echo off, which would leave it off if the process died mid-prompt.
|
|
22
|
+
*/
|
|
23
|
+
function mute(rl, streams) {
|
|
24
|
+
const target = rl.output ?? streams.output;
|
|
25
|
+
const original = target.write.bind(target);
|
|
26
|
+
// `readline` writes the prompt through the same stream it echoes through, so the prompt
|
|
27
|
+
// has already been written by the time this is installed: everything after it is input.
|
|
28
|
+
target.write = swallow;
|
|
29
|
+
return () => {
|
|
30
|
+
target.write = original;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A reader and writer over a real stream pair.
|
|
35
|
+
*
|
|
36
|
+
* The reader resolves `undefined` when the stream ends, which `ask()` reads as a
|
|
37
|
+
* cancellation — `Ctrl-D` and a closed pipe both arrive that way, and both mean nobody is
|
|
38
|
+
* going to type.
|
|
39
|
+
*/
|
|
40
|
+
export function createIo(streams) {
|
|
41
|
+
const rl = createInterface({ input: streams.input, output: streams.output, terminal: streams.output.isTTY === true });
|
|
42
|
+
let ended = false;
|
|
43
|
+
rl.once('close', () => {
|
|
44
|
+
ended = true;
|
|
45
|
+
});
|
|
46
|
+
const reader = {
|
|
47
|
+
line: async ({ hidden = false } = {}) => {
|
|
48
|
+
if (ended)
|
|
49
|
+
return undefined;
|
|
50
|
+
const unmute = hidden ? mute(rl, streams) : undefined;
|
|
51
|
+
try {
|
|
52
|
+
return await new Promise((resolve) => {
|
|
53
|
+
const onLine = (value) => {
|
|
54
|
+
rl.off('close', onClose);
|
|
55
|
+
resolve(value);
|
|
56
|
+
};
|
|
57
|
+
const onClose = () => {
|
|
58
|
+
rl.off('line', onLine);
|
|
59
|
+
resolve(undefined);
|
|
60
|
+
};
|
|
61
|
+
rl.once('line', onLine);
|
|
62
|
+
rl.once('close', onClose);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
unmute?.();
|
|
67
|
+
// The newline the person typed was swallowed with the echo; put it back so the
|
|
68
|
+
// next question does not start on the same line as the hidden answer.
|
|
69
|
+
if (hidden)
|
|
70
|
+
streams.output.write('\n');
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
const writer = {
|
|
75
|
+
write: (text) => {
|
|
76
|
+
streams.output.write(text);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return { reader, writer, close: () => rl.close() };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=terminal.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caique",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "The parrot that always answers back, and the boat that goes between ship and shore. Prompts that are flags first, so agents answer before they are asked and non-TTY callers get an error naming the flag, never a hang. Drop-in path for inquirer and clack.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,36 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"import": "./dist/index.js",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./ask": {
|
|
17
|
+
"types": "./dist/ask.d.ts",
|
|
18
|
+
"import": "./dist/ask.js",
|
|
19
|
+
"default": "./dist/ask.js"
|
|
20
|
+
},
|
|
21
|
+
"./binding": {
|
|
22
|
+
"types": "./dist/binding.d.ts",
|
|
23
|
+
"import": "./dist/binding.js",
|
|
24
|
+
"default": "./dist/binding.js"
|
|
25
|
+
},
|
|
26
|
+
"./decide": {
|
|
27
|
+
"types": "./dist/decide.d.ts",
|
|
28
|
+
"import": "./dist/decide.js",
|
|
29
|
+
"default": "./dist/decide.js"
|
|
30
|
+
},
|
|
31
|
+
"./raw": {
|
|
32
|
+
"types": "./dist/raw.d.ts",
|
|
33
|
+
"import": "./dist/raw.js",
|
|
34
|
+
"default": "./dist/raw.js"
|
|
35
|
+
},
|
|
36
|
+
"./spec": {
|
|
37
|
+
"types": "./dist/spec.d.ts",
|
|
38
|
+
"import": "./dist/spec.js",
|
|
39
|
+
"default": "./dist/spec.js"
|
|
40
|
+
},
|
|
41
|
+
"./terminal": {
|
|
42
|
+
"types": "./dist/terminal.d.ts",
|
|
43
|
+
"import": "./dist/terminal.js",
|
|
44
|
+
"default": "./dist/terminal.js"
|
|
15
45
|
}
|
|
16
46
|
},
|
|
17
47
|
"files": [
|
|
@@ -23,6 +53,7 @@
|
|
|
23
53
|
"build": "tsc -p tsconfig.build.json",
|
|
24
54
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
55
|
"test": "vitest run --passWithNoTests",
|
|
56
|
+
"coverage": "vitest run --coverage.enabled",
|
|
26
57
|
"lint": "eslint src"
|
|
27
58
|
},
|
|
28
59
|
"repository": {
|
|
@@ -46,6 +77,6 @@
|
|
|
46
77
|
"non-tty"
|
|
47
78
|
],
|
|
48
79
|
"devDependencies": {
|
|
49
|
-
"vitest": "^
|
|
80
|
+
"vitest": "^5.0.0"
|
|
50
81
|
}
|
|
51
82
|
}
|