@envseal/prompters 0.1.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 +201 -0
- package/dist/ide.d.ts +8 -0
- package/dist/ide.js +173 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +13 -0
- package/dist/loopback.d.ts +25 -0
- package/dist/loopback.js +415 -0
- package/dist/native.d.ts +9 -0
- package/dist/native.js +196 -0
- package/dist/none.d.ts +12 -0
- package/dist/none.js +23 -0
- package/dist/registry.d.ts +16 -0
- package/dist/registry.js +60 -0
- package/dist/tty.d.ts +10 -0
- package/dist/tty.js +166 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +18 -0
- package/package.json +33 -0
package/dist/registry.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { IdePrompter } from './ide.js';
|
|
2
|
+
import { LoopbackPrompter } from './loopback.js';
|
|
3
|
+
import { NativePrompter } from './native.js';
|
|
4
|
+
import { NonePrompter } from './none.js';
|
|
5
|
+
import { TtyPrompter } from './tty.js';
|
|
6
|
+
let ide = null;
|
|
7
|
+
let loopback = null;
|
|
8
|
+
let native = null;
|
|
9
|
+
let tty = null;
|
|
10
|
+
const none = new NonePrompter();
|
|
11
|
+
export function allPrompters() {
|
|
12
|
+
loopback ??= new LoopbackPrompter();
|
|
13
|
+
native ??= new NativePrompter();
|
|
14
|
+
ide ??= new IdePrompter();
|
|
15
|
+
tty ??= new TtyPrompter();
|
|
16
|
+
return [loopback, native, ide, tty, none];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Select the first available surface in the order mandated by PLAN.md §5.3:
|
|
20
|
+
* prefer -> ide -> native-dialog (SEP_PREFER_NATIVE) -> loopback-browser ->
|
|
21
|
+
* tty (opt-in) -> none. A `CI` environment forces `none` unless `prefer`
|
|
22
|
+
* names a concrete surface.
|
|
23
|
+
*/
|
|
24
|
+
export async function selectPrompter(opts = {}) {
|
|
25
|
+
if (opts.prefer !== undefined) {
|
|
26
|
+
const preferred = allPrompters().find((p) => p.id === opts.prefer);
|
|
27
|
+
if (preferred === undefined) {
|
|
28
|
+
throw new Error(`unknown prompter id: ${opts.prefer}`);
|
|
29
|
+
}
|
|
30
|
+
if (await preferred.available()) {
|
|
31
|
+
return preferred;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`preferred prompter is not available: ${opts.prefer}`);
|
|
34
|
+
}
|
|
35
|
+
if (process.env.CI !== undefined) {
|
|
36
|
+
return none;
|
|
37
|
+
}
|
|
38
|
+
ide ??= new IdePrompter();
|
|
39
|
+
if (await ide.available()) {
|
|
40
|
+
return ide;
|
|
41
|
+
}
|
|
42
|
+
if (process.env.SEP_PREFER_NATIVE !== undefined) {
|
|
43
|
+
native ??= new NativePrompter();
|
|
44
|
+
if (await native.available()) {
|
|
45
|
+
return native;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
loopback ??= new LoopbackPrompter();
|
|
49
|
+
if (await loopback.available()) {
|
|
50
|
+
return loopback;
|
|
51
|
+
}
|
|
52
|
+
if (opts.allowTty === true) {
|
|
53
|
+
tty ??= new TtyPrompter();
|
|
54
|
+
if (await tty.available()) {
|
|
55
|
+
return tty;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return none;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=registry.js.map
|
package/dist/tty.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Prompter, PromptRequest, PromptResponse } from './types.js';
|
|
2
|
+
export declare class TtyPrompter implements Prompter {
|
|
3
|
+
readonly id: 'tty';
|
|
4
|
+
available(): Promise<boolean>;
|
|
5
|
+
private openReadFd;
|
|
6
|
+
private openWriteFd;
|
|
7
|
+
cancel(_ticket: string): Promise<void>;
|
|
8
|
+
prompt(req: PromptRequest): Promise<PromptResponse>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=tty.d.ts.map
|
package/dist/tty.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// NOTE: This is NOT the default surface. Opening the controlling TTY collides
|
|
2
|
+
// with a harness's full-screen TUI repainting (PLAN.md §5.3). It is only
|
|
3
|
+
// selected explicitly, or as the last usable fallback when allowed.
|
|
4
|
+
import { openSync, writeSync, closeSync } from 'node:fs';
|
|
5
|
+
import { ReadStream } from 'node:tty';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { asSecret } from '@envseal/protocol';
|
|
8
|
+
const POSIX_DEVICE = '/dev/tty';
|
|
9
|
+
const WIN_IN = 'CONIN$';
|
|
10
|
+
const WIN_OUT = 'CONOUT$';
|
|
11
|
+
const ENABLE_ECHO_INPUT = 0x4;
|
|
12
|
+
function isPosix() {
|
|
13
|
+
return process.platform !== 'win32';
|
|
14
|
+
}
|
|
15
|
+
function readLineRaw(inStream, outFd, prompt) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
writeSync(outFd, `${prompt}: `);
|
|
18
|
+
let buffer = '';
|
|
19
|
+
let done = false;
|
|
20
|
+
const settle = (value, err) => {
|
|
21
|
+
if (done) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
done = true;
|
|
25
|
+
process.removeListener('SIGINT', onSigint);
|
|
26
|
+
inStream.removeListener('data', onData);
|
|
27
|
+
inStream.removeListener('error', onError);
|
|
28
|
+
if (err !== undefined) {
|
|
29
|
+
reject(err);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
resolve(value);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const onSigint = () => {
|
|
36
|
+
settle('');
|
|
37
|
+
};
|
|
38
|
+
const onData = (chunk) => {
|
|
39
|
+
const text = chunk.toString('utf8');
|
|
40
|
+
const at = text.search(/[\r\n]/);
|
|
41
|
+
if (at !== -1) {
|
|
42
|
+
settle(buffer + text.slice(0, at));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
buffer += text;
|
|
46
|
+
};
|
|
47
|
+
const onError = (err) => {
|
|
48
|
+
settle('', err);
|
|
49
|
+
};
|
|
50
|
+
process.once('SIGINT', onSigint);
|
|
51
|
+
inStream.on('data', onData);
|
|
52
|
+
inStream.on('error', onError);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function winConsoleScript(enableEcho) {
|
|
56
|
+
const setMode = enableEcho
|
|
57
|
+
? '($m -bor 4)'
|
|
58
|
+
: '($m -band (-bnot 4))';
|
|
59
|
+
return ("$sig='[DllImport(\"kernel32.dll\",SetLastError=$true)] public static extern bool " +
|
|
60
|
+
'SetConsoleMode(IntPtr h,uint mode); [DllImport("kernel32.dll",SetLastError=$true)] ' +
|
|
61
|
+
'public static extern bool GetConsoleMode(IntPtr h,[ref]uint mode); ' +
|
|
62
|
+
'[DllImport("kernel32.dll",SetLastError=$true)] public static extern IntPtr GetStdHandle(int n);\'; ' +
|
|
63
|
+
'Add-Type -Namespace EnvSeal -MemberDefinition $sig -Name Native; ' +
|
|
64
|
+
'$h=[EnvSeal.Native]::GetStdHandle(-10); ' +
|
|
65
|
+
'[uint32]$m=0; ' +
|
|
66
|
+
'[void][EnvSeal.Native]::GetConsoleMode($h,[ref]$m); ' +
|
|
67
|
+
`[void][EnvSeal.Native]::SetConsoleMode($h,${setMode}); `);
|
|
68
|
+
}
|
|
69
|
+
/** Toggle echo on the console input buffer shared by this process (SetConsoleMode). */
|
|
70
|
+
function setWinConinEcho(enabled) {
|
|
71
|
+
const encoded = Buffer.from(winConsoleScript(enabled), 'utf8').toString('base64');
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const child = spawn('powershell', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
});
|
|
76
|
+
child.once('error', reject);
|
|
77
|
+
child.once('exit', (code) => {
|
|
78
|
+
if (code === 0) {
|
|
79
|
+
resolve();
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
reject(new Error(`SetConsoleMode failed with exit code ${code}`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export class TtyPrompter {
|
|
88
|
+
id = 'tty';
|
|
89
|
+
async available() {
|
|
90
|
+
try {
|
|
91
|
+
const fd = this.openReadFd();
|
|
92
|
+
new ReadStream(fd).destroy();
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
openReadFd() {
|
|
100
|
+
return isPosix() ? openSync(POSIX_DEVICE, 'r+') : openSync(WIN_IN, 'r');
|
|
101
|
+
}
|
|
102
|
+
openWriteFd(readFd) {
|
|
103
|
+
return isPosix() ? readFd : openSync(WIN_OUT, 'w');
|
|
104
|
+
}
|
|
105
|
+
async cancel(_ticket) {
|
|
106
|
+
// A raw read is interrupted from the reading side (SIGINT or device close).
|
|
107
|
+
}
|
|
108
|
+
async prompt(req) {
|
|
109
|
+
const results = [];
|
|
110
|
+
const readFd = this.openReadFd();
|
|
111
|
+
const writeFd = this.openWriteFd(readFd);
|
|
112
|
+
const inStream = new ReadStream(readFd);
|
|
113
|
+
let modeChanged = false;
|
|
114
|
+
try {
|
|
115
|
+
if (isPosix()) {
|
|
116
|
+
inStream.setRawMode(true);
|
|
117
|
+
modeChanged = true;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
await setWinConinEcho(false);
|
|
121
|
+
modeChanged = true;
|
|
122
|
+
}
|
|
123
|
+
for (const key of req.keys) {
|
|
124
|
+
const line = await readLineRaw(inStream, writeFd, formatLabel(key));
|
|
125
|
+
if (line === '') {
|
|
126
|
+
results.push({ key: key.key, outcome: 'skipped' });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
results.push({ key: key.key, outcome: 'entered', value: asSecret(Buffer.from(line, 'utf8')) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
if (isPosix()) {
|
|
134
|
+
if (modeChanged) {
|
|
135
|
+
try {
|
|
136
|
+
inStream.setRawMode(false);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// stream already destroyed
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (modeChanged) {
|
|
144
|
+
try {
|
|
145
|
+
await setWinConinEcho(true);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// best-effort restore
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
inStream.destroy();
|
|
152
|
+
if (!isPosix()) {
|
|
153
|
+
closeSync(writeFd);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { ticket: req.ticket, results };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function formatLabel(key) {
|
|
160
|
+
let label = key.key;
|
|
161
|
+
if (key.description) {
|
|
162
|
+
label += ` (${key.description})`;
|
|
163
|
+
}
|
|
164
|
+
return label;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=tty.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SecretValue } from '@envseal/protocol';
|
|
2
|
+
export interface PromptKeyRequest {
|
|
3
|
+
key: string;
|
|
4
|
+
description: string;
|
|
5
|
+
providerName?: string;
|
|
6
|
+
signupUrl?: string;
|
|
7
|
+
docsUrl?: string;
|
|
8
|
+
formatHint?: string;
|
|
9
|
+
pattern?: string;
|
|
10
|
+
optional?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface PromptRequest {
|
|
13
|
+
ticket: string;
|
|
14
|
+
nonce: string;
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
keys: PromptKeyRequest[];
|
|
18
|
+
timeoutMs: number;
|
|
19
|
+
}
|
|
20
|
+
export type PromptKeyResult = {
|
|
21
|
+
key: string;
|
|
22
|
+
outcome: 'entered';
|
|
23
|
+
value: SecretValue;
|
|
24
|
+
} | {
|
|
25
|
+
key: string;
|
|
26
|
+
outcome: 'skipped' | 'cancelled' | 'timeout';
|
|
27
|
+
};
|
|
28
|
+
export interface PromptResponse {
|
|
29
|
+
ticket: string;
|
|
30
|
+
results: PromptKeyResult[];
|
|
31
|
+
}
|
|
32
|
+
export type PrompterId = 'loopback-browser' | 'native-dialog' | 'ide' | 'tty' | 'none';
|
|
33
|
+
export interface Prompter {
|
|
34
|
+
readonly id: PrompterId;
|
|
35
|
+
available(): Promise<boolean>;
|
|
36
|
+
prompt(req: PromptRequest): Promise<PromptResponse>;
|
|
37
|
+
cancel(ticket: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export declare function makeDisplayNonce(): string;
|
|
40
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
3
|
+
export function makeDisplayNonce() {
|
|
4
|
+
const bytes = randomBytes(8);
|
|
5
|
+
let out = '';
|
|
6
|
+
for (let i = 0; i < 8; i += 1) {
|
|
7
|
+
const byte = bytes[i];
|
|
8
|
+
if (byte === undefined) {
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
out += CROCKFORD.charAt(byte & 0x1f);
|
|
12
|
+
if (i === 3) {
|
|
13
|
+
out += '-';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@envseal/prompters",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"!dist/**/*.map"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"provenance": true
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@envseal/protocol": "0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"undici": "^7.2.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
}
|
|
33
|
+
}
|