withruntime 0.14.0 → 0.16.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/BLAXEL.md +97 -32
- package/COMPATIBILITY.md +120 -5
- package/DAYTONA.md +34 -3
- package/E2B.md +34 -5
- package/README.md +33 -0
- package/README.src.md +33 -0
- package/VERCEL.md +52 -5
- package/compatibility-lock.json +71 -111
- package/dist/blaxel/client.d.ts +11 -0
- package/dist/blaxel/client.js +28 -0
- package/dist/blaxel/errors.d.ts +65 -0
- package/dist/blaxel/errors.js +114 -0
- package/dist/blaxel/index.d.ts +10 -3
- package/dist/blaxel/index.js +23 -4
- package/dist/blaxel/preview.d.ts +29 -4
- package/dist/blaxel/preview.js +79 -5
- package/dist/blaxel/sandbox.js +1 -0
- package/dist/blaxel/session.js +4 -2
- package/dist/cli-extras.js +2 -1
- package/dist/cli-words.js +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +54 -21
- package/dist/client.d.ts +1 -1
- package/dist/daytona/client.d.ts +4 -0
- package/dist/daytona/client.js +8 -3
- package/dist/daytona/daytona.d.ts +3 -0
- package/dist/daytona/daytona.js +2 -1
- package/dist/e2b/sandbox.d.ts +6 -5
- package/dist/e2b/sandbox.js +9 -12
- package/dist/freestyle/index.d.ts +77 -5
- package/dist/freestyle/index.js +380 -48
- package/dist/identity.js +36 -18
- package/dist/index.d.ts +2 -2
- package/dist/machine.d.ts +24 -12
- package/dist/machine.js +46 -14
- package/dist/mcp.js +16 -3
- package/dist/modal/index.d.ts +15 -0
- package/dist/modal/index.js +35 -7
- package/dist/morph/index.d.ts +255 -0
- package/dist/morph/index.js +641 -0
- package/dist/network-cli.js +2 -2
- package/dist/product-names.d.ts +2 -2
- package/dist/product-names.js +2 -2
- package/dist/products/limits.d.ts +16 -2
- package/dist/products/limits.js +2 -2
- package/dist/products/mcp.d.ts +5 -0
- package/dist/products/network-products.d.ts +21 -3
- package/dist/products/network-products.js +3 -1
- package/dist/products/previews.d.ts +6 -3
- package/dist/products/previews.js +1 -1
- package/dist/snapshots.d.ts +4 -0
- package/dist/ssh.d.ts +35 -0
- package/dist/ssh.js +93 -1
- package/dist/transport.d.ts +11 -8
- package/dist/transport.js +46 -12
- package/dist/types.d.ts +24 -0
- package/dist/vercel/command.d.ts +48 -1
- package/dist/vercel/command.js +237 -6
- package/dist/vercel/sandbox-user.js +2 -1
- package/dist/vercel/sandbox.d.ts +16 -2
- package/dist/vercel/sandbox.js +45 -2
- package/package.json +6 -1
package/dist/vercel/command.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { guard, StreamError, translate } from "./errors.js";
|
|
1
|
+
import { APIError, guard, StreamError, translate } from "./errors.js";
|
|
2
2
|
const SIGNAL_NUMBERS = {
|
|
3
3
|
1: "SIGHUP",
|
|
4
4
|
2: "SIGINT",
|
|
@@ -22,6 +22,11 @@ function join(lines, stream) {
|
|
|
22
22
|
.map((line) => line.data)
|
|
23
23
|
.join("");
|
|
24
24
|
}
|
|
25
|
+
/** One stdin request carries at most this much of a write (Runtime's input
|
|
26
|
+
* request size); a client error on it means none of it was applied. */
|
|
27
|
+
const STDIN_REQUEST_BYTES = 1_048_576;
|
|
28
|
+
/** Reading a piped stream pauses once this much waits behind a write. */
|
|
29
|
+
const STDIN_BUFFERED_BYTES = 512 * 1024;
|
|
25
30
|
/** A command in a sandbox, as Vercel's `Command`. A detached one runs on:
|
|
26
31
|
* `wait()` for its end, `logs()` for its output as it happens. `cmdId` is
|
|
27
32
|
* Runtime's process id. */
|
|
@@ -30,6 +35,13 @@ export class Command {
|
|
|
30
35
|
durationMs;
|
|
31
36
|
init;
|
|
32
37
|
#finished;
|
|
38
|
+
/** Pending stdin writes, so concurrent calls reach the process in call order. */
|
|
39
|
+
#stdinQueue = Promise.resolve();
|
|
40
|
+
/** Set when a write failed after possibly writing part of it; later writes
|
|
41
|
+
* are refused so nothing is appended to a partial message. */
|
|
42
|
+
#stdinError = null;
|
|
43
|
+
/** Set when the command was started with a `Readable` for stdin. */
|
|
44
|
+
stdinPipe = null;
|
|
33
45
|
constructor(init) {
|
|
34
46
|
this.init = init;
|
|
35
47
|
}
|
|
@@ -93,10 +105,20 @@ export class Command {
|
|
|
93
105
|
[Symbol.dispose]: () => abort.abort(),
|
|
94
106
|
});
|
|
95
107
|
}
|
|
96
|
-
/** Waits for the command to end.
|
|
108
|
+
/** Waits for the command to end. With a `Readable` piped to its stdin,
|
|
109
|
+
* rejects if piping fails, and stops piping once the command exits. */
|
|
97
110
|
wait(params = {}) {
|
|
98
111
|
this.#finished ??= this.#follow(params);
|
|
99
|
-
|
|
112
|
+
const pipe = this.stdinPipe;
|
|
113
|
+
if (!pipe)
|
|
114
|
+
return this.#finished;
|
|
115
|
+
return waitWithStdinPipe(this.#finished, pipe, {
|
|
116
|
+
...(params.signal ? { signal: params.signal } : {}),
|
|
117
|
+
onPipeError: () => {
|
|
118
|
+
if (this.stdinPipe === pipe)
|
|
119
|
+
this.stdinPipe = null;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
100
122
|
}
|
|
101
123
|
async #follow(params) {
|
|
102
124
|
const lines = [];
|
|
@@ -105,8 +127,11 @@ export class Command {
|
|
|
105
127
|
for await (const event of this.init.process.output(params.signal ? { signal: params.signal } : {})) {
|
|
106
128
|
if (event.type === "truncated")
|
|
107
129
|
throw new StreamError("output_truncated", "Some command output is no longer available.", this.init.process.id);
|
|
108
|
-
if (event.type === "stdout" || event.type === "stderr")
|
|
109
|
-
|
|
130
|
+
if (event.type === "stdout" || event.type === "stderr") {
|
|
131
|
+
const line = { stream: event.type, data: event.data };
|
|
132
|
+
lines.push(line);
|
|
133
|
+
this.init.onLine?.(line);
|
|
134
|
+
}
|
|
110
135
|
else if (event.type === "exit")
|
|
111
136
|
exit = {
|
|
112
137
|
exitCode: event.exitCode,
|
|
@@ -120,7 +145,8 @@ export class Command {
|
|
|
120
145
|
this.exitCode = exitCodeOf(exit.exitCode, exit.timedOut);
|
|
121
146
|
if (exit.durationMs !== undefined)
|
|
122
147
|
this.durationMs = exit.durationMs;
|
|
123
|
-
|
|
148
|
+
const { onLine: _onLine, ...init } = this.init;
|
|
149
|
+
return new CommandFinished({ ...init, lines }, this.exitCode, exit.durationMs ?? undefined);
|
|
124
150
|
}
|
|
125
151
|
/** stdout, stderr, or both in the order they were written. For a detached
|
|
126
152
|
* command this waits for it to end. */
|
|
@@ -145,6 +171,203 @@ export class Command {
|
|
|
145
171
|
throw new RangeError(`Signal ${signal} has no name Runtime knows; use SIGTERM, SIGKILL, SIGINT, SIGHUP, SIGQUIT, SIGUSR1 or SIGUSR2.`);
|
|
146
172
|
await guard(() => this.init.process.kill(name, opts.abortSignal ? { signal: opts.abortSignal } : {}), this.init.sandboxName);
|
|
147
173
|
}
|
|
174
|
+
/** Writes to the stdin of a command started with `stdin: true` and
|
|
175
|
+
* `detached: true`. Writes reach the process in call order and resolve once
|
|
176
|
+
* it has accepted the data; Runtime's input offsets make every resend exact,
|
|
177
|
+
* so no byte is written twice. If a write fails, part of it may have been
|
|
178
|
+
* written, and later writes are refused; closeStdin still works. */
|
|
179
|
+
async writeStdin(data, opts = {}) {
|
|
180
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
181
|
+
if (bytes.length === 0)
|
|
182
|
+
return;
|
|
183
|
+
await this.#enqueueStdin(async (process) => {
|
|
184
|
+
if (this.#stdinError)
|
|
185
|
+
throw new Error("Stdin is in an unknown state after an earlier write failed. Close stdin or kill the command.", { cause: this.#stdinError });
|
|
186
|
+
opts.abortSignal?.throwIfAborted();
|
|
187
|
+
try {
|
|
188
|
+
await guard(() => process.write(bytes, opts.abortSignal ? { signal: opts.abortSignal } : {}), this.init.sandboxName);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const rejectedWhole = error instanceof APIError &&
|
|
192
|
+
error.response.status >= 400 &&
|
|
193
|
+
error.response.status < 500 &&
|
|
194
|
+
bytes.length <= STDIN_REQUEST_BYTES;
|
|
195
|
+
if (!rejectedWhole)
|
|
196
|
+
this.#stdinError = error;
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}, opts.abortSignal);
|
|
200
|
+
}
|
|
201
|
+
/** Closes the command's stdin, so it reads EOF once it has consumed what
|
|
202
|
+
* was written. */
|
|
203
|
+
async closeStdin(opts = {}) {
|
|
204
|
+
await this.#enqueueStdin((process) => guard(() => process.write(new Uint8Array(0), {
|
|
205
|
+
eof: true,
|
|
206
|
+
...(opts.abortSignal ? { signal: opts.abortSignal } : {}),
|
|
207
|
+
}), this.init.sandboxName), opts.abortSignal);
|
|
208
|
+
}
|
|
209
|
+
#enqueueStdin(work, signal) {
|
|
210
|
+
const previous = this.#stdinQueue;
|
|
211
|
+
const run = (async () => {
|
|
212
|
+
await waitForTurn(previous, signal);
|
|
213
|
+
return work(this.init.process);
|
|
214
|
+
})();
|
|
215
|
+
this.#stdinQueue = Promise.allSettled([previous, run]).then(() => { });
|
|
216
|
+
return run;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function waitForTurn(previous, signal) {
|
|
220
|
+
if (!signal)
|
|
221
|
+
return previous;
|
|
222
|
+
signal.throwIfAborted();
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const onAbort = () => reject(signal.reason);
|
|
225
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
226
|
+
void previous.then(() => {
|
|
227
|
+
signal.removeEventListener("abort", onAbort);
|
|
228
|
+
resolve();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/** The command exited or closed its input: piping ends quietly, as a closed
|
|
233
|
+
* pipe does in a shell. */
|
|
234
|
+
function inputNoLongerAccepted(error) {
|
|
235
|
+
return (error instanceof APIError &&
|
|
236
|
+
(error.response.status === 404 ||
|
|
237
|
+
(error.response.status === 409 && error.code === "stdin_closed")));
|
|
238
|
+
}
|
|
239
|
+
/** Writes a stream to a command's stdin in order, closing stdin when it
|
|
240
|
+
* ends. What is read while a write is in flight goes in the next write, and
|
|
241
|
+
* reading pauses while too much waits. The stream is never ended or
|
|
242
|
+
* destroyed, and every listener is removed once piping stops. */
|
|
243
|
+
export function pipeStdin(command, stream, signal) {
|
|
244
|
+
let stopped = false;
|
|
245
|
+
let writing = false;
|
|
246
|
+
let ended = false;
|
|
247
|
+
let waiting = [];
|
|
248
|
+
let waitingBytes = 0;
|
|
249
|
+
let resolveDone;
|
|
250
|
+
let rejectDone;
|
|
251
|
+
const done = new Promise((resolve, reject) => {
|
|
252
|
+
resolveDone = resolve;
|
|
253
|
+
rejectDone = reject;
|
|
254
|
+
});
|
|
255
|
+
done.catch(() => { });
|
|
256
|
+
const stop = () => {
|
|
257
|
+
if (stopped)
|
|
258
|
+
return false;
|
|
259
|
+
stopped = true;
|
|
260
|
+
stream.off("data", onData);
|
|
261
|
+
stream.off("end", onEnd);
|
|
262
|
+
stream.off("error", onError);
|
|
263
|
+
stream.off("close", onClose);
|
|
264
|
+
signal?.removeEventListener("abort", onAbort);
|
|
265
|
+
stream.pause();
|
|
266
|
+
return true;
|
|
267
|
+
};
|
|
268
|
+
const settle = (error) => {
|
|
269
|
+
if (!stop())
|
|
270
|
+
return;
|
|
271
|
+
if (error === undefined)
|
|
272
|
+
resolveDone();
|
|
273
|
+
else
|
|
274
|
+
rejectDone(error);
|
|
275
|
+
};
|
|
276
|
+
const failed = (error) => settle(inputNoLongerAccepted(error) ? undefined : error);
|
|
277
|
+
const flush = () => {
|
|
278
|
+
if (stopped || writing)
|
|
279
|
+
return;
|
|
280
|
+
if (waiting.length) {
|
|
281
|
+
writing = true;
|
|
282
|
+
const data = concat(waiting);
|
|
283
|
+
waiting = [];
|
|
284
|
+
waitingBytes = 0;
|
|
285
|
+
command.writeStdin(data).then(() => {
|
|
286
|
+
writing = false;
|
|
287
|
+
if (stopped)
|
|
288
|
+
return;
|
|
289
|
+
if (!ended)
|
|
290
|
+
stream.resume();
|
|
291
|
+
flush();
|
|
292
|
+
}, failed);
|
|
293
|
+
}
|
|
294
|
+
else if (ended) {
|
|
295
|
+
writing = true;
|
|
296
|
+
command.closeStdin().then(() => settle(), failed);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
function onData(chunk) {
|
|
300
|
+
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
|
|
301
|
+
waiting.push(bytes);
|
|
302
|
+
waitingBytes += bytes.length;
|
|
303
|
+
if (waitingBytes >= STDIN_BUFFERED_BYTES)
|
|
304
|
+
stream.pause();
|
|
305
|
+
flush();
|
|
306
|
+
}
|
|
307
|
+
function onEnd() {
|
|
308
|
+
ended = true;
|
|
309
|
+
flush();
|
|
310
|
+
}
|
|
311
|
+
function onError(error) {
|
|
312
|
+
if (!stop())
|
|
313
|
+
return;
|
|
314
|
+
void command
|
|
315
|
+
.closeStdin()
|
|
316
|
+
.catch(() => { })
|
|
317
|
+
.finally(() => rejectDone(error));
|
|
318
|
+
}
|
|
319
|
+
function onClose() {
|
|
320
|
+
if (!stream.readableEnded)
|
|
321
|
+
onError(new Error("The stdin stream closed before it ended"));
|
|
322
|
+
}
|
|
323
|
+
function onAbort() {
|
|
324
|
+
settle();
|
|
325
|
+
}
|
|
326
|
+
if (signal?.aborted)
|
|
327
|
+
settle();
|
|
328
|
+
else {
|
|
329
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
330
|
+
stream.on("error", onError);
|
|
331
|
+
stream.on("close", onClose);
|
|
332
|
+
if (stream.destroyed && !stream.readableEnded)
|
|
333
|
+
onClose();
|
|
334
|
+
else {
|
|
335
|
+
stream.on("end", onEnd);
|
|
336
|
+
stream.on("data", onData);
|
|
337
|
+
if (stream.readableEnded)
|
|
338
|
+
onEnd();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { done, stop: () => settle() };
|
|
342
|
+
}
|
|
343
|
+
function concat(chunks) {
|
|
344
|
+
if (chunks.length === 1)
|
|
345
|
+
return chunks[0];
|
|
346
|
+
const out = new Uint8Array(chunks.reduce((n, chunk) => n + chunk.length, 0));
|
|
347
|
+
let at = 0;
|
|
348
|
+
for (const chunk of chunks) {
|
|
349
|
+
out.set(chunk, at);
|
|
350
|
+
at += chunk.length;
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
/** Waits for a command to finish, rejecting early if piping its stdin fails,
|
|
355
|
+
* and stops piping once it has exited. Aborting `signal` stops waiting but
|
|
356
|
+
* leaves the pipe running. */
|
|
357
|
+
export async function waitWithStdinPipe(finished, pipe, opts = {}) {
|
|
358
|
+
if (!pipe)
|
|
359
|
+
return finished;
|
|
360
|
+
finished.then(() => pipe.stop(), () => {
|
|
361
|
+
if (!opts.signal?.aborted)
|
|
362
|
+
pipe.stop();
|
|
363
|
+
});
|
|
364
|
+
return Promise.race([
|
|
365
|
+
finished,
|
|
366
|
+
pipe.done.then(() => finished, (error) => {
|
|
367
|
+
opts.onPipeError?.();
|
|
368
|
+
throw error;
|
|
369
|
+
}),
|
|
370
|
+
]);
|
|
148
371
|
}
|
|
149
372
|
/** A command that has ended; `exitCode` is set. */
|
|
150
373
|
export class CommandFinished extends Command {
|
|
@@ -157,4 +380,12 @@ export class CommandFinished extends Command {
|
|
|
157
380
|
wait() {
|
|
158
381
|
return Promise.resolve(this);
|
|
159
382
|
}
|
|
383
|
+
/** Always throws: the command has already exited. */
|
|
384
|
+
async writeStdin(_data, _opts) {
|
|
385
|
+
throw new Error("Cannot write to stdin: the command has already finished.");
|
|
386
|
+
}
|
|
387
|
+
/** Always throws: the command has already exited. */
|
|
388
|
+
async closeStdin(_opts) {
|
|
389
|
+
throw new Error("Cannot close stdin: the command has already finished.");
|
|
390
|
+
}
|
|
160
391
|
}
|
|
@@ -76,7 +76,8 @@ export class SandboxUser {
|
|
|
76
76
|
return this.#sandbox.runCommand({
|
|
77
77
|
cmd: wrapped.cmd,
|
|
78
78
|
args: wrapped.args,
|
|
79
|
-
...(params.detached ? { detached: true } : {}),
|
|
79
|
+
...(params.detached ? { detached: true } : { onStart: params.onStart }),
|
|
80
|
+
...(params.stdin ? { stdin: params.stdin } : {}),
|
|
80
81
|
...(params.stdout ? { stdout: params.stdout } : {}),
|
|
81
82
|
...(params.stderr ? { stderr: params.stderr } : {}),
|
|
82
83
|
...(params.signal ? { signal: params.signal } : {}),
|
package/dist/vercel/sandbox.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Writable } from "node:stream";
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
2
|
import type { Runtime } from "../client.js";
|
|
3
3
|
import { type Credentials, type RuntimeCreate, type RuntimeSandbox, type WithRuntime } from "./client.js";
|
|
4
4
|
import { Command, CommandFinished } from "./command.js";
|
|
@@ -82,7 +82,7 @@ export interface CreateSandboxParams extends Credentials {
|
|
|
82
82
|
/** Runtime-only: an explicit client, or fields for the create. */
|
|
83
83
|
withruntime?: WithRuntime;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
interface RunCommandParamsBase {
|
|
86
86
|
cmd: string;
|
|
87
87
|
args?: string[];
|
|
88
88
|
cwd?: string;
|
|
@@ -90,11 +90,25 @@ export interface RunCommandParams {
|
|
|
90
90
|
/** Runs the command under `sudo`, which needs no password. */
|
|
91
91
|
sudo?: boolean;
|
|
92
92
|
detached?: boolean;
|
|
93
|
+
/** `true` keeps stdin open for `writeStdin` and `closeStdin` (needs
|
|
94
|
+
* `detached: true`). A `Readable` is written to stdin, which closes when it
|
|
95
|
+
* ends; the stream is never ended or destroyed. If piping fails, an
|
|
96
|
+
* attached command is killed and `runCommand` rejects; a detached one's
|
|
97
|
+
* `wait()` rejects. Unset or false: stdin is empty. */
|
|
98
|
+
stdin?: boolean | Readable;
|
|
93
99
|
stdout?: Writable;
|
|
94
100
|
stderr?: Writable;
|
|
95
101
|
signal?: AbortSignal;
|
|
96
102
|
timeoutMs?: number;
|
|
97
103
|
}
|
|
104
|
+
export type RunCommandParams = RunCommandParamsBase & ({
|
|
105
|
+
onStart?: never;
|
|
106
|
+
} | {
|
|
107
|
+
detached?: false;
|
|
108
|
+
/** Called with the Command as soon as it starts, before it exits;
|
|
109
|
+
* awaited before `runCommand` resolves. */
|
|
110
|
+
onStart?: (command: Command) => void | Promise<void>;
|
|
111
|
+
});
|
|
98
112
|
export interface SandboxRoute {
|
|
99
113
|
port: number;
|
|
100
114
|
subdomain: string;
|
package/dist/vercel/sandbox.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { extensionSeconds, LeaseKeeper, LEASE_MAX_SECONDS } from "../compat/lease.js";
|
|
2
2
|
import { RuntimeError } from "../errors.js";
|
|
3
3
|
import { clientFor, signalOf, } from "./client.js";
|
|
4
|
-
import { Command, CommandFinished, exitCodeOf } from "./command.js";
|
|
4
|
+
import { Command, CommandFinished, exitCodeOf, pipeStdin, waitWithStdinPipe, } from "./command.js";
|
|
5
5
|
import { APIError, guard, NotSupportedError, StreamError, translate } from "./errors.js";
|
|
6
6
|
import { FileSystem } from "./filesystem.js";
|
|
7
7
|
import { rootOrThrow, SandboxUser, SandboxUserAlreadyExistsError, validateName, } from "./sandbox-user.js";
|
|
@@ -479,6 +479,9 @@ export class Sandbox {
|
|
|
479
479
|
const params = typeof commandOrParams === "string"
|
|
480
480
|
? { cmd: commandOrParams, ...(args ? { args } : {}), ...opts }
|
|
481
481
|
: commandOrParams;
|
|
482
|
+
if (params.stdin === true && !params.detached)
|
|
483
|
+
throw new TypeError("`stdin: true` requires `detached: true`, otherwise the command would wait for input that can never be written");
|
|
484
|
+
const stdinStream = typeof params.stdin === "object" && params.stdin !== null ? params.stdin : null;
|
|
482
485
|
const argv = [
|
|
483
486
|
...(params.sudo ? ["sudo", "--preserve-env"] : []),
|
|
484
487
|
params.cmd,
|
|
@@ -497,8 +500,9 @@ export class Sandbox {
|
|
|
497
500
|
};
|
|
498
501
|
const startedAt = Date.now();
|
|
499
502
|
const vercelCwd = params.cwd === undefined ? HOME : toVercelPath(params.cwd);
|
|
503
|
+
const spawn = () => this.#withResume((runtime) => runtime.spawn(argv, { ...common, ...(params.stdin ? { stdin: "pipe" } : {}) }), params);
|
|
500
504
|
if (params.detached) {
|
|
501
|
-
const process = await
|
|
505
|
+
const process = await spawn();
|
|
502
506
|
const command = new Command({
|
|
503
507
|
id: process.id,
|
|
504
508
|
cwd: vercelCwd,
|
|
@@ -506,10 +510,38 @@ export class Sandbox {
|
|
|
506
510
|
sandboxName: this.name,
|
|
507
511
|
process,
|
|
508
512
|
});
|
|
513
|
+
if (stdinStream)
|
|
514
|
+
command.stdinPipe = pipeStdin(command, stdinStream, params.signal);
|
|
509
515
|
if (params.stdout || params.stderr)
|
|
510
516
|
void pipe(command, params);
|
|
511
517
|
return command;
|
|
512
518
|
}
|
|
519
|
+
if (stdinStream || params.onStart) {
|
|
520
|
+
// Started as a process so its Command exists before it exits: for
|
|
521
|
+
// onStart, and as the target of the piped stdin.
|
|
522
|
+
const process = await spawn();
|
|
523
|
+
const command = new Command({
|
|
524
|
+
id: process.id,
|
|
525
|
+
cwd: vercelCwd,
|
|
526
|
+
startedAt,
|
|
527
|
+
sandboxName: this.name,
|
|
528
|
+
process,
|
|
529
|
+
onLine: (line) => (line.stream === "stdout" ? params.stdout : params.stderr)?.write(line.data),
|
|
530
|
+
});
|
|
531
|
+
const stdinPipe = stdinStream && pipeStdinOrKill(command, stdinStream, params.signal);
|
|
532
|
+
const finished = waitWithStdinPipe(command.wait(params.signal ? { signal: params.signal } : {}), stdinPipe);
|
|
533
|
+
// The command can fail while onStart is pending or after it throws.
|
|
534
|
+
finished.catch(() => { });
|
|
535
|
+
try {
|
|
536
|
+
await params.onStart?.(command);
|
|
537
|
+
return await finished;
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
if (params.signal?.aborted)
|
|
541
|
+
await command.kill("SIGTERM").catch(() => { });
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
513
545
|
const lines = [];
|
|
514
546
|
const record = (stream, writer) => (data) => {
|
|
515
547
|
lines.push({ stream, data });
|
|
@@ -855,6 +887,17 @@ export function retentionDays(ms) {
|
|
|
855
887
|
return 365;
|
|
856
888
|
return Math.min(365, Math.max(1, Math.ceil(ms / 86_400_000)));
|
|
857
889
|
}
|
|
890
|
+
/** Pipes stdin for a command the caller waits on, killing the command if
|
|
891
|
+
* piping fails, since the caller has no handle to stop it otherwise. */
|
|
892
|
+
function pipeStdinOrKill(command, stream, signal) {
|
|
893
|
+
const stdinPipe = pipeStdin(command, stream, signal);
|
|
894
|
+
const done = stdinPipe.done.catch((error) => {
|
|
895
|
+
command.kill().catch(() => { });
|
|
896
|
+
throw error;
|
|
897
|
+
});
|
|
898
|
+
done.catch(() => { });
|
|
899
|
+
return { done, stop: () => stdinPipe.stop() };
|
|
900
|
+
}
|
|
858
901
|
async function pipe(command, params) {
|
|
859
902
|
try {
|
|
860
903
|
for await (const line of command.logs())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "withruntime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"mcpName": "com.withruntime/runtime",
|
|
6
6
|
"type": "module",
|
|
@@ -125,6 +125,11 @@
|
|
|
125
125
|
"import": "./dist/freestyle/index.js",
|
|
126
126
|
"default": "./dist/freestyle/index.js"
|
|
127
127
|
},
|
|
128
|
+
"./morph": {
|
|
129
|
+
"types": "./dist/morph/index.d.ts",
|
|
130
|
+
"import": "./dist/morph/index.js",
|
|
131
|
+
"default": "./dist/morph/index.js"
|
|
132
|
+
},
|
|
128
133
|
"./modal": {
|
|
129
134
|
"types": "./dist/modal/index.d.ts",
|
|
130
135
|
"import": "./dist/modal/index.js",
|