pty-spawn 1.0.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 +181 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.mjs +271 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Hiroki Osame <hiroki.osame@gmail.com>
|
|
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
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# pty-spawn
|
|
2
|
+
|
|
3
|
+
Spawn a PTY process and `await` it like a promise. Built on [`node-pty`](https://github.com/microsoft/node-pty).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install pty-spawn
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { spawn, waitFor } from 'pty-spawn'
|
|
15
|
+
|
|
16
|
+
const subprocess = spawn('node', ['server.js'])
|
|
17
|
+
|
|
18
|
+
// Wait for specific output
|
|
19
|
+
await waitFor(subprocess, output => output.includes('Listening on port 3000'))
|
|
20
|
+
|
|
21
|
+
// Interact
|
|
22
|
+
subprocess.stdin.write('quit\n')
|
|
23
|
+
|
|
24
|
+
// Await final result
|
|
25
|
+
const result = await subprocess
|
|
26
|
+
console.log(result.output)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why?
|
|
30
|
+
|
|
31
|
+
`node-pty` gives you low-level event wiring. `pty-spawn` wraps it into a single awaitable object:
|
|
32
|
+
|
|
33
|
+
| Without `pty-spawn` | With `pty-spawn` |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| Wire up promise + event cleanup | `await subprocess` |
|
|
36
|
+
| Buffer output + poll + handle exit/timeout | `waitFor(subprocess, predicate)` |
|
|
37
|
+
| Abort/exit race conditions | Late abort never overrides a clean exit |
|
|
38
|
+
| Manual output accumulation loop | `subprocess.output` (live string) |
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `spawn(file, args?, options?)`
|
|
43
|
+
|
|
44
|
+
Spawns a PTY process. Returns a [`Subprocess`](#subprocess).
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const subprocess = spawn('node', ['server.js'], { timeout: 5000 })
|
|
48
|
+
|
|
49
|
+
// Without args
|
|
50
|
+
const subprocess = spawn('bash', { window: { cols: 120 } })
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Options
|
|
54
|
+
|
|
55
|
+
All [`node-pty` options](https://github.com/microsoft/node-pty) are supported, plus:
|
|
56
|
+
|
|
57
|
+
| Option | Type | Default | Description |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `window` | `{ cols?, rows? }` | — | PTY window size |
|
|
60
|
+
| `timeout` | `number` | `0` (disabled) | Auto-abort after _N_ ms |
|
|
61
|
+
| `signal` | `AbortSignal` | — | Abort control |
|
|
62
|
+
| `reject` | `boolean` | `true` | Reject on non-zero exit, signal, or abort. When `false`, always resolves |
|
|
63
|
+
|
|
64
|
+
### Subprocess
|
|
65
|
+
|
|
66
|
+
A `Promise<Result>` with control properties attached.
|
|
67
|
+
|
|
68
|
+
#### Properties
|
|
69
|
+
|
|
70
|
+
| Property | Type | Description |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `pid` | `number` | Process ID |
|
|
73
|
+
| `output` | `string` | Accumulated output (live — grows as the process writes) |
|
|
74
|
+
|
|
75
|
+
#### `stdin.write(data)`
|
|
76
|
+
|
|
77
|
+
Write to the process stdin.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
subprocess.stdin.write('hello\n')
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `kill(signal?, options?)`
|
|
84
|
+
|
|
85
|
+
Terminate the process and wait for exit.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
await subprocess.kill()
|
|
89
|
+
await subprocess.kill('SIGTERM')
|
|
90
|
+
await subprocess.kill('SIGTERM', { forceKill: 3000 })
|
|
91
|
+
await subprocess.kill({ forceKill: 3000 })
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`forceKill` escalates to `SIGKILL` after the given milliseconds if the process traps the initial signal. Safe to call after exit.
|
|
95
|
+
|
|
96
|
+
#### `resize(cols, rows)`
|
|
97
|
+
|
|
98
|
+
Resize the PTY window. Safe to call after exit.
|
|
99
|
+
|
|
100
|
+
#### Async iteration
|
|
101
|
+
|
|
102
|
+
The subprocess itself is `AsyncIterable<string>` — stream output chunks in real time:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
for await (const chunk of subprocess) {
|
|
106
|
+
process.stdout.write(chunk)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Async disposal
|
|
111
|
+
|
|
112
|
+
Supports [`await using`](https://github.com/tc39/proposal-explicit-resource-management) for automatic cleanup:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
{
|
|
116
|
+
await using subprocess = spawn('node', ['server.js'])
|
|
117
|
+
// killed when scope exits
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Result
|
|
122
|
+
|
|
123
|
+
`await subprocess` resolves with:
|
|
124
|
+
|
|
125
|
+
| Property | Type | Description |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `output` | `string` | All terminal output |
|
|
128
|
+
| `exitCode` | `number` | Process exit code |
|
|
129
|
+
| `signalName` | `string?` | Signal name if terminated (e.g. `'SIGTERM'`) |
|
|
130
|
+
| `file` | `string` | Spawned file path |
|
|
131
|
+
| `args` | `string[]` | Arguments passed |
|
|
132
|
+
| `durationMs` | `number` | Wall-clock duration in ms |
|
|
133
|
+
|
|
134
|
+
> [!NOTE]
|
|
135
|
+
> PTYs combine stdout and stderr into a single stream. `output` contains everything the process wrote to the terminal.
|
|
136
|
+
|
|
137
|
+
### SubprocessError
|
|
138
|
+
|
|
139
|
+
Non-zero exit or signal termination rejects with `SubprocessError`, which extends `Error` and includes all `Result` fields.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { spawn, SubprocessError } from 'pty-spawn'
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await subprocess
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof SubprocessError) {
|
|
148
|
+
error.exitCode // e.g. 1
|
|
149
|
+
error.signalName // e.g. 'SIGTERM'
|
|
150
|
+
error.output // captured output
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Abort and timeout behavior:
|
|
156
|
+
- `timeout` and `signal` abort the process, rejecting with `SubprocessError`
|
|
157
|
+
- `error.cause` contains the underlying reason (e.g. `TimeoutError`)
|
|
158
|
+
- If the process exits cleanly before the abort fires, success wins the race
|
|
159
|
+
|
|
160
|
+
### `waitFor(subprocess, predicate, options?)`
|
|
161
|
+
|
|
162
|
+
Wait for output to satisfy a predicate.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
await waitFor(subprocess, output => output.includes('Ready'))
|
|
166
|
+
|
|
167
|
+
// With timeout
|
|
168
|
+
await waitFor(subprocess, output => output.includes('Ready'), {
|
|
169
|
+
signal: AbortSignal.timeout(5000)
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The predicate receives accumulated output (since `waitFor` was called) and can be async. Calls are serialized — a slow predicate won't run concurrently.
|
|
174
|
+
|
|
175
|
+
| Option | Type | Description |
|
|
176
|
+
| --- | --- | --- |
|
|
177
|
+
| `signal` | `AbortSignal` | Abort control (use `AbortSignal.timeout()` for timeouts) |
|
|
178
|
+
|
|
179
|
+
## Inspiration
|
|
180
|
+
|
|
181
|
+
Thanks to [execa](https://github.com/sindresorhus/execa) and [nano-spawn](https://github.com/sindresorhus/nano-spawn) for the inspiration. They proved that `await spawn(...)` is the right primitive for child processes — pty-spawn brings that model to pseudo-terminals.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { IPtyForkOptions } from 'node-pty';
|
|
2
|
+
|
|
3
|
+
type Result = {
|
|
4
|
+
output: string;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
signalName?: string;
|
|
7
|
+
file: string;
|
|
8
|
+
args: readonly string[];
|
|
9
|
+
durationMs: number;
|
|
10
|
+
};
|
|
11
|
+
type WindowOptions = {
|
|
12
|
+
cols?: number;
|
|
13
|
+
rows?: number;
|
|
14
|
+
};
|
|
15
|
+
type Options = Omit<IPtyForkOptions, 'cols' | 'rows'> & {
|
|
16
|
+
window?: WindowOptions;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
timeout?: number;
|
|
19
|
+
reject?: boolean;
|
|
20
|
+
};
|
|
21
|
+
declare class SubprocessError extends Error implements Result {
|
|
22
|
+
output: string;
|
|
23
|
+
exitCode: number;
|
|
24
|
+
signalName?: string;
|
|
25
|
+
file: string;
|
|
26
|
+
args: readonly string[];
|
|
27
|
+
durationMs: number;
|
|
28
|
+
constructor(message: string, { cause, ...result }: Result & {
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
type KillOptions = {
|
|
33
|
+
forceKill?: number;
|
|
34
|
+
};
|
|
35
|
+
type Subprocess = Promise<Result> & {
|
|
36
|
+
readonly pid: number;
|
|
37
|
+
readonly output: string;
|
|
38
|
+
kill: {
|
|
39
|
+
(signal?: string, options?: KillOptions): Promise<void>;
|
|
40
|
+
(options?: KillOptions): Promise<void>;
|
|
41
|
+
};
|
|
42
|
+
resize: (cols: number, rows: number) => void;
|
|
43
|
+
stdin: {
|
|
44
|
+
write: (data: string) => void;
|
|
45
|
+
};
|
|
46
|
+
[Symbol.asyncIterator]: () => AsyncIterator<string>;
|
|
47
|
+
[Symbol.asyncDispose]: () => Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
declare function spawn(file: string, args: readonly string[], options?: Options): Subprocess;
|
|
50
|
+
declare function spawn(file: string, options?: Options): Subprocess;
|
|
51
|
+
|
|
52
|
+
type WaitForOptions = {
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
};
|
|
55
|
+
type WaitForPredicate = (output: string) => boolean | Promise<boolean>;
|
|
56
|
+
declare const waitFor: (subprocess: Subprocess, predicate: WaitForPredicate, { signal, }?: WaitForOptions) => Promise<void>;
|
|
57
|
+
|
|
58
|
+
export { SubprocessError, spawn, waitFor };
|
|
59
|
+
export type { KillOptions, Options, Result, Subprocess, WaitForOptions, WaitForPredicate, WindowOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { EventEmitter, on } from 'node:events';
|
|
2
|
+
import { constants } from 'node:os';
|
|
3
|
+
import { spawn as spawn$1 } from 'node-pty';
|
|
4
|
+
|
|
5
|
+
const getSignalName = (signal) => {
|
|
6
|
+
if (signal === void 0) {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
for (const [name, number] of Object.entries(constants.signals)) {
|
|
10
|
+
if (number === signal) {
|
|
11
|
+
return name;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return void 0;
|
|
15
|
+
};
|
|
16
|
+
const createAbortSignal = (userSignal, timeout) => {
|
|
17
|
+
if (!userSignal && timeout <= 0) {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
if (!userSignal) {
|
|
21
|
+
return AbortSignal.timeout(timeout);
|
|
22
|
+
}
|
|
23
|
+
if (timeout <= 0) {
|
|
24
|
+
return userSignal;
|
|
25
|
+
}
|
|
26
|
+
return AbortSignal.any([userSignal, AbortSignal.timeout(timeout)]);
|
|
27
|
+
};
|
|
28
|
+
class SubprocessError extends Error {
|
|
29
|
+
output;
|
|
30
|
+
exitCode;
|
|
31
|
+
signalName;
|
|
32
|
+
file;
|
|
33
|
+
args;
|
|
34
|
+
durationMs;
|
|
35
|
+
constructor(message, { cause, ...result }) {
|
|
36
|
+
super(message, { cause });
|
|
37
|
+
this.name = "SubprocessError";
|
|
38
|
+
Object.assign(this, result);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function spawn(file, argsOrOptions = [], maybeOptions = {}) {
|
|
42
|
+
const args = Array.isArray(argsOrOptions) ? [...argsOrOptions] : [];
|
|
43
|
+
const options = Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions;
|
|
44
|
+
const {
|
|
45
|
+
window,
|
|
46
|
+
signal: userSignal,
|
|
47
|
+
timeout = 0,
|
|
48
|
+
reject: shouldReject = true,
|
|
49
|
+
...ptyOptions
|
|
50
|
+
} = options;
|
|
51
|
+
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
52
|
+
throw new TypeError("options.timeout must be a non-negative finite number.");
|
|
53
|
+
}
|
|
54
|
+
const startedAt = Date.now();
|
|
55
|
+
const ptyProcess = spawn$1(file, args, {
|
|
56
|
+
...ptyOptions,
|
|
57
|
+
cols: window?.cols,
|
|
58
|
+
rows: window?.rows
|
|
59
|
+
});
|
|
60
|
+
const signal = createAbortSignal(userSignal, timeout);
|
|
61
|
+
const emitter = new EventEmitter();
|
|
62
|
+
let output = "";
|
|
63
|
+
let exitCode;
|
|
64
|
+
let exitSignalName;
|
|
65
|
+
let lastDataAt = 0;
|
|
66
|
+
let settled = false;
|
|
67
|
+
let abortedBeforeExit = false;
|
|
68
|
+
let settleQuietTimer;
|
|
69
|
+
let settleMaxTimer;
|
|
70
|
+
const quietMs = 50;
|
|
71
|
+
const maxMs = 3e3;
|
|
72
|
+
let resolveResult;
|
|
73
|
+
let rejectResult;
|
|
74
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
75
|
+
resolveResult = resolve;
|
|
76
|
+
rejectResult = reject;
|
|
77
|
+
});
|
|
78
|
+
const safeKill = (killSignal) => {
|
|
79
|
+
try {
|
|
80
|
+
ptyProcess.kill(killSignal);
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const settle = (code) => {
|
|
85
|
+
if (settled) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
settled = true;
|
|
89
|
+
if (settleQuietTimer) {
|
|
90
|
+
clearTimeout(settleQuietTimer);
|
|
91
|
+
}
|
|
92
|
+
if (settleMaxTimer) {
|
|
93
|
+
clearTimeout(settleMaxTimer);
|
|
94
|
+
}
|
|
95
|
+
signal?.removeEventListener("abort", onAbort);
|
|
96
|
+
const result = {
|
|
97
|
+
output,
|
|
98
|
+
exitCode: code,
|
|
99
|
+
signalName: exitSignalName,
|
|
100
|
+
file,
|
|
101
|
+
args,
|
|
102
|
+
durationMs: Date.now() - startedAt
|
|
103
|
+
};
|
|
104
|
+
if (!shouldReject) {
|
|
105
|
+
resolveResult(result);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (signal?.aborted && abortedBeforeExit) {
|
|
109
|
+
rejectResult(new SubprocessError("Subprocess aborted.", {
|
|
110
|
+
...result,
|
|
111
|
+
cause: signal.reason
|
|
112
|
+
}));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (code !== 0 || exitSignalName) {
|
|
116
|
+
const message = exitSignalName ? `Subprocess terminated by signal ${exitSignalName}.` : `Subprocess exited with code ${code}.`;
|
|
117
|
+
rejectResult(new SubprocessError(message, result));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
resolveResult(result);
|
|
121
|
+
};
|
|
122
|
+
const scheduleSettle = () => {
|
|
123
|
+
if (exitCode === void 0) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const elapsedSinceLastData = lastDataAt === 0 ? 0 : Date.now() - lastDataAt;
|
|
127
|
+
const quietDelayMs = Math.max(0, quietMs - elapsedSinceLastData);
|
|
128
|
+
if (settleQuietTimer) {
|
|
129
|
+
clearTimeout(settleQuietTimer);
|
|
130
|
+
}
|
|
131
|
+
const code = exitCode;
|
|
132
|
+
settleQuietTimer = setTimeout(() => {
|
|
133
|
+
settle(code);
|
|
134
|
+
}, quietDelayMs);
|
|
135
|
+
};
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
if (exitCode === void 0) {
|
|
138
|
+
abortedBeforeExit = true;
|
|
139
|
+
}
|
|
140
|
+
safeKill();
|
|
141
|
+
};
|
|
142
|
+
if (signal?.aborted) {
|
|
143
|
+
onAbort();
|
|
144
|
+
} else {
|
|
145
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
146
|
+
}
|
|
147
|
+
ptyProcess.onData((data) => {
|
|
148
|
+
lastDataAt = Date.now();
|
|
149
|
+
output += data;
|
|
150
|
+
emitter.emit("data", data);
|
|
151
|
+
if (exitCode !== void 0) {
|
|
152
|
+
scheduleSettle();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
ptyProcess.onExit(({ exitCode: nextExitCode, signal: exitSignal }) => {
|
|
156
|
+
exitCode = nextExitCode;
|
|
157
|
+
exitSignalName = getSignalName(exitSignal);
|
|
158
|
+
emitter.emit("exit", nextExitCode);
|
|
159
|
+
scheduleSettle();
|
|
160
|
+
settleMaxTimer = setTimeout(() => {
|
|
161
|
+
settle(nextExitCode);
|
|
162
|
+
}, maxMs);
|
|
163
|
+
});
|
|
164
|
+
const iterateOutput = async function* iterateOutput2() {
|
|
165
|
+
if (exitCode !== void 0) {
|
|
166
|
+
await resultPromise;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const iteratorAbort = new AbortController();
|
|
170
|
+
const onIteratorExit = () => {
|
|
171
|
+
iteratorAbort.abort();
|
|
172
|
+
};
|
|
173
|
+
emitter.once("exit", onIteratorExit);
|
|
174
|
+
if (exitCode !== void 0) {
|
|
175
|
+
iteratorAbort.abort();
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
for await (const [chunk] of on(emitter, "data", { signal: iteratorAbort.signal })) {
|
|
179
|
+
yield chunk;
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!iteratorAbort.signal.aborted) {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
} finally {
|
|
186
|
+
emitter.off("exit", onIteratorExit);
|
|
187
|
+
await resultPromise;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const kill = async (signalOrOptions, killOptions) => {
|
|
191
|
+
const killSignal = typeof signalOrOptions === "string" ? signalOrOptions : void 0;
|
|
192
|
+
const { forceKill } = typeof signalOrOptions === "object" ? signalOrOptions : killOptions ?? {};
|
|
193
|
+
safeKill(killSignal);
|
|
194
|
+
const forceKillTimer = forceKill === void 0 ? void 0 : setTimeout(() => safeKill("SIGKILL"), forceKill);
|
|
195
|
+
await resultPromise.catch(() => {
|
|
196
|
+
});
|
|
197
|
+
if (forceKillTimer) {
|
|
198
|
+
clearTimeout(forceKillTimer);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const subprocess = Object.assign(resultPromise, {
|
|
202
|
+
pid: ptyProcess.pid,
|
|
203
|
+
kill,
|
|
204
|
+
resize: (cols, rows) => {
|
|
205
|
+
try {
|
|
206
|
+
ptyProcess.resize(cols, rows);
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
stdin: {
|
|
211
|
+
write: (data) => {
|
|
212
|
+
ptyProcess.write(data);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
[Symbol.asyncIterator]: iterateOutput,
|
|
216
|
+
[Symbol.asyncDispose]: kill
|
|
217
|
+
});
|
|
218
|
+
Object.defineProperty(subprocess, "output", {
|
|
219
|
+
get: () => output,
|
|
220
|
+
enumerable: true,
|
|
221
|
+
configurable: true
|
|
222
|
+
});
|
|
223
|
+
return subprocess;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const waitFor = async (subprocess, predicate, {
|
|
227
|
+
signal
|
|
228
|
+
} = {}) => {
|
|
229
|
+
if (signal?.aborted) {
|
|
230
|
+
throw signal.reason;
|
|
231
|
+
}
|
|
232
|
+
let output = "";
|
|
233
|
+
const iterator = subprocess[Symbol.asyncIterator]();
|
|
234
|
+
let removeAbortListener;
|
|
235
|
+
const abortPromise = signal ? new Promise((_resolve, reject) => {
|
|
236
|
+
const handler = () => reject(signal.reason);
|
|
237
|
+
signal.addEventListener("abort", handler, { once: true });
|
|
238
|
+
removeAbortListener = () => signal.removeEventListener("abort", handler);
|
|
239
|
+
}) : void 0;
|
|
240
|
+
abortPromise?.catch(() => {
|
|
241
|
+
});
|
|
242
|
+
try {
|
|
243
|
+
while (true) {
|
|
244
|
+
const next = abortPromise ? await Promise.race([iterator.next(), abortPromise]) : await iterator.next();
|
|
245
|
+
if (next.done) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
output += next.value;
|
|
249
|
+
if (await predicate(output)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (signal?.aborted) {
|
|
253
|
+
throw signal.reason;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (signal?.aborted) {
|
|
258
|
+
throw signal.reason;
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
} finally {
|
|
262
|
+
removeAbortListener?.();
|
|
263
|
+
}
|
|
264
|
+
const exitCode = await subprocess.then((result) => result.exitCode).catch((error) => error.exitCode);
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Process exited with code ${exitCode} before waitFor predicate was satisfied.
|
|
267
|
+
Last output: ${JSON.stringify(output.slice(-200))}`
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export { SubprocessError, spawn, waitFor };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pty-spawn",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tiny pseudo-terminal spawning for humans",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pty",
|
|
7
|
+
"terminal",
|
|
8
|
+
"spawn",
|
|
9
|
+
"subprocess",
|
|
10
|
+
"node-pty"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": "privatenumber/pty-spawn",
|
|
14
|
+
"funding": "https://github.com/privatenumber/pty-spawn?sponsor=1",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Hiroki Osame",
|
|
17
|
+
"email": "hiroki.osame@gmail.com"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"default": "./dist/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"imports": {
|
|
28
|
+
"#pty-spawn": {
|
|
29
|
+
"dev": "./src/index.ts",
|
|
30
|
+
"default": "./dist/index.mjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20.20.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"node-pty": "1.2.0-beta.10"
|
|
38
|
+
}
|
|
39
|
+
}
|