caique 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -10
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/plugin.d.ts +81 -0
- package/dist/plugin.js +163 -0
- package/dist/raw.d.ts +3 -17
- package/dist/raw.js +30 -13
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.js +18 -0
- package/dist/schema.json +300 -0
- package/dist/spec.d.ts +23 -2
- package/dist/spec.js +9 -0
- package/dist/terminal.d.ts +8 -2
- package/dist/terminal.js +7 -1
- package/package.json +12 -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,4 +1,22 @@
|
|
|
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>
|
|
9
|
+
|
|
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/dependencies-closeout-0a6b47?style=flat-square" alt="One dependency: closeout" />
|
|
18
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
|
|
19
|
+
</p>
|
|
2
20
|
|
|
3
21
|
**Pre-release.** The first working slice is here — `decide()`, below — and the rest follows
|
|
4
22
|
[`.sdlc/intents/caique/`](https://github.com/ofri-peretz/burgee/tree/main/.sdlc/intents/caique).
|
|
@@ -17,12 +35,13 @@ flags in, a verdict out.
|
|
|
17
35
|
|
|
18
36
|
```js
|
|
19
37
|
import { decide } from 'caique/decide';
|
|
38
|
+
import { processRuntime } from 'caique';
|
|
20
39
|
|
|
21
40
|
decide({
|
|
22
41
|
value: undefined, // nothing was passed
|
|
23
42
|
spec: { kind: 'text', message: 'Where should it go?' },
|
|
24
43
|
option: 'output-dir',
|
|
25
|
-
runtime: { env
|
|
44
|
+
runtime: processRuntime(), // or your own { env, isTTY: { stdin } }
|
|
26
45
|
required: true,
|
|
27
46
|
});
|
|
28
47
|
// no terminal -> { action: 'error', code: 'USAGE',
|
|
@@ -83,7 +102,7 @@ import { resolvePrompts } from 'caique/binding';
|
|
|
83
102
|
const { values, failure } = await resolvePrompts({
|
|
84
103
|
options, // { name: { required: true, prompt: { kind: 'text', message: 'Project name?' } } }
|
|
85
104
|
values, // what every other source resolved
|
|
86
|
-
runtime:
|
|
105
|
+
runtime: processRuntime(),
|
|
87
106
|
flags: { json, yes, interactive },
|
|
88
107
|
io: { reader, writer },
|
|
89
108
|
});
|
|
@@ -107,7 +126,7 @@ that touches a terminal:
|
|
|
107
126
|
import { createIo } from 'caique/terminal';
|
|
108
127
|
import { ask } from 'caique/ask';
|
|
109
128
|
|
|
110
|
-
const io = createIo(
|
|
129
|
+
const io = createIo(); // the terminal the program was started in
|
|
111
130
|
await ask({ kind: 'password', message: 'Token?' }, io);
|
|
112
131
|
io.close();
|
|
113
132
|
```
|
|
@@ -126,9 +145,11 @@ not a second implementation:
|
|
|
126
145
|
|
|
127
146
|
```js
|
|
128
147
|
import { askList, canRender } from 'caique/raw';
|
|
129
|
-
import { createIo } from 'caique/terminal';
|
|
148
|
+
import { createIo, streamsOf } from 'caique/terminal';
|
|
149
|
+
import { processRuntime } from 'caique';
|
|
130
150
|
|
|
131
|
-
const
|
|
151
|
+
const rt = processRuntime();
|
|
152
|
+
const io = { ...createIo(streamsOf(rt)), keys: rt.stdin };
|
|
132
153
|
const spec = { kind: 'select', message: 'Which host?', choices: [{ value: 'ora' }, { value: 'chalk' }] };
|
|
133
154
|
const answer = canRender(io.keys) ? await askList(spec, io) : await ask(spec, io);
|
|
134
155
|
```
|
|
@@ -172,8 +193,76 @@ counted whole across its own resolved tree.
|
|
|
172
193
|
- **`--interactive`** asks for every missing required option in one pass; **`--yes`** accepts
|
|
173
194
|
every confirmation; cancellation exits `CANCELLED` and restores the terminal.
|
|
174
195
|
- **Accessible mode** falls back to line input with no live redraw.
|
|
175
|
-
- **
|
|
196
|
+
- **A migration path from `@inquirer/prompts` and `@clack/prompts`**, graded by their own
|
|
197
|
+
suites. See below for what that is graded at today, which is zero.
|
|
198
|
+
|
|
199
|
+
## Which incumbents this is measured against
|
|
200
|
+
|
|
201
|
+
**`@inquirer/prompts`** (28.8 M/wk) and **`@clack/prompts`**. Those two, and not the
|
|
202
|
+
package whose download count is larger:
|
|
203
|
+
|
|
204
|
+
- **`inquirer` (34.3 M/wk) is out of scope, deliberately.** Its 34 million are the *legacy*
|
|
205
|
+
`inquirer.prompt([...])` façade, an API its own maintainer moved off; a new CLI written
|
|
206
|
+
today writes `@inquirer/prompts`. Reproducing the legacy object API would be work spent
|
|
207
|
+
on a shape nobody new adopts, and it is not on this package's roadmap.
|
|
208
|
+
- **`@inquirer/core`** is what the compatibility oracle grades, because it is where the
|
|
209
|
+
prompt *loop* — the keypress state machine both façades sit on — is actually tested.
|
|
210
|
+
`inquirer`'s own npm tarball ships **no tests at all**, so there is nothing there to grade.
|
|
211
|
+
|
|
212
|
+
| Incumbent's suite | Cases | Their own package | `caique` |
|
|
213
|
+
| :-- | --: | --: | --: |
|
|
214
|
+
| [`@inquirer/core` 12.0.3](https://github.com/SBoudrias/Inquirer.js) | 41 | 41 (100%) | **0 (0.0%)** |
|
|
215
|
+
| [`@clack/prompts` 1.8.1](https://github.com/bombshell-dev/clack) | 606 | 576 (95.0%) | **0 (0.0%)** |
|
|
216
|
+
|
|
217
|
+
Measured 2026-09-14 by `npm run compat`, which runs each incumbent's own unedited suite
|
|
218
|
+
twice: once against the incumbent (the control — the column that proves the gate works)
|
|
219
|
+
and once against caique. **Both caique columns are zero, and they are zero because no
|
|
220
|
+
façade exists yet** — `caique` exports `ask`, `decide` and `spec`, not `createPrompt` or
|
|
221
|
+
`text` in their spelling. The row is here because a claimed replacement with no number
|
|
222
|
+
beside it is a claim; this is the number, and it can only go up from here.
|
|
223
|
+
|
|
224
|
+
The 30 cases `@clack/prompts` fails against itself are `path.test.ts`, which mocks
|
|
225
|
+
`node:fs` in a way this repository's vitest does not reproduce — a harness divergence,
|
|
226
|
+
recorded with its reason in `packages/compat-oracle/src/hosts.ts` rather than rounded away.
|
|
227
|
+
|
|
228
|
+
## Following along
|
|
229
|
+
|
|
230
|
+
The intent and design are committed before the code is, so you can read what it will be —
|
|
231
|
+
and argue with it — before it exists:
|
|
232
|
+
|
|
233
|
+
- [`.sdlc/intents/caique/`](https://github.com/ofri-peretz/burgee/tree/main/.sdlc/intents/caique)
|
|
234
|
+
— the intent and the design.
|
|
235
|
+
- [Open an issue](https://github.com/ofri-peretz/burgee/issues) if a prompt in your CLI
|
|
236
|
+
cannot be expressed as a flag. That case is the interesting one.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on
|
|
241
|
+
[burgee](https://www.npmjs.com/package/burgee) declares what it is,
|
|
242
|
+
[roundel](https://www.npmjs.com/package/roundel) carries its colours,
|
|
243
|
+
[flagstaff](https://www.npmjs.com/package/flagstaff) flies it, and caique answers back.
|
|
244
|
+
caique installs one of them: [closeout](https://www.npmjs.com/package/closeout), because a
|
|
245
|
+
prompt hides the cursor and owes it back however the process dies, and there is exactly one
|
|
246
|
+
correct implementation of that. Nothing outside this repository is installed.
|
|
247
|
+
|
|
248
|
+
MIT © Ofri Peretz — see [LICENSE](./LICENSE).
|
|
249
|
+
|
|
250
|
+
## Benchmarks
|
|
251
|
+
|
|
252
|
+
Every number here is produced by `npm run bench` and published at [/docs/benchmarks](/docs/benchmarks).
|
|
253
|
+
|
|
254
|
+
Graded by the incumbent's own test suite:
|
|
255
|
+
|
|
256
|
+
| suite | passing |
|
|
257
|
+
| :-- | --: |
|
|
258
|
+
| `clack` | 0 / 606 |
|
|
259
|
+
| `inquirer-core` | 0 / 41 |
|
|
260
|
+
|
|
261
|
+
Weight, installed and tree-inclusive: **84,740 bytes** against **182,219** for the incumbents it replaces — a ratio of **0.4650** (@inquirer/core not installed here, so the ceiling is understated).
|
|
262
|
+
|
|
263
|
+
That ratio is not yet a claim: nothing here passes an incumbent suite, so it is the weight of a package that does not do the job.
|
|
264
|
+
## Where it sits
|
|
265
|
+
|
|
266
|
+
Plugins register under the `widgets` key, against the one schema the whole family shares.
|
|
176
267
|
|
|
177
|
-
|
|
178
|
-
what it is, roundel carries its colours, flagstaff flies it, caique answers back. Each is
|
|
179
|
-
an independent package; none requires the others.
|
|
268
|
+
Nothing in this family builds on it yet, and it builds on `closeout`.
|
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,9 @@ export * from './decide.js';
|
|
|
10
10
|
export * from './raw.js';
|
|
11
11
|
export * from './spec.js';
|
|
12
12
|
export * from './terminal.js';
|
|
13
|
+
/**
|
|
14
|
+
* Named, not starred: `runtime.ts` and `decide.ts` both call their slice `Runtime`, and two
|
|
15
|
+
* star exports of one name is an ambiguity TypeScript resolves by dropping it. The root
|
|
16
|
+
* keeps `decide`'s — the one a caller of `decide()` constructs — and takes the function.
|
|
17
|
+
*/
|
|
18
|
+
export { processRuntime } from './runtime.js';
|
package/dist/index.js
CHANGED
|
@@ -10,4 +10,10 @@ export * from './decide.js';
|
|
|
10
10
|
export * from './raw.js';
|
|
11
11
|
export * from './spec.js';
|
|
12
12
|
export * from './terminal.js';
|
|
13
|
+
/**
|
|
14
|
+
* Named, not starred: `runtime.ts` and `decide.ts` both call their slice `Runtime`, and two
|
|
15
|
+
* star exports of one name is an ambiguity TypeScript resolves by dropping it. The root
|
|
16
|
+
* keeps `decide`'s — the one a caller of `decide()` constructs — and takes the function.
|
|
17
|
+
*/
|
|
18
|
+
export { processRuntime } from './runtime.js';
|
|
13
19
|
//# sourceMappingURL=index.js.map
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type PromptSpec } from './spec.js';
|
|
2
|
+
/**
|
|
3
|
+
* The plugin contract version. One number for the family — the same `1` flagstaff and
|
|
4
|
+
* roundel declare, written out rather than imported for the reason in the file comment.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONTRACT = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Two named states of plain data, which a grader renders a widget with (R7).
|
|
9
|
+
*
|
|
10
|
+
* It carries no behaviour, so reading it does not mean running the author's code — that is
|
|
11
|
+
* why an optional `sample` does not turn a plugin into a program.
|
|
12
|
+
*/
|
|
13
|
+
export interface WidgetSample {
|
|
14
|
+
running: unknown;
|
|
15
|
+
done: unknown;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* How a plugin draws one kind of prompt.
|
|
19
|
+
*
|
|
20
|
+
* `static(spec)` is what a pipe, an agent and a screen reader get; it is what `projection()`
|
|
21
|
+
* already returns for the six built-ins, so a plugin widget slots into the same surface
|
|
22
|
+
* rather than beside it. `frame` is optional and drives `caique/raw`.
|
|
23
|
+
*/
|
|
24
|
+
export interface Widget {
|
|
25
|
+
static: (spec: PromptSpec) => string;
|
|
26
|
+
frame?: (t: number, spec: PromptSpec) => string;
|
|
27
|
+
sample?: WidgetSample;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The keys caique reads. Declared structurally: any object with these fields is a plugin
|
|
31
|
+
* here, whatever else it carries.
|
|
32
|
+
*/
|
|
33
|
+
export interface Plugin {
|
|
34
|
+
name: string;
|
|
35
|
+
contract?: number;
|
|
36
|
+
widgets?: Record<string, Widget>;
|
|
37
|
+
}
|
|
38
|
+
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT' | 'E_NO_STATIC_PROJECTION' | 'E_UNKNOWN_KIND';
|
|
39
|
+
/** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
|
|
40
|
+
export declare class PluginError extends Error {
|
|
41
|
+
readonly code: PluginErrorCode;
|
|
42
|
+
readonly fix: string;
|
|
43
|
+
constructor(code: PluginErrorCode, message: string, fix: string);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Refuse a plugin that cannot contribute a widget, at the door.
|
|
47
|
+
*
|
|
48
|
+
* Every refusal here is about the `widgets` key or the plugin's own identity. A key another
|
|
49
|
+
* layer owns is not inspected and not rejected (R1) — caique has no opinion about a spinner.
|
|
50
|
+
*/
|
|
51
|
+
export declare function validate(plugin: unknown): asserts plugin is Plugin;
|
|
52
|
+
/** Which plugin last contributed each kind — the shadowing a `plugin check` prints. */
|
|
53
|
+
export interface Contribution {
|
|
54
|
+
kind: string;
|
|
55
|
+
from: string;
|
|
56
|
+
/** Plugins that contributed this kind earlier and were overridden, in order. */
|
|
57
|
+
shadowed: string[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Register a plugin. Later wins, like ESLint flat config: the array is ordered, a caller
|
|
61
|
+
* reads it top to bottom, and the last word on a kind is the one nearest the program.
|
|
62
|
+
*/
|
|
63
|
+
export declare function register(plugin: unknown): void;
|
|
64
|
+
/** Forget every registered plugin. For tests, and for a program that re-registers at runtime. */
|
|
65
|
+
export declare function reset(): void;
|
|
66
|
+
/** The plugins registered, in registration order. */
|
|
67
|
+
export declare function registered(): readonly Plugin[];
|
|
68
|
+
/** Every kind a plugin contributed, with who won it and who it shadowed. */
|
|
69
|
+
export declare function widgets(): Contribution[];
|
|
70
|
+
/** The widget that draws `kind`, or nothing when no plugin registered one. */
|
|
71
|
+
export declare function widgetFor(kind: string): Widget | undefined;
|
|
72
|
+
/** Every kind that can be drawn right now: the six, plus whatever is registered. */
|
|
73
|
+
export declare function kinds(): string[];
|
|
74
|
+
/**
|
|
75
|
+
* The static projection for *any* kind — the one surface a caller needs.
|
|
76
|
+
*
|
|
77
|
+
* The six are still drawn by caique; anything else is a registered widget's `static`. A
|
|
78
|
+
* kind that is neither is a refusal naming what *is* registered, so the reader can see the
|
|
79
|
+
* typo rather than a text prompt where their rating widget should have been.
|
|
80
|
+
*/
|
|
81
|
+
export declare function projectionOf(spec: PromptSpec): string;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin host for caique's half of the contract (`plugin-contract` R1, R5, R6, R7, R8).
|
|
3
|
+
*
|
|
4
|
+
* A plugin is one plain object shared by the whole family. This file keeps the key caique
|
|
5
|
+
* understands — `widgets`, a prompt kind and how to draw it — and **ignores every other key
|
|
6
|
+
* without complaining**, which is what makes the same object work on any subset of the
|
|
7
|
+
* family that is installed. A plugin written for flagstaff registers here and contributes
|
|
8
|
+
* its widgets; its `tokens`, `glyphs` and `components` are not caique's business and are
|
|
9
|
+
* not an error.
|
|
10
|
+
*
|
|
11
|
+
* **Nothing here imports another layer**, and the plugin shape is declared rather than
|
|
12
|
+
* imported (R3). Types erase, so an import would cost nothing at run time — and it would
|
|
13
|
+
* still put flagstaff in caique's dependency story, which is the one thing the family
|
|
14
|
+
* promises it does not do.
|
|
15
|
+
*
|
|
16
|
+
* **A widget is the same shape a flagstaff component is** — `{ static, frame?, sample? }` —
|
|
17
|
+
* and a widget without `static` is refused with `E_NO_STATIC_PROJECTION`, the same code and
|
|
18
|
+
* the same fix shape. That sameness is the whole content of R5: the moment the two shapes
|
|
19
|
+
* differ by a key, "the same shape" stops being a fact a lock can hold and becomes prose.
|
|
20
|
+
*
|
|
21
|
+
* **Why this file, and not `ask.ts`, raises `E_UNKNOWN_KIND`.** `PromptKind` is an open
|
|
22
|
+
* union now, so `{ kind: 'acme-rating' }` type-checks whether or not anyone can draw it.
|
|
23
|
+
* The refusal therefore has to happen where the registry is, and it has to be *loud*: a
|
|
24
|
+
* kind nobody registered rendered as a text prompt is the silent-wrong-answer failure this
|
|
25
|
+
* package exists to prevent, wearing a hat.
|
|
26
|
+
*/
|
|
27
|
+
import { projection } from './ask.js';
|
|
28
|
+
import { BUILT_IN_KINDS } from './spec.js';
|
|
29
|
+
/**
|
|
30
|
+
* The plugin contract version. One number for the family — the same `1` flagstaff and
|
|
31
|
+
* roundel declare, written out rather than imported for the reason in the file comment.
|
|
32
|
+
*/
|
|
33
|
+
export const CONTRACT = 1;
|
|
34
|
+
/** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
|
|
35
|
+
export class PluginError extends Error {
|
|
36
|
+
code;
|
|
37
|
+
fix;
|
|
38
|
+
constructor(code, message, fix) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.fix = fix;
|
|
42
|
+
this.name = 'PluginError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
46
|
+
/** The six, in the order a person reads them — for the `fix` on an unknown kind. */
|
|
47
|
+
const builtIns = () => [...BUILT_IN_KINDS].join(', ');
|
|
48
|
+
/**
|
|
49
|
+
* Refuse a plugin that cannot contribute a widget, at the door.
|
|
50
|
+
*
|
|
51
|
+
* Every refusal here is about the `widgets` key or the plugin's own identity. A key another
|
|
52
|
+
* layer owns is not inspected and not rejected (R1) — caique has no opinion about a spinner.
|
|
53
|
+
*/
|
|
54
|
+
export function validate(plugin) {
|
|
55
|
+
if (!isRecord(plugin))
|
|
56
|
+
throw new PluginError('E_PLUGIN_SCHEMA', 'a plugin is a plain object', 'export an object, not a function or an array');
|
|
57
|
+
if (typeof plugin['name'] !== 'string' || plugin['name'] === '') {
|
|
58
|
+
throw new PluginError('E_PLUGIN_SCHEMA', 'a plugin needs a name', 'add `name: "…"` — it is how a shadowed widget is reported');
|
|
59
|
+
}
|
|
60
|
+
const contract = plugin['contract'];
|
|
61
|
+
if (contract !== undefined && (!Number.isInteger(contract) || contract > CONTRACT)) {
|
|
62
|
+
throw new PluginError('E_PLUGIN_CONTRACT', `plugin "${plugin['name']}" declares contract ${String(contract)}; this caique knows ${CONTRACT}`, 'upgrade caique, or lower the plugin’s contract');
|
|
63
|
+
}
|
|
64
|
+
validateWidgets(plugin['widgets'], plugin['name']);
|
|
65
|
+
}
|
|
66
|
+
function validateWidgets(widgetMap, name) {
|
|
67
|
+
if (widgetMap === undefined)
|
|
68
|
+
return;
|
|
69
|
+
if (!isRecord(widgetMap))
|
|
70
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widgets must be an object`, 'map a prompt kind to a widget: `{ static, frame?, sample? }`');
|
|
71
|
+
for (const [kind, widget] of Object.entries(widgetMap)) {
|
|
72
|
+
if (kind === '')
|
|
73
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": a widget’s kind is empty`, 'name the kind — it is what a `PromptSpec` sets as `kind`');
|
|
74
|
+
/**
|
|
75
|
+
* A plugin may not replace one of the six. The built-ins are the accessible floor and
|
|
76
|
+
* the drop-in surface, and `password` in particular guarantees that nothing writes back
|
|
77
|
+
* what it read — a third party that could override it could defeat that from a config
|
|
78
|
+
* file. Extension is the space *outside* the six, which is exactly what widening
|
|
79
|
+
* `PromptKind` opened up.
|
|
80
|
+
*/
|
|
81
|
+
if (BUILT_IN_KINDS.has(kind)) {
|
|
82
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": "${kind}" is a built-in prompt kind`, `caique draws the six itself (${builtIns()}); name a kind of your own`);
|
|
83
|
+
}
|
|
84
|
+
if (!isRecord(widget))
|
|
85
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widget "${kind}" is not an object`, 'a widget is `{ static, frame?, sample? }`');
|
|
86
|
+
/** U3 everywhere else, and the reason a third-party prompt cannot break the non-TTY guarantee. */
|
|
87
|
+
if (typeof widget['static'] !== 'function') {
|
|
88
|
+
throw new PluginError('E_NO_STATIC_PROJECTION', `plugin "${name}": widget "${kind}" has no static projection`, 'add `static: (spec) => "…"` — it is what a pipe, an agent and a screen reader get');
|
|
89
|
+
}
|
|
90
|
+
if (widget['frame'] !== undefined && typeof widget['frame'] !== 'function') {
|
|
91
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widget "${kind}" has a \`frame\` that is not a function`, 'a frame is `(t, spec) => "…"`, or leave it out and the widget is line-mode only');
|
|
92
|
+
}
|
|
93
|
+
validateSample(widget['sample'], name, kind);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function validateSample(sample, name, kind) {
|
|
97
|
+
if (sample === undefined)
|
|
98
|
+
return;
|
|
99
|
+
if (!isRecord(sample) || !('running' in sample) || !('done' in sample)) {
|
|
100
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widget "${kind}" has a malformed \`sample\``, 'a sample is `{ running, done }` — two named states of plain data, which a grader renders the widget with');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const order = [];
|
|
104
|
+
/**
|
|
105
|
+
* Register a plugin. Later wins, like ESLint flat config: the array is ordered, a caller
|
|
106
|
+
* reads it top to bottom, and the last word on a kind is the one nearest the program.
|
|
107
|
+
*/
|
|
108
|
+
export function register(plugin) {
|
|
109
|
+
validate(plugin);
|
|
110
|
+
order.push(plugin);
|
|
111
|
+
}
|
|
112
|
+
/** Forget every registered plugin. For tests, and for a program that re-registers at runtime. */
|
|
113
|
+
export function reset() {
|
|
114
|
+
order.length = 0;
|
|
115
|
+
}
|
|
116
|
+
/** The plugins registered, in registration order. */
|
|
117
|
+
export function registered() {
|
|
118
|
+
return order;
|
|
119
|
+
}
|
|
120
|
+
/** Every kind a plugin contributed, with who won it and who it shadowed. */
|
|
121
|
+
export function widgets() {
|
|
122
|
+
const by = new Map();
|
|
123
|
+
for (const plugin of order) {
|
|
124
|
+
for (const kind of Object.keys(plugin.widgets ?? {})) {
|
|
125
|
+
const existing = by.get(kind);
|
|
126
|
+
by.set(kind, { kind, from: plugin.name, shadowed: existing === undefined ? [] : [...existing.shadowed, existing.from] });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...by.values()];
|
|
130
|
+
}
|
|
131
|
+
/** The widget that draws `kind`, or nothing when no plugin registered one. */
|
|
132
|
+
export function widgetFor(kind) {
|
|
133
|
+
// Walked backwards because later wins, and the first hit from the end is the winner.
|
|
134
|
+
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
135
|
+
const widget = order[i].widgets?.[kind];
|
|
136
|
+
if (widget !== undefined)
|
|
137
|
+
return widget;
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
/** Every kind that can be drawn right now: the six, plus whatever is registered. */
|
|
142
|
+
export function kinds() {
|
|
143
|
+
return [...BUILT_IN_KINDS, ...widgets().map((c) => c.kind)];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The static projection for *any* kind — the one surface a caller needs.
|
|
147
|
+
*
|
|
148
|
+
* The six are still drawn by caique; anything else is a registered widget's `static`. A
|
|
149
|
+
* kind that is neither is a refusal naming what *is* registered, so the reader can see the
|
|
150
|
+
* typo rather than a text prompt where their rating widget should have been.
|
|
151
|
+
*/
|
|
152
|
+
export function projectionOf(spec) {
|
|
153
|
+
if (BUILT_IN_KINDS.has(spec.kind))
|
|
154
|
+
return projection(spec);
|
|
155
|
+
const widget = widgetFor(spec.kind);
|
|
156
|
+
if (widget === undefined) {
|
|
157
|
+
const known = widgets().map((c) => c.kind);
|
|
158
|
+
const named = known.length === 0 ? 'no plugin has registered a widget' : `registered kinds: ${known.join(', ')}`;
|
|
159
|
+
throw new PluginError('E_UNKNOWN_KIND', `no widget draws prompt kind ${JSON.stringify(spec.kind)} — ${named}`, `register a plugin whose \`widgets\` defines ${JSON.stringify(spec.kind)}, or use a built-in kind: ${builtIns()}`);
|
|
160
|
+
}
|
|
161
|
+
return widget.static(spec).trimEnd();
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=plugin.js.map
|
package/dist/raw.d.ts
CHANGED
|
@@ -1,20 +1,3 @@
|
|
|
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
1
|
import { type Asked, type Io } from './ask.js';
|
|
19
2
|
import { type Choice, type PromptSpec } from './spec.js';
|
|
20
3
|
/** A stream that can be put into raw mode and read a key at a time. */
|
|
@@ -50,6 +33,9 @@ export declare function renderList(spec: PromptSpec, choices: Choice[], state: L
|
|
|
50
33
|
* Returns the same `Asked` shape `ask()` does, so a caller can swap the two without
|
|
51
34
|
* knowing which ran — and cancels the same way, because `Ctrl-C` in raw mode is a byte and
|
|
52
35
|
* not a signal, and a person who presses it means to leave.
|
|
36
|
+
*
|
|
37
|
+
* The cursor is hidden through `closeout`: the byte path resolves and `restore()` runs in
|
|
38
|
+
* the `finally`; every path that never reaches the `finally` is the exit hook's.
|
|
53
39
|
*/
|
|
54
40
|
export declare function askList(spec: PromptSpec, io: RawIo, multi?: boolean): Promise<Asked>;
|
|
55
41
|
export {};
|
package/dist/raw.js
CHANGED
|
@@ -7,20 +7,23 @@
|
|
|
7
7
|
* between them by asking whether the terminal is one. Anything this can do that line mode
|
|
8
8
|
* cannot is decoration; anything line mode can do that this cannot would be a bug.
|
|
9
9
|
*
|
|
10
|
-
* **On
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `
|
|
10
|
+
* **On its one dependency.** The design said "spinner from flagstaff"; this imports
|
|
11
|
+
* `closeout` instead, and the difference is the point. A repaint is one escape sequence
|
|
12
|
+
* and belongs here. Hiding the cursor is a global side effect on someone else's terminal,
|
|
13
|
+
* and the obligation it creates — put it back however the process dies — is not a repaint.
|
|
14
|
+
* This file used to own `HIDE_CURSOR`/`SHOW_CURSOR`, the family's third copy, and restore
|
|
15
|
+
* on one path only: the keypress loop, which sees Ctrl-C because raw mode delivers it as a
|
|
16
|
+
* byte. A `SIGINT` from a parent, a `SIGTERM`, a crash or a `process.exit()` elsewhere
|
|
17
|
+
* never reached it, and left the cursor invisible until the user typed `reset` (measured
|
|
18
|
+
* against `dist/raw.js`, 2026-09-15: hide 1, show 0). `hideCursor()` registers the restore
|
|
19
|
+
* in the call that hides, so the two cannot drift.
|
|
17
20
|
*/
|
|
21
|
+
import { hideCursor } from 'closeout/cursor';
|
|
22
|
+
import exitHook from 'closeout/exit-hook';
|
|
18
23
|
import {} from './ask.js';
|
|
19
24
|
import {} from './spec.js';
|
|
20
25
|
const ESC = '\u001B';
|
|
21
26
|
const CSI = `${ESC}[`;
|
|
22
|
-
const HIDE_CURSOR = `${CSI}?25l`;
|
|
23
|
-
const SHOW_CURSOR = `${CSI}?25h`;
|
|
24
27
|
/** Column 1, up `n` lines, clear to the end of the screen — the only repaint this needs. */
|
|
25
28
|
const erase = (lines) => `${CSI}1G${lines > 1 ? `${CSI}${lines - 1}A` : ''}${CSI}0J`;
|
|
26
29
|
/**
|
|
@@ -94,12 +97,22 @@ function chosen(choices, state, multi) {
|
|
|
94
97
|
return choices[state.cursor]?.value ?? '';
|
|
95
98
|
return [...state.selected].sort((a, b) => a - b).map((index) => choices[index]?.value ?? '');
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The writer, seen as a terminal, for `hideCursor()` — which refuses a non-TTY. caique's
|
|
102
|
+
* `Writer` is one method and has no `isTTY`: the terminal test this renderer makes is
|
|
103
|
+
* `canRender(io.keys)`, on the other half of the same terminal, and it has already been
|
|
104
|
+
* made. So it is answered here rather than re-asked of a stream that cannot answer.
|
|
105
|
+
*/
|
|
106
|
+
const asTerminal = (writer) => ({ write: (text) => writer.write(text), isTTY: true });
|
|
97
107
|
/**
|
|
98
108
|
* Drive a list prompt with the arrow keys, repainting in place.
|
|
99
109
|
*
|
|
100
110
|
* Returns the same `Asked` shape `ask()` does, so a caller can swap the two without
|
|
101
111
|
* knowing which ran — and cancels the same way, because `Ctrl-C` in raw mode is a byte and
|
|
102
112
|
* not a signal, and a person who presses it means to leave.
|
|
113
|
+
*
|
|
114
|
+
* The cursor is hidden through `closeout`: the byte path resolves and `restore()` runs in
|
|
115
|
+
* the `finally`; every path that never reaches the `finally` is the exit hook's.
|
|
103
116
|
*/
|
|
104
117
|
export async function askList(spec, io, multi = false) {
|
|
105
118
|
const choices = spec.choices ?? [];
|
|
@@ -107,11 +120,14 @@ export async function askList(spec, io, multi = false) {
|
|
|
107
120
|
let painted = 0;
|
|
108
121
|
const paint = () => {
|
|
109
122
|
const frame = renderList(spec, choices, state, multi);
|
|
110
|
-
io.writer.write((painted === 0 ?
|
|
123
|
+
io.writer.write((painted === 0 ? '' : erase(painted)) + frame);
|
|
111
124
|
painted = frame.split('\n').length;
|
|
112
125
|
};
|
|
113
126
|
io.keys.setRawMode?.(true);
|
|
114
127
|
io.keys.resume?.();
|
|
128
|
+
// Hide and register the restore together. `restore()` shows the cursor and unregisters,
|
|
129
|
+
// so a prompt that ends normally leaves nothing behind for exit to do.
|
|
130
|
+
const restore = hideCursor(asTerminal(io.writer), exitHook);
|
|
115
131
|
paint();
|
|
116
132
|
try {
|
|
117
133
|
return await new Promise((resolve) => {
|
|
@@ -130,15 +146,16 @@ export async function askList(spec, io, multi = false) {
|
|
|
130
146
|
};
|
|
131
147
|
const done = (answer) => {
|
|
132
148
|
io.keys.off('data', onData);
|
|
133
|
-
// Leave the answered question on screen
|
|
134
|
-
//
|
|
135
|
-
io.writer.write(`${erase(painted)}${renderList(spec, choices, state, multi)}\n
|
|
149
|
+
// Leave the answered question on screen; the `finally` puts the cursor and the
|
|
150
|
+
// terminal back, because a prompt that exits in raw mode leaves the shell unusable.
|
|
151
|
+
io.writer.write(`${erase(painted)}${renderList(spec, choices, state, multi)}\n`);
|
|
136
152
|
resolve(answer);
|
|
137
153
|
};
|
|
138
154
|
io.keys.on('data', onData);
|
|
139
155
|
});
|
|
140
156
|
}
|
|
141
157
|
finally {
|
|
158
|
+
restore();
|
|
142
159
|
io.keys.setRawMode?.(false);
|
|
143
160
|
io.keys.pause?.();
|
|
144
161
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of the world caique needs, and the one file here that names `process` (Y9) —
|
|
3
|
+
* the same seam `paratext/src/runtime.ts` declares. `decide()` already took the fields it
|
|
4
|
+
* reads and `createIo()` already took its streams; this is the other half, so a program can
|
|
5
|
+
* get a real one without writing `process.stdin` itself.
|
|
6
|
+
*
|
|
7
|
+
* A **function**, for paratext's reason: a runtime built at import freezes the environment
|
|
8
|
+
* as it was when the module graph loaded, which is before a test can say what it wants.
|
|
9
|
+
* `runtime.test.ts` asks twice across a change, so a captured constant cannot pass.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Declared structurally, not imported: burgee's `Runtime` satisfies it, so does a literal in
|
|
13
|
+
* a test. `decide.ts` names the narrower pair it reads for the same reason; this satisfies it.
|
|
14
|
+
*/
|
|
15
|
+
export interface Runtime {
|
|
16
|
+
/** `decide()` reads `CI`: set means nobody is there to type (R6). */
|
|
17
|
+
env: Record<string, string | undefined>;
|
|
18
|
+
stdin: NodeJS.ReadableStream & {
|
|
19
|
+
isTTY?: boolean;
|
|
20
|
+
};
|
|
21
|
+
stdout: NodeJS.WritableStream & {
|
|
22
|
+
isTTY?: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** `stdin` decides whether a person can be asked at all; `stdout`, whether anything is drawn. */
|
|
25
|
+
isTTY: {
|
|
26
|
+
stdin: boolean;
|
|
27
|
+
stdout: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** What a real process looks like. Callers that have not got one pass their own. */
|
|
31
|
+
export declare const processRuntime: () => Runtime;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of the world caique needs, and the one file here that names `process` (Y9) —
|
|
3
|
+
* the same seam `paratext/src/runtime.ts` declares. `decide()` already took the fields it
|
|
4
|
+
* reads and `createIo()` already took its streams; this is the other half, so a program can
|
|
5
|
+
* get a real one without writing `process.stdin` itself.
|
|
6
|
+
*
|
|
7
|
+
* A **function**, for paratext's reason: a runtime built at import freezes the environment
|
|
8
|
+
* as it was when the module graph loaded, which is before a test can say what it wants.
|
|
9
|
+
* `runtime.test.ts` asks twice across a change, so a captured constant cannot pass.
|
|
10
|
+
*/
|
|
11
|
+
/** What a real process looks like. Callers that have not got one pass their own. */
|
|
12
|
+
export const processRuntime = () => ({
|
|
13
|
+
env: process.env,
|
|
14
|
+
stdin: process.stdin,
|
|
15
|
+
stdout: process.stdout,
|
|
16
|
+
isTTY: { stdin: process.stdin.isTTY === true, stdout: process.stdout.isTTY === true },
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=runtime.js.map
|
package/dist/schema.json
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json",
|
|
4
|
+
"title": "flagstaff plugin",
|
|
5
|
+
"description": "A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"name"
|
|
9
|
+
],
|
|
10
|
+
"additionalProperties": true,
|
|
11
|
+
"properties": {
|
|
12
|
+
"name": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"minLength": 1,
|
|
15
|
+
"description": "The plugin's name; also the prefix a host may use when two plugins contribute the same key."
|
|
16
|
+
},
|
|
17
|
+
"contract": {
|
|
18
|
+
"type": "integer",
|
|
19
|
+
"minimum": 1,
|
|
20
|
+
"description": "The plugin contract this object follows. A host refuses a newer contract than it knows."
|
|
21
|
+
},
|
|
22
|
+
"tokens": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"description": "A roundel theme: semantic token name to a hex colour, contrast-checked when flown.",
|
|
25
|
+
"additionalProperties": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"pattern": "^#[0-9a-fA-F]{6}$"
|
|
28
|
+
},
|
|
29
|
+
"propertyNames": {
|
|
30
|
+
"enum": [
|
|
31
|
+
"error",
|
|
32
|
+
"warn",
|
|
33
|
+
"ok",
|
|
34
|
+
"hint",
|
|
35
|
+
"muted",
|
|
36
|
+
"command",
|
|
37
|
+
"flag",
|
|
38
|
+
"value",
|
|
39
|
+
"heading",
|
|
40
|
+
"ground"
|
|
41
|
+
],
|
|
42
|
+
"description": "roundel's ten semantic tokens, and nothing else — `roundel`'s own validate() refuses any other name."
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"glyphs": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"description": "Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.",
|
|
48
|
+
"additionalProperties": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"minLength": 1
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"spinners": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"description": "Spinner styles by name, in cli-spinners' shape plus the static projection.",
|
|
56
|
+
"additionalProperties": {
|
|
57
|
+
"$ref": "#/$defs/spinner"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"borders": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"description": "Border styles a box can be drawn with, by name.",
|
|
63
|
+
"additionalProperties": {
|
|
64
|
+
"$ref": "#/$defs/border"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"components": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"description": "Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.",
|
|
70
|
+
"additionalProperties": {
|
|
71
|
+
"$ref": "#/$defs/component"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"capabilities": {
|
|
75
|
+
"$ref": "#/$defs/capabilities"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"$defs": {
|
|
79
|
+
"spinner": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"required": [
|
|
82
|
+
"frames",
|
|
83
|
+
"interval",
|
|
84
|
+
"static"
|
|
85
|
+
],
|
|
86
|
+
"properties": {
|
|
87
|
+
"frames": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": {
|
|
90
|
+
"type": "string"
|
|
91
|
+
},
|
|
92
|
+
"minItems": 1
|
|
93
|
+
},
|
|
94
|
+
"interval": {
|
|
95
|
+
"type": "integer",
|
|
96
|
+
"minimum": 1,
|
|
97
|
+
"description": "Milliseconds between frames on a terminal."
|
|
98
|
+
},
|
|
99
|
+
"static": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "What a pipe prints instead of the animation."
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"component": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"required": [
|
|
108
|
+
"static"
|
|
109
|
+
],
|
|
110
|
+
"properties": {
|
|
111
|
+
"static": {
|
|
112
|
+
"description": "(state) => string. Required: the projection every non-terminal mode prints."
|
|
113
|
+
},
|
|
114
|
+
"frame": {
|
|
115
|
+
"description": "(t, state) => string. Optional: the frame at t milliseconds since hoisting."
|
|
116
|
+
},
|
|
117
|
+
"sample": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"required": [
|
|
120
|
+
"running",
|
|
121
|
+
"done"
|
|
122
|
+
],
|
|
123
|
+
"description": "Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.",
|
|
124
|
+
"properties": {
|
|
125
|
+
"running": {
|
|
126
|
+
"description": "The state to open with."
|
|
127
|
+
},
|
|
128
|
+
"done": {
|
|
129
|
+
"description": "The state to close with."
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"interval": {
|
|
134
|
+
"type": "integer",
|
|
135
|
+
"minimum": 1,
|
|
136
|
+
"description": "Milliseconds between repaints when `frame` is given; 80 when omitted."
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"border": {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"required": [
|
|
143
|
+
"topLeft",
|
|
144
|
+
"top",
|
|
145
|
+
"topRight",
|
|
146
|
+
"left",
|
|
147
|
+
"right",
|
|
148
|
+
"bottomLeft",
|
|
149
|
+
"bottom",
|
|
150
|
+
"bottomRight"
|
|
151
|
+
],
|
|
152
|
+
"description": "cli-boxes' shape exactly, so that corpus imports unchanged.",
|
|
153
|
+
"properties": {
|
|
154
|
+
"topLeft": {
|
|
155
|
+
"type": "string"
|
|
156
|
+
},
|
|
157
|
+
"top": {
|
|
158
|
+
"type": "string"
|
|
159
|
+
},
|
|
160
|
+
"topRight": {
|
|
161
|
+
"type": "string"
|
|
162
|
+
},
|
|
163
|
+
"left": {
|
|
164
|
+
"type": "string"
|
|
165
|
+
},
|
|
166
|
+
"right": {
|
|
167
|
+
"type": "string"
|
|
168
|
+
},
|
|
169
|
+
"bottomLeft": {
|
|
170
|
+
"type": "string"
|
|
171
|
+
},
|
|
172
|
+
"bottom": {
|
|
173
|
+
"type": "string"
|
|
174
|
+
},
|
|
175
|
+
"bottomRight": {
|
|
176
|
+
"type": "string"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"capability": {
|
|
181
|
+
"type": "object",
|
|
182
|
+
"required": [
|
|
183
|
+
"name",
|
|
184
|
+
"osc",
|
|
185
|
+
"when",
|
|
186
|
+
"encode",
|
|
187
|
+
"fallback"
|
|
188
|
+
],
|
|
189
|
+
"additionalProperties": false,
|
|
190
|
+
"description": "One paratext capability: what it says to the terminal, when the terminal is believed to understand it, and what prints when it does not. No functions, so it travels through JSON. `encode` and `fallback` are templates: `{field}` is the field's value, `{field|base64}` is it base64-encoded, and `[ … ]` is emitted only when every field inside it has a value.",
|
|
191
|
+
"properties": {
|
|
192
|
+
"name": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"minLength": 1,
|
|
195
|
+
"description": "How callers name it. Registering an existing name replaces it — how a caller corrects a guess we got wrong."
|
|
196
|
+
},
|
|
197
|
+
"osc": {
|
|
198
|
+
"description": "The OSC code this speaks, or 'BEL' for the bell and for protocols that are not OSC at all, such as Kitty's.",
|
|
199
|
+
"oneOf": [
|
|
200
|
+
{
|
|
201
|
+
"type": "integer",
|
|
202
|
+
"minimum": 0
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"const": "BEL"
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
"when": {
|
|
210
|
+
"type": "object",
|
|
211
|
+
"description": "When the terminal is believed to understand it. Every clause must hold; `termProgram` and `envAny` are ORs within themselves. Guesses — no terminal answers 'do you do OSC 1337' — and therefore data a caller can replace.",
|
|
212
|
+
"additionalProperties": false,
|
|
213
|
+
"properties": {
|
|
214
|
+
"tty": {
|
|
215
|
+
"type": "boolean",
|
|
216
|
+
"description": "Refuse a pipe. Almost always true: a file that receives OSC gets control bytes in it."
|
|
217
|
+
},
|
|
218
|
+
"termProgram": {
|
|
219
|
+
"type": "array",
|
|
220
|
+
"items": {
|
|
221
|
+
"type": "string"
|
|
222
|
+
},
|
|
223
|
+
"description": "Any one of these TERM_PROGRAM values."
|
|
224
|
+
},
|
|
225
|
+
"envAny": {
|
|
226
|
+
"type": "array",
|
|
227
|
+
"items": {
|
|
228
|
+
"type": "string"
|
|
229
|
+
},
|
|
230
|
+
"description": "Any one of these environment variables merely being set, as VTE announces itself."
|
|
231
|
+
},
|
|
232
|
+
"term": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"description": "An exact TERM — Kitty is xterm-kitty."
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"encode": {
|
|
239
|
+
"type": "string",
|
|
240
|
+
"minLength": 1,
|
|
241
|
+
"description": "The bytes, as a template, for a terminal that does understand."
|
|
242
|
+
},
|
|
243
|
+
"fallback": {
|
|
244
|
+
"type": "string",
|
|
245
|
+
"description": "What to print when it does not — PRINCIPLES rule 6. '' is a legitimate answer, a window title having nothing to say in a log; absence is not, which is why this is required and may be ''."
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
"examples": [
|
|
249
|
+
{
|
|
250
|
+
"name": "kitty-image",
|
|
251
|
+
"osc": "BEL",
|
|
252
|
+
"when": {
|
|
253
|
+
"tty": true,
|
|
254
|
+
"term": "xterm-kitty"
|
|
255
|
+
},
|
|
256
|
+
"encode": "\u001b_Ga=T,f=100;{base64}\u001b\\",
|
|
257
|
+
"fallback": "{caption}"
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
"capabilities": {
|
|
262
|
+
"type": "object",
|
|
263
|
+
"description": "paratext capabilities by name — the OSC section of a plugin, read the way flagstaff reads `spinners`.",
|
|
264
|
+
"additionalProperties": {
|
|
265
|
+
"$ref": "#/$defs/capability"
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
"capabilityDocument": {
|
|
269
|
+
"description": "What paratext's `check()` accepts. Two shapes, and only one of them survives 1.0.",
|
|
270
|
+
"oneOf": [
|
|
271
|
+
{
|
|
272
|
+
"description": "The family shape: a plugin carrying its capabilities under `capabilities`.",
|
|
273
|
+
"type": "object",
|
|
274
|
+
"required": [
|
|
275
|
+
"name",
|
|
276
|
+
"capabilities"
|
|
277
|
+
],
|
|
278
|
+
"properties": {
|
|
279
|
+
"name": {
|
|
280
|
+
"type": "string",
|
|
281
|
+
"minLength": 1
|
|
282
|
+
},
|
|
283
|
+
"contract": {
|
|
284
|
+
"type": "integer",
|
|
285
|
+
"minimum": 1
|
|
286
|
+
},
|
|
287
|
+
"capabilities": {
|
|
288
|
+
"$ref": "#/$defs/capabilities"
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"deprecated": true,
|
|
294
|
+
"description": "Deprecated: one capability as the whole document, the shape paratext's schema had before this one absorbed it. Accepted for one minor release, removed at 1.0 — `check()` validates it and says so.",
|
|
295
|
+
"$ref": "#/$defs/capability"
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
package/dist/spec.d.ts
CHANGED
|
@@ -9,8 +9,29 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Nothing here imports a runtime, a stream or a terminal. This file is data.
|
|
11
11
|
*/
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The six kinds a prompt can be — plus whatever a plugin adds.
|
|
14
|
+
*
|
|
15
|
+
* **Why `(string & {})` rather than a closed union** (`plugin-contract` R5, D5). A closed
|
|
16
|
+
* union makes a plugin's seventh kind a type error, so hosting `widgets` at all would be a
|
|
17
|
+
* breaking change written as an additive one: every caller of `caique/plugin` would need
|
|
18
|
+
* this file edited before it could name its own kind. The intersection keeps all six
|
|
19
|
+
* literals in an editor's completion list — which a bare `string` would throw away — while
|
|
20
|
+
* admitting the kinds `caique/plugin` renders.
|
|
21
|
+
*
|
|
22
|
+
* Anything more than a *kind* is still a wizard, which is out of scope. Widening the type
|
|
23
|
+
* does not widen the model: a prompt is one question with one answer, whoever draws it.
|
|
24
|
+
*/
|
|
25
|
+
export type PromptKind = 'text' | 'confirm' | 'select' | 'multiselect' | 'password' | 'path' | (string & {});
|
|
26
|
+
/**
|
|
27
|
+
* The six caique draws itself, as data rather than as a `switch` nobody can read back.
|
|
28
|
+
*
|
|
29
|
+
* `caique/plugin` needs this set to answer two questions a plugin makes askable for the
|
|
30
|
+
* first time: whether a kind is already spoken for, and which kinds a refusal should name.
|
|
31
|
+
* Deriving it from the widget dispatch in `ask.ts` would mean two lists that agree only by
|
|
32
|
+
* inspection, which is the drift the family's locks exist to prevent.
|
|
33
|
+
*/
|
|
34
|
+
export declare const BUILT_IN_KINDS: ReadonlySet<string>;
|
|
14
35
|
/** One choice in a `select` or `multiselect`. `value` is what the option receives. */
|
|
15
36
|
export interface Choice {
|
|
16
37
|
value: string;
|
package/dist/spec.js
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Nothing here imports a runtime, a stream or a terminal. This file is data.
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* The six caique draws itself, as data rather than as a `switch` nobody can read back.
|
|
14
|
+
*
|
|
15
|
+
* `caique/plugin` needs this set to answer two questions a plugin makes askable for the
|
|
16
|
+
* first time: whether a kind is already spoken for, and which kinds a refusal should name.
|
|
17
|
+
* Deriving it from the widget dispatch in `ask.ts` would mean two lists that agree only by
|
|
18
|
+
* inspection, which is the drift the family's locks exist to prevent.
|
|
19
|
+
*/
|
|
20
|
+
export const BUILT_IN_KINDS = new Set(['text', 'confirm', 'select', 'multiselect', 'password', 'path']);
|
|
12
21
|
/** `--output-dir`, which is what a refusal has to say to be actionable. */
|
|
13
22
|
export function flagOf(option) {
|
|
14
23
|
return `--${option}`;
|
package/dist/terminal.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Io } from './ask.js';
|
|
2
|
-
|
|
2
|
+
import { type Runtime } from './runtime.js';
|
|
3
|
+
/** The stream pair a terminal Io is built over: a `Runtime`'s `stdin` and its `stdout`. */
|
|
3
4
|
export interface Streams {
|
|
4
5
|
input: NodeJS.ReadableStream & {
|
|
5
6
|
isTTY?: boolean;
|
|
@@ -8,13 +9,18 @@ export interface Streams {
|
|
|
8
9
|
isTTY?: boolean;
|
|
9
10
|
};
|
|
10
11
|
}
|
|
12
|
+
/** A runtime's two streams as the pair `createIo` takes — the mapping, written down once. */
|
|
13
|
+
export declare const streamsOf: (runtime: Runtime) => Streams;
|
|
11
14
|
/**
|
|
12
15
|
* A reader and writer over a real stream pair.
|
|
13
16
|
*
|
|
14
17
|
* The reader resolves `undefined` when the stream ends, which `ask()` reads as a
|
|
15
18
|
* cancellation — `Ctrl-D` and a closed pipe both arrive that way, and both mean nobody is
|
|
16
19
|
* going to type.
|
|
20
|
+
*
|
|
21
|
+
* With no argument it builds over the real process, read when it is called and not at
|
|
22
|
+
* import: a program that wants the terminal it was started in writes `createIo()`.
|
|
17
23
|
*/
|
|
18
|
-
export declare function createIo(streams
|
|
24
|
+
export declare function createIo(streams?: Streams): Io & {
|
|
19
25
|
close: () => void;
|
|
20
26
|
};
|
package/dist/terminal.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createInterface } from 'node:readline';
|
|
15
15
|
import {} from './ask.js';
|
|
16
|
+
import { processRuntime } from './runtime.js';
|
|
17
|
+
/** A runtime's two streams as the pair `createIo` takes — the mapping, written down once. */
|
|
18
|
+
export const streamsOf = (runtime) => ({ input: runtime.stdin, output: runtime.stdout });
|
|
16
19
|
/** Accepts a chunk and writes nothing: what a muted stream's `write` does. */
|
|
17
20
|
const swallow = () => true;
|
|
18
21
|
/**
|
|
@@ -36,8 +39,11 @@ function mute(rl, streams) {
|
|
|
36
39
|
* The reader resolves `undefined` when the stream ends, which `ask()` reads as a
|
|
37
40
|
* cancellation — `Ctrl-D` and a closed pipe both arrive that way, and both mean nobody is
|
|
38
41
|
* going to type.
|
|
42
|
+
*
|
|
43
|
+
* With no argument it builds over the real process, read when it is called and not at
|
|
44
|
+
* import: a program that wants the terminal it was started in writes `createIo()`.
|
|
39
45
|
*/
|
|
40
|
-
export function createIo(streams) {
|
|
46
|
+
export function createIo(streams = streamsOf(processRuntime())) {
|
|
41
47
|
const rl = createInterface({ input: streams.input, output: streams.output, terminal: streams.output.isTTY === true });
|
|
42
48
|
let ended = false;
|
|
43
49
|
rl.once('close', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caique",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -28,11 +28,17 @@
|
|
|
28
28
|
"import": "./dist/decide.js",
|
|
29
29
|
"default": "./dist/decide.js"
|
|
30
30
|
},
|
|
31
|
+
"./plugin": {
|
|
32
|
+
"types": "./dist/plugin.d.ts",
|
|
33
|
+
"import": "./dist/plugin.js",
|
|
34
|
+
"default": "./dist/plugin.js"
|
|
35
|
+
},
|
|
31
36
|
"./raw": {
|
|
32
37
|
"types": "./dist/raw.d.ts",
|
|
33
38
|
"import": "./dist/raw.js",
|
|
34
39
|
"default": "./dist/raw.js"
|
|
35
40
|
},
|
|
41
|
+
"./schema.json": "./dist/schema.json",
|
|
36
42
|
"./spec": {
|
|
37
43
|
"types": "./dist/spec.d.ts",
|
|
38
44
|
"import": "./dist/spec.js",
|
|
@@ -50,9 +56,10 @@
|
|
|
50
56
|
"!dist/**/*.test.*"
|
|
51
57
|
],
|
|
52
58
|
"scripts": {
|
|
53
|
-
"build": "tsc -p tsconfig.build.json",
|
|
59
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/schema-to-dist.mjs",
|
|
54
60
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
55
61
|
"test": "vitest run --passWithNoTests",
|
|
62
|
+
"coverage": "vitest run --coverage.enabled",
|
|
56
63
|
"lint": "eslint src"
|
|
57
64
|
},
|
|
58
65
|
"repository": {
|
|
@@ -75,6 +82,9 @@
|
|
|
75
82
|
"agent",
|
|
76
83
|
"non-tty"
|
|
77
84
|
],
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"closeout": "^0.2.0"
|
|
87
|
+
},
|
|
78
88
|
"devDependencies": {
|
|
79
89
|
"vitest": "^5.0.0"
|
|
80
90
|
}
|