@orkestrel/terminal 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -11
- package/dist/src/core/index.cjs +118 -83
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +265 -150
- package/dist/src/core/index.d.ts +265 -150
- package/dist/src/core/index.js +119 -84
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +111 -86
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +122 -92
- package/dist/src/server/index.d.ts +122 -92
- package/dist/src/server/index.js +111 -86
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -19
package/dist/src/server/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { CSI, createStyler } from "@orkestrel/console";
|
|
2
2
|
import { NEWLINE, RETURN, TerminalError, createCheckboxState, createConfirmState, createEditorState, createInputState, createPasswordState, createPromptTheme, createSelectState, parseKey, reduceCheckbox, reduceConfirm, reduceEditor, reduceInput, reducePassword, reduceSelect, renderErrorLine, renderHintedHeader, renderPromptHeader, sanitizeDisplayText } from "../core/index.js";
|
|
3
|
+
import { isBoolean, isFunction, isNumber, isObject, isString } from "@orkestrel/contract";
|
|
3
4
|
import { matchesAnswer, parseValue } from "@orkestrel/form";
|
|
4
5
|
import { isStreamTarget } from "@orkestrel/console/server";
|
|
5
6
|
import { createInterface } from "node:readline";
|
|
6
7
|
import { stdin, stdout } from "node:process";
|
|
7
8
|
//#region src/server/constants.ts
|
|
8
9
|
/**
|
|
9
|
-
* Holds the cursor-
|
|
10
|
-
* interpolates the `{count}` placeholder with the
|
|
11
|
-
* the count stays out of the constant.
|
|
10
|
+
* Holds the cursor-up sequence template (`ESC[{count}A`) —
|
|
11
|
+
* {@link import('./helpers.js').renderCursorUp} interpolates the `{count}` placeholder with the
|
|
12
|
+
* number of lines to climb. Kept as a template so the count stays out of the constant.
|
|
12
13
|
*/
|
|
13
14
|
var CSI_UP = `${CSI}{count}A`;
|
|
14
15
|
/**
|
|
@@ -19,9 +20,9 @@ var CURSOR_HIDE = `${CSI}?25l`;
|
|
|
19
20
|
/** Shows the cursor (`ESC[?25h`) — restores the cursor after a prompt resolves / cancels (the {@link CURSOR_HIDE} pair). */
|
|
20
21
|
var CURSOR_SHOW = `${CSI}?25h`;
|
|
21
22
|
/**
|
|
22
|
-
* Erases from the cursor down to the end of the screen (`ESC[J`) — wipes the
|
|
23
|
-
*
|
|
24
|
-
* never leaves orphaned rows
|
|
23
|
+
* Erases from the cursor down to the end of the screen (`ESC[J`) — wipes the whole previous view,
|
|
24
|
+
* which a `select` or `checkbox` can spread over several lines, in one write before the new view is
|
|
25
|
+
* rendered, so a redraw never leaves orphaned rows behind.
|
|
25
26
|
*/
|
|
26
27
|
var CLEAR_DOWN = `${CSI}J`;
|
|
27
28
|
/**
|
|
@@ -53,9 +54,15 @@ var CONTROL_HINTS = Object.freeze({
|
|
|
53
54
|
});
|
|
54
55
|
/** Holds the instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. */
|
|
55
56
|
var FILE_HINT = "One path per line, blank to finish";
|
|
56
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Holds the lead on the line listing an open `select`'s offered values, which a typed answer can
|
|
59
|
+
* ignore.
|
|
60
|
+
*/
|
|
57
61
|
var SUGGESTION_LEAD = "Suggestions";
|
|
58
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Holds the lead on the line listing the choices a `select` or `checkbox` shows but refuses, so a
|
|
64
|
+
* reader sees why one is missing from the list it heads.
|
|
65
|
+
*/
|
|
59
66
|
var UNAVAILABLE_LEAD = "Unavailable";
|
|
60
67
|
/** Holds the mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. */
|
|
61
68
|
var LOCKED_MARK = "(locked)";
|
|
@@ -69,55 +76,55 @@ var REFUSAL_MESSAGE = "Enter a value this field accepts";
|
|
|
69
76
|
//#endregion
|
|
70
77
|
//#region src/server/helpers.ts
|
|
71
78
|
/**
|
|
72
|
-
* Checks whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` /
|
|
73
|
-
* `'data'` subscription methods. A total type guard: it
|
|
74
|
-
*
|
|
75
|
-
* or a fake TTY a test injects) to the exact slice the driver reads
|
|
79
|
+
* Checks whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` /
|
|
80
|
+
* `off` `'data'` subscription methods. A total type guard: it never throws and returns `false` for
|
|
81
|
+
* anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`,
|
|
82
|
+
* or a fake TTY a test injects) to the exact slice the driver reads, never an assertion.
|
|
76
83
|
*
|
|
77
84
|
* @remarks
|
|
78
85
|
* Only `on` / `off` are required (the irreducible event seam); `setRawMode` / `resume` / `pause` /
|
|
79
86
|
* `isTTY` are optional on {@link InputStreamInterface}, so their absence does not disqualify a stream
|
|
80
|
-
* — a piped, non-TTY stream is still a valid input,
|
|
87
|
+
* — a piped, non-TTY stream is still a valid input, one the driver reads through the readline
|
|
81
88
|
* fallback rather than raw mode.
|
|
82
89
|
*
|
|
83
90
|
* @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)
|
|
84
91
|
* @returns True if `value` has callable `on` and `off`; false otherwise
|
|
85
92
|
*/
|
|
86
93
|
function isInputStream(value) {
|
|
87
|
-
return
|
|
94
|
+
return isObject(value) && "on" in value && isFunction(value.on) && "off" in value && isFunction(value.off);
|
|
88
95
|
}
|
|
89
96
|
/**
|
|
90
97
|
* Checks whether `value` is a Node {@link NodeJS.ReadableStream} — a total structural guard
|
|
91
|
-
* checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface`
|
|
92
|
-
* as its `input`. The non-TTY fallback narrows the resolved input stream through this
|
|
93
|
-
* it to readline
|
|
94
|
-
* crosses into the readline boundary honestly. Never throws; returns
|
|
95
|
-
* isn't a full readable.
|
|
98
|
+
* checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface`
|
|
99
|
+
* requires as its `input`. The non-TTY fallback narrows the resolved input stream through this
|
|
100
|
+
* before handing it to readline, never through an assertion, so a real piped `process.stdin` (or a
|
|
101
|
+
* `PassThrough` a test injects) crosses into the readline boundary honestly. Never throws; returns
|
|
102
|
+
* `false` for a minimal fake that isn't a full readable.
|
|
96
103
|
*
|
|
97
104
|
* @param value - The resolved input stream (or any value crossing the boundary)
|
|
98
105
|
* @returns True if `value` has the readable methods readline needs; false otherwise
|
|
99
106
|
*/
|
|
100
107
|
function isReadable(value) {
|
|
101
|
-
return
|
|
108
|
+
return isObject(value) && "read" in value && isFunction(value.read) && "pipe" in value && isFunction(value.pipe);
|
|
102
109
|
}
|
|
103
110
|
/**
|
|
104
|
-
* Checks whether an input stream can be driven in
|
|
105
|
-
* exposes a callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to
|
|
106
|
-
*
|
|
107
|
-
* `node:readline` line-input fallback
|
|
108
|
-
* never throws.
|
|
111
|
+
* Checks whether an input stream can be driven in raw mode — it reports `isTTY === true` and
|
|
112
|
+
* exposes a callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to
|
|
113
|
+
* choose its path: true selects the interactive raw-mode fields, with arrow-key navigation and a
|
|
114
|
+
* live re-render; false selects the `node:readline` line-input fallback, because a piped or
|
|
115
|
+
* non-terminal stream cannot enter raw mode. Total — never throws.
|
|
109
116
|
*
|
|
110
117
|
* @param input - The resolved {@link InputStreamInterface}
|
|
111
118
|
* @returns True if the stream is a TTY with `setRawMode`; false otherwise
|
|
112
119
|
*/
|
|
113
120
|
function supportsRawMode(input) {
|
|
114
|
-
return input.isTTY === true &&
|
|
121
|
+
return input.isTTY === true && isFunction(input.setRawMode);
|
|
115
122
|
}
|
|
116
123
|
/**
|
|
117
|
-
* Counts the terminal
|
|
118
|
-
*
|
|
119
|
-
* re-render: the driver records the line count of the view it
|
|
120
|
-
* far up to move the cursor before overwriting. Total; an empty string is one
|
|
124
|
+
* Counts the terminal lines a rendered prompt `view` occupies — one more than its newline count, so
|
|
125
|
+
* a view with no newline is a single line and a view with N newlines spans N+1 lines. The basis of
|
|
126
|
+
* the in-place re-render: the driver records the line count of the view it wrote so the next redraw
|
|
127
|
+
* knows how far up to move the cursor before overwriting. Total; an empty string is one empty line.
|
|
121
128
|
*
|
|
122
129
|
* @param view - The rendered (possibly multi-line, possibly ANSI-styled) view string
|
|
123
130
|
* @returns The number of lines the view spans (always at least 1)
|
|
@@ -128,10 +135,10 @@ function lineCount(view) {
|
|
|
128
135
|
return lines;
|
|
129
136
|
}
|
|
130
137
|
/**
|
|
131
|
-
* Returns the cursor-
|
|
132
|
-
* empty string when `count` is zero or negative
|
|
133
|
-
* The pure step the in-place re-render uses to climb back over the previous view
|
|
134
|
-
* Total.
|
|
138
|
+
* Returns the cursor-up control sequence that moves the cursor up `count` lines (`ESC[{count}A`),
|
|
139
|
+
* or the empty string when `count` is zero or negative, because no movement is needed and `ESC[0A`
|
|
140
|
+
* is a wasted write. The pure step the in-place re-render uses to climb back over the previous view
|
|
141
|
+
* before clearing it. Total.
|
|
135
142
|
*
|
|
136
143
|
* @param count - How many lines to move the cursor up
|
|
137
144
|
* @returns The `ESC[{count}A` sequence, or `''` when `count <= 0`
|
|
@@ -141,31 +148,31 @@ function renderCursorUp(count) {
|
|
|
141
148
|
return `${CSI_UP.replace("{count}", String(count))}`;
|
|
142
149
|
}
|
|
143
150
|
/**
|
|
144
|
-
* Returns the full reposition-and-clear prefix to write
|
|
145
|
-
* the line count of the
|
|
146
|
-
* 0, and erases everything from there to the end of the screen, so the next view is drawn
|
|
147
|
-
*
|
|
148
|
-
* followed by the new view.
|
|
151
|
+
* Returns the full reposition-and-clear prefix to write before re-rendering a prompt view in place
|
|
152
|
+
* — given the line count of the previous view, it moves the cursor up over those lines, returns it
|
|
153
|
+
* to column 0, and erases everything from there to the end of the screen, so the next view is drawn
|
|
154
|
+
* on a clean region and a taller previous view leaves no orphaned rows. Pure; the driver writes
|
|
155
|
+
* this immediately followed by the new view.
|
|
149
156
|
*
|
|
150
157
|
* @remarks
|
|
151
|
-
* For the
|
|
152
|
-
* the prefix is
|
|
153
|
-
* subsequent render it climbs `previousLines - 1` lines (the cursor is on the
|
|
158
|
+
* For the first render `previousLines` is `1` (the cursor sits on the line the prompt opened on) so
|
|
159
|
+
* the prefix is a carriage return + clear-down — the prompt draws from the current line. For a
|
|
160
|
+
* subsequent render it climbs `previousLines - 1` lines (the cursor is on the last line of the prior
|
|
154
161
|
* view) before clearing. Keeping the math here (not in the driver) makes the re-render unit-testable
|
|
155
162
|
* without a real terminal.
|
|
156
163
|
*
|
|
157
|
-
* @param previousLines - The line count of the view
|
|
164
|
+
* @param previousLines - The line count of the view on screen (from {@link lineCount})
|
|
158
165
|
* @returns The control-sequence prefix to write before the new view
|
|
159
166
|
*/
|
|
160
167
|
function redrawPrefix(previousLines) {
|
|
161
168
|
return `${renderCursorUp(previousLines - 1)}${RETURN}${CLEAR_DOWN}`;
|
|
162
169
|
}
|
|
163
170
|
/**
|
|
164
|
-
* Projects any field the walk reads as a
|
|
165
|
-
* takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`,
|
|
166
|
-
* `
|
|
167
|
-
*
|
|
168
|
-
* projection carries no rule, because the
|
|
171
|
+
* Projects any field the walk reads as a line of text into the {@link TextField} the text reducer
|
|
172
|
+
* takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`, `time`,
|
|
173
|
+
* `datetime`, `color`, and one `file` entry. The label carries that control's format cue from
|
|
174
|
+
* {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The
|
|
175
|
+
* projection carries no rule, because the authoritative form still evaluates the answer this line
|
|
169
176
|
* binds; it exists only so one reducer covers every one of them.
|
|
170
177
|
*
|
|
171
178
|
* @param field - The field being read
|
|
@@ -181,12 +188,12 @@ function fieldToText(field) {
|
|
|
181
188
|
const label = field.label ?? field.name;
|
|
182
189
|
const hint = CONTROL_HINTS[field.control];
|
|
183
190
|
const seed = "default" in field ? field.default : void 0;
|
|
184
|
-
const text =
|
|
191
|
+
const text = isNumber(seed) ? String(seed) : seed;
|
|
185
192
|
return {
|
|
186
193
|
control: "text",
|
|
187
194
|
name: field.name,
|
|
188
195
|
label: hint === void 0 ? label : `${label} ${hint}`,
|
|
189
|
-
...
|
|
196
|
+
...isString(text) ? { default: text } : {}
|
|
190
197
|
};
|
|
191
198
|
}
|
|
192
199
|
/**
|
|
@@ -199,13 +206,13 @@ function fieldToText(field) {
|
|
|
199
206
|
*/
|
|
200
207
|
function valueToText(value) {
|
|
201
208
|
if (value === void 0) return "";
|
|
202
|
-
if (
|
|
203
|
-
if (
|
|
204
|
-
if (
|
|
209
|
+
if (isString(value)) return value;
|
|
210
|
+
if (isNumber(value)) return String(value);
|
|
211
|
+
if (isBoolean(value)) return value ? "yes" : "no";
|
|
205
212
|
return value.join(", ");
|
|
206
213
|
}
|
|
207
214
|
/**
|
|
208
|
-
* Returns the choices a `select` or `checkbox` field actually
|
|
215
|
+
* Returns the choices a `select` or `checkbox` field actually offers — the form refuses a disabled
|
|
209
216
|
* choice's value at every door, including a fill, so the walk never puts one in front of the
|
|
210
217
|
* cursor. Pair with {@link filterDisabled} to tell the reader what was withheld.
|
|
211
218
|
*
|
|
@@ -216,7 +223,7 @@ function filterEnabled(choices) {
|
|
|
216
223
|
return choices.filter((choice) => choice.disabled !== true);
|
|
217
224
|
}
|
|
218
225
|
/**
|
|
219
|
-
* Returns the choices a `select` or `checkbox` field
|
|
226
|
+
* Returns the choices a `select` or `checkbox` field shows but refuses — the complement of
|
|
220
227
|
* {@link filterEnabled}, rendered by {@link renderUnavailableLine} above the list so a reader sees
|
|
221
228
|
* why a declared choice is missing from it.
|
|
222
229
|
*
|
|
@@ -239,9 +246,9 @@ function renderGroupHeader(styler, theme, label) {
|
|
|
239
246
|
return styler.render(theme.roles.message, label);
|
|
240
247
|
}
|
|
241
248
|
/**
|
|
242
|
-
* Renders the read-only line a
|
|
243
|
-
* answer the form already holds. The walk writes this instead of a prompt, because the field is
|
|
244
|
-
* validated and still submitted but must not be edited here.
|
|
249
|
+
* Renders the read-only line a locked field shows — its label, the {@link LOCKED_MARK}, and the
|
|
250
|
+
* answer the form already holds. The walk writes this instead of a prompt, because the field is
|
|
251
|
+
* still validated and still submitted but must not be edited here.
|
|
245
252
|
*
|
|
246
253
|
* @param styler - The console styler that renders each role
|
|
247
254
|
* @param theme - The resolved prompt theme
|
|
@@ -254,7 +261,7 @@ function renderLockedLine(styler, theme, label, value) {
|
|
|
254
261
|
return value.length === 0 ? head : `${head} ${styler.render(theme.roles.content, value)}`;
|
|
255
262
|
}
|
|
256
263
|
/**
|
|
257
|
-
* Renders the line listing an
|
|
264
|
+
* Renders the line listing an open select's offered values above its text prompt — a suggestion
|
|
258
265
|
* list, because an open select admits an answer the list does not offer.
|
|
259
266
|
*
|
|
260
267
|
* @param styler - The console styler that renders each role
|
|
@@ -295,12 +302,13 @@ function renderNumberedList(styler, theme, choices) {
|
|
|
295
302
|
//#endregion
|
|
296
303
|
//#region src/server/Terminal.ts
|
|
297
304
|
/**
|
|
298
|
-
* Implements {@link TerminalInterface} for a human at this machine's keyboard — the interactive
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* the form it is given, and this class owns only raw mode, the cursor, and
|
|
305
|
+
* Implements {@link TerminalInterface} for a human at this machine's keyboard — the interactive
|
|
306
|
+
* form driver, and the only impure part of the terminal stack. {@link ask} walks one form's fields
|
|
307
|
+
* in schema order, feeds raw-mode stdin bytes through `parseKey` into the matching pure reducer,
|
|
308
|
+
* renders each returned view in place, binds every answer through the form's own `fill`, and
|
|
309
|
+
* re-asks what the form refused. It owns no form logic: the schema, the rules, the values, and the
|
|
310
|
+
* settlement all belong to the form it is given, and this class owns only raw mode, the cursor, and
|
|
311
|
+
* the re-render.
|
|
304
312
|
*
|
|
305
313
|
* @remarks
|
|
306
314
|
* See {@link TerminalInterface} for the driving contract. The walk itself:
|
|
@@ -312,12 +320,11 @@ function renderNumberedList(styler, theme, choices) {
|
|
|
312
320
|
* `open` means the answer need not come from the list.
|
|
313
321
|
* - **The binding projects through `matchesAnswer`.** Every answer is filled as
|
|
314
322
|
* `fill(name, matchesAnswer(value) ? value : undefined)` after `parseValue` has coerced it to the
|
|
315
|
-
* control's own shape, so a bare return on a field with no default binds as
|
|
323
|
+
* control's own shape, so a bare return on a field with no default binds as absence and the
|
|
316
324
|
* form's `required` rule refuses it. A typed answer the control cannot hold binds as absence and
|
|
317
325
|
* invalidates the field, so the walk asks again with the reason on screen.
|
|
318
|
-
* - **Visibility is honored.** A `hidden` field and a field
|
|
319
|
-
*
|
|
320
|
-
* header.
|
|
326
|
+
* - **Visibility is honored.** A `hidden` field and a field in `form.disabled` are skipped; a
|
|
327
|
+
* `locked` field renders read-only; entering a new group writes its label as a section header.
|
|
321
328
|
* - **Refusal re-asks.** After the walk the form is submitted. A refusal re-walks only the erroring
|
|
322
329
|
* fields the walk can edit and submits again. When every erroring field is one the walk cannot
|
|
323
330
|
* edit — hidden, locked, or disabled — the form is abandoned instead, because asking again could
|
|
@@ -745,12 +752,12 @@ var Terminal = class {
|
|
|
745
752
|
//#endregion
|
|
746
753
|
//#region src/server/factories.ts
|
|
747
754
|
/**
|
|
748
|
-
* Creates the interactive terminal form driver — the local-keyboard arm of the terminal trio,
|
|
749
|
-
* the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the
|
|
750
|
-
*
|
|
751
|
-
* form's fields in schema order, drives each control's pure reducer over raw-mode stdin,
|
|
752
|
-
* answer through the form's own `fill`, and submits. It is the only impure part of the
|
|
753
|
-
* stack.
|
|
755
|
+
* Creates the interactive terminal form driver — the local-keyboard arm of the terminal trio,
|
|
756
|
+
* beside the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the
|
|
757
|
+
* broker parks a live form until somebody elsewhere answers it, a `Terminal` answers one here: it
|
|
758
|
+
* walks the form's fields in schema order, drives each control's pure reducer over raw-mode stdin,
|
|
759
|
+
* binds every answer through the form's own `fill`, and submits. It is the only impure part of the
|
|
760
|
+
* terminal stack.
|
|
754
761
|
*
|
|
755
762
|
* @param options - See {@link TerminalOptions}
|
|
756
763
|
* @returns A {@link TerminalInterface} whose `ask` drives one whole form over the resolved streams
|
|
@@ -770,21 +777,39 @@ var Terminal = class {
|
|
|
770
777
|
* - **Non-TTY fallback.** A piped stream cannot enter raw mode, so the same walk runs over
|
|
771
778
|
* `node:readline` line input.
|
|
772
779
|
*
|
|
773
|
-
* @example
|
|
780
|
+
* @example Ask one form at this keyboard
|
|
774
781
|
* ```ts
|
|
775
782
|
* import { createForm } from '@orkestrel/form'
|
|
783
|
+
* import { isTerminalError } from '@orkestrel/terminal'
|
|
776
784
|
* import { createTerminal } from '@orkestrel/terminal/server'
|
|
777
785
|
*
|
|
778
|
-
* const terminal = createTerminal()
|
|
779
|
-
* const
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
786
|
+
* const terminal = createTerminal() // process.stdin / process.stdout by default
|
|
787
|
+
* const form = createForm({
|
|
788
|
+
* label: 'Sign up',
|
|
789
|
+
* fields: [
|
|
790
|
+
* { control: 'text', name: 'name', label: 'Your name', rule: { required: true, minimum: 2 } },
|
|
791
|
+
* { control: 'text', name: 'email', label: 'Email', rule: { required: true, email: true } },
|
|
792
|
+
* { control: 'password', name: 'token', label: 'Token' },
|
|
793
|
+
* { control: 'confirm', name: 'terms', label: 'Accept the terms', rule: { required: true } },
|
|
794
|
+
* {
|
|
795
|
+
* control: 'select',
|
|
796
|
+
* name: 'role',
|
|
797
|
+
* label: 'Role',
|
|
798
|
+
* choices: [
|
|
799
|
+
* { value: 'admin', label: 'Admin' },
|
|
800
|
+
* { value: 'viewer', label: 'Viewer', help: 'read-only' },
|
|
801
|
+
* ],
|
|
802
|
+
* },
|
|
803
|
+
* ],
|
|
804
|
+
* })
|
|
805
|
+
*
|
|
806
|
+
* try {
|
|
807
|
+
* const values = await terminal.ask(form)
|
|
808
|
+
* deploy(values)
|
|
809
|
+
* } catch (error) {
|
|
810
|
+
* // Ctrl-c: the walk ended, and the form is still `editing` for whoever owns it.
|
|
811
|
+
* if (isTerminalError(error) && error.code === 'CANCEL') form.destroy()
|
|
812
|
+
* }
|
|
788
813
|
* ```
|
|
789
814
|
*/
|
|
790
815
|
function createTerminal(options) {
|