Skip to content
Source
Sign inSign up
Open History to see the last commit

shell.ts

64 lines2.3 KB

1import * as Console from "effect/Console";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4import * as Stream from "effect/Stream";
5import { ChildProcess, ChildProcessSpawner } from "effect/process";
6​
7export class ScenarioError extends Schema.TaggedError<ScenarioError>()("ScenarioError", {
8 message: Schema.String,
9}) {}
10​
11interface RunOptions {
12 readonly cwd?: string;
13 /** Replaces the inherited environment when given. */
14 readonly env?: NodeJS.ProcessEnv;
15 /** Exit codes that are answers, not failures. Defaults to 0 only. */
16 readonly allowExit?: ReadonlyArray<number>;
17 /** Prints the command's output to stderr as it arrives, keeping stdout for results. */
18 readonly echo?: boolean;
20​
21const collect = <E>(stream: Stream.Stream<Uint8Array, E>, echo: boolean) =>
22 echo
23 ? stream.pipe(
24 Stream.decodeText(),
25 Stream.splitLines,
26 Stream.tap((line) => Console.error(line)),
27 Stream.runCollect,
28 Effect.map((lines) => [...lines].join("\n")),
29 )
30 : Stream.mkString(Stream.decodeText(stream));
31​
32/** Runs a command to its end and returns what it printed. */
33export const run = Effect.fn("scenario.run")(
34 function* (program: string, args: ReadonlyArray<string>, options: RunOptions = {}) {
35 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
36 const child = yield* spawner.spawn(
37 ChildProcess.make(program, [...args], {
38 ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
39 ...(options.env === undefined ? {} : { env: options.env, extendEnv: false }),
40 stdin: "ignore",
41 stdout: "pipe",
42 stderr: "pipe",
43 }),
44 );
45 const [stdout, stderr, exitCode] = yield* Effect.all(
46 [
47 collect(child.stdout, options.echo === true),
48 collect(child.stderr, options.echo === true),
49 child.exitCode,
50 ],
51 { concurrency: "unbounded" },
52 );
53 if (!(options.allowExit ?? [0]).includes(exitCode)) {
54 return yield* new ScenarioError({
55 message: `${program} ${args.slice(0, 3).join(" ")} exited with ${exitCode}: ${stderr.trim().slice(-600)}`,
56 });
57 }
58 return { stdout, stderr, exitCode: Number(exitCode) };
59 },
60 Effect.scoped,
61 Effect.mapError((error) =>
62 error._tag === "ScenarioError" ? error : new ScenarioError({ message: String(error) }),
63 ),
64);