@shardflux/cli 0.4.0 → 0.5.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/dist/input.js ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Secrets a person types or a script pipes (0.5.0+): passwords never come from arguments (shell history and process
3
+ * listings would expose them; the parser refuses --password). On a terminal, `shard` prompts on stderr without echo
4
+ * (raw mode: Enter ends, Backspace and Ctrl-U edit, Ctrl-C interrupts, Ctrl-D on an empty line ends input); otherwise
5
+ * it reads standard input one line per secret, so a command that needs two (the current and the new password) reads
6
+ * two lines:
7
+ *
8
+ * printf '%s\n' "$PASSWORD" | shard auth login --email me@example.com
9
+ * printf '%s\n%s\n' "$OLD" "$NEW" | shard auth password change
10
+ *
11
+ * One Input serves the whole command, so several reads continue where the previous one stopped.
12
+ */
13
+ import { InterruptedError, UsageError } from "./errors.js";
14
+ /** More than any password or code: a runaway pipe is refused instead of buffered. */
15
+ const MAX_PIPED_BYTES = 64 * 1024;
16
+ export class Input {
17
+ #stdin;
18
+ #stderr;
19
+ #it = null;
20
+ #decoder = new TextDecoder('utf-8');
21
+ #buf = '';
22
+ #eof = false;
23
+ #read = 0;
24
+ constructor(stdin, stderr) {
25
+ this.#stdin = stdin;
26
+ this.#stderr = stderr;
27
+ }
28
+ /** True when standard input is a terminal `shard` can prompt on. */
29
+ get interactive() {
30
+ return this.#stdin.isTTY === true;
31
+ }
32
+ async #more() {
33
+ if (this.#eof)
34
+ return false;
35
+ this.#it ??= this.#stdin[Symbol.asyncIterator]();
36
+ const r = await this.#it.next();
37
+ if (r.done) {
38
+ this.#eof = true;
39
+ this.#buf += this.#decoder.decode();
40
+ return false;
41
+ }
42
+ const chunk = typeof r.value === 'string' ? Buffer.from(r.value) : r.value;
43
+ this.#read += chunk.length;
44
+ if (this.#read > MAX_PIPED_BYTES && !this.interactive)
45
+ throw new UsageError(`standard input exceeds ${MAX_PIPED_BYTES} bytes; pipe one secret per line`);
46
+ this.#buf += this.#decoder.decode(chunk, { stream: true });
47
+ return true;
48
+ }
49
+ /** The next line of piped input without its line ending, or null at the end of input. */
50
+ async line() {
51
+ for (;;) {
52
+ const nl = this.#buf.indexOf('\n');
53
+ if (nl >= 0) {
54
+ const line = this.#buf.slice(0, nl);
55
+ this.#buf = this.#buf.slice(nl + 1);
56
+ return line.endsWith('\r') ? line.slice(0, -1) : line;
57
+ }
58
+ if (!(await this.#more())) {
59
+ if (this.#buf.length === 0)
60
+ return null;
61
+ const line = this.#buf;
62
+ this.#buf = '';
63
+ return line.endsWith('\r') ? line.slice(0, -1) : line;
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * A secret: a no-echo prompt on a terminal, else the next piped line. `what` names it in errors ("the password").
69
+ * An empty secret is refused.
70
+ */
71
+ async secret(prompt, what, pipeHint) {
72
+ const value = this.interactive ? await this.#prompt(prompt) : await this.line();
73
+ if (value === null) {
74
+ throw new UsageError(`${what} is read from standard input (never from arguments), and standard input ended: ${pipeHint}`);
75
+ }
76
+ if (value.length === 0)
77
+ throw new UsageError(`${what} is empty`);
78
+ return value;
79
+ }
80
+ /** A new password: asked twice (and compared) on a terminal, one piped line otherwise. */
81
+ async newPassword(prompt, what, pipeHint) {
82
+ const first = await this.secret(prompt, what, pipeHint);
83
+ if (!this.interactive)
84
+ return first;
85
+ const again = await this.secret('Repeat it: ', what, pipeHint);
86
+ if (again !== first)
87
+ throw new UsageError('the two passwords do not match');
88
+ return first;
89
+ }
90
+ /** A visible answer to a question on a terminal (e.g. an authenticator code), else the next piped line. */
91
+ async answer(prompt) {
92
+ if (!this.interactive)
93
+ return this.line();
94
+ return this.#prompt(prompt, true);
95
+ }
96
+ async #prompt(prompt, echo = false) {
97
+ const raw = this.#stdin.setRawMode;
98
+ if (typeof raw !== 'function')
99
+ throw new UsageError('standard input is a terminal that cannot hide what you type; pipe the value in instead');
100
+ this.#stderr.write(prompt);
101
+ raw.call(this.#stdin, true);
102
+ let value = '';
103
+ try {
104
+ for (;;) {
105
+ if (this.#buf.length === 0 && !(await this.#more())) {
106
+ this.#stderr.write('\n');
107
+ if (value.length > 0)
108
+ return value;
109
+ throw new UsageError('standard input ended before anything was entered');
110
+ }
111
+ while (this.#buf.length > 0) {
112
+ const cp = this.#buf.codePointAt(0);
113
+ const ch = String.fromCodePoint(cp);
114
+ this.#buf = this.#buf.slice(ch.length);
115
+ if (ch === '\r' || ch === '\n') {
116
+ // A CR LF pair (a paste) ends the entry once.
117
+ if (ch === '\r' && this.#buf.startsWith('\n'))
118
+ this.#buf = this.#buf.slice(1);
119
+ this.#stderr.write('\n');
120
+ return value;
121
+ }
122
+ if (ch === '\u0003') {
123
+ this.#stderr.write('\n');
124
+ throw new InterruptedError('Interrupted.');
125
+ }
126
+ if (ch === '\u0004') {
127
+ this.#stderr.write('\n');
128
+ if (value.length > 0)
129
+ return value;
130
+ throw new UsageError('input ended before anything was entered');
131
+ }
132
+ if (ch === '\u007f' || ch === '\b') {
133
+ if (value.length > 0) {
134
+ const chars = [...value];
135
+ chars.pop();
136
+ value = chars.join('');
137
+ if (echo)
138
+ this.#stderr.write('\b \b');
139
+ }
140
+ continue;
141
+ }
142
+ if (ch === '\u0015') {
143
+ if (echo)
144
+ this.#stderr.write('\b \b'.repeat([...value].length));
145
+ value = '';
146
+ continue;
147
+ }
148
+ if (ch === '\u001b') {
149
+ // An escape sequence (arrow keys, ...): CSI/SS3 up to its final byte, else the next character.
150
+ const m = /^(?:\[[0-?]*[ -/]*[@-~]|O.|.)/s.exec(this.#buf);
151
+ if (m)
152
+ this.#buf = this.#buf.slice(m[0].length);
153
+ continue;
154
+ }
155
+ if (cp < 0x20)
156
+ continue;
157
+ value += ch;
158
+ if (echo)
159
+ this.#stderr.write(ch);
160
+ }
161
+ }
162
+ }
163
+ finally {
164
+ raw.call(this.#stdin, false);
165
+ }
166
+ }
167
+ }
package/dist/main.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import type { Io } from './commands.js';
2
+ import type { Distribution } from './update-check.js';
2
3
  /** A sleep that ends early when the signal aborts (so Ctrl-C never waits out a poll interval). */
3
4
  export declare function abortableSleep(signal: AbortSignal): (ms: number) => Promise<void>;
4
- export declare function run(argv: readonly string[], io: Io, opts?: {
5
+ export interface RunOptions {
5
6
  signal?: AbortSignal;
6
- }): Promise<number>;
7
+ /** The package that runs the CLI, for the version check (default @shardflux/cli at CLI_VERSION). */
8
+ distribution?: Distribution;
9
+ }
10
+ export declare function run(argv: readonly string[], io: Io, opts?: RunOptions): Promise<number>;
11
+ export interface MainOptions {
12
+ /** The package that runs the CLI (the `shardflux` bundle passes its name and version); default @shardflux/cli. */
13
+ distribution?: Distribution;
14
+ }
15
+ /**
16
+ * Runs `shard` as the process: argv, stdio and environment of this process. Ctrl-C aborts in-flight waits and
17
+ * streams cleanly (exit 130); a second Ctrl-C exits at once. Output is flushed before exiting (stdout/stderr pipes
18
+ * are asynchronous on macOS).
19
+ */
20
+ export declare function main(opts?: MainOptions): Promise<never>;