Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Update execa
Thomas Scholtes committed 1 year ago
commit ffd49e82ee77a981ebf711b1e1bc9967e59d9841
parent 04de7e7526ef69896a00515a40c7f106455a18b7
7 files changed +248 -150
modified package-lock.json
@@ -49,7 +49,7 @@
        "eslint-config-prettier": "^9.1.0",
        "eslint-plugin-no-only-tests": "^3.1.0",
        "eslint-plugin-svelte": "^2.35.1",
-
        "execa": "^8.0.1",
+
        "execa": "^9.0.0",
        "get-port": "^7.1.0",
        "happy-dom": "^14.3.8",
        "prettier": "^3.2.5",
@@ -908,6 +908,12 @@
        "win32"
      ]
    },
+
    "node_modules/@sec-ant/readable-stream": {
+
      "version": "0.4.1",
+
      "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
+
      "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+
      "dev": true
+
    },
    "node_modules/@sideway/address": {
      "version": "4.1.5",
      "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -935,6 +941,18 @@
      "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
      "dev": true
    },
+
    "node_modules/@sindresorhus/merge-streams": {
+
      "version": "4.0.0",
+
      "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+
      "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=18"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
    "node_modules/@sinonjs/commons": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
@@ -2430,23 +2448,26 @@
      }
    },
    "node_modules/execa": {
-
      "version": "8.0.1",
-
      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
-
      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+
      "version": "9.1.0",
+
      "resolved": "https://registry.npmjs.org/execa/-/execa-9.1.0.tgz",
+
      "integrity": "sha512-lSgHc4Elo2m6bUDhc3Hl/VxvUDJdQWI40RZ4KMY9bKRc+hgMOT7II/JjbNDhI8VnMtrCb7U/fhpJIkLORZozWw==",
      "dev": true,
      "dependencies": {
+
        "@sindresorhus/merge-streams": "^4.0.0",
        "cross-spawn": "^7.0.3",
-
        "get-stream": "^8.0.1",
-
        "human-signals": "^5.0.0",
-
        "is-stream": "^3.0.0",
-
        "merge-stream": "^2.0.0",
-
        "npm-run-path": "^5.1.0",
-
        "onetime": "^6.0.0",
+
        "figures": "^6.1.0",
+
        "get-stream": "^9.0.0",
+
        "human-signals": "^7.0.0",
+
        "is-plain-obj": "^4.1.0",
+
        "is-stream": "^4.0.1",
+
        "npm-run-path": "^5.2.0",
+
        "pretty-ms": "^9.0.0",
        "signal-exit": "^4.1.0",
-
        "strip-final-newline": "^3.0.0"
+
        "strip-final-newline": "^4.0.0",
+
        "yoctocolors": "^2.0.0"
      },
      "engines": {
-
        "node": ">=16.17"
+
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sindresorhus/execa?sponsor=1"
@@ -2518,6 +2539,21 @@
        "reusify": "^1.0.4"
      }
    },
+
    "node_modules/figures": {
+
      "version": "6.1.0",
+
      "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+
      "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
+
      "dev": true,
+
      "dependencies": {
+
        "is-unicode-supported": "^2.0.0"
+
      },
+
      "engines": {
+
        "node": ">=18"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
    "node_modules/file-entry-cache": {
      "version": "6.0.1",
      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -2675,12 +2711,16 @@
      }
    },
    "node_modules/get-stream": {
-
      "version": "8.0.1",
-
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
-
      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+
      "version": "9.0.1",
+
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+
      "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
      "dev": true,
+
      "dependencies": {
+
        "@sec-ant/readable-stream": "^0.4.1",
+
        "is-stream": "^4.0.1"
+
      },
      "engines": {
-
        "node": ">=16"
+
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
@@ -2957,12 +2997,12 @@
      }
    },
    "node_modules/human-signals": {
-
      "version": "5.0.0",
-
      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
-
      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+
      "version": "7.0.0",
+
      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-7.0.0.tgz",
+
      "integrity": "sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==",
      "dev": true,
      "engines": {
-
        "node": ">=16.17.0"
+
        "node": ">=18.18.0"
      }
    },
    "node_modules/ieee754": {
@@ -3107,6 +3147,18 @@
        "node": ">=8"
      }
    },
+
    "node_modules/is-plain-obj": {
+
      "version": "4.1.0",
+
      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+
      "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=12"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
    "node_modules/is-reference": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
@@ -3116,12 +3168,24 @@
      }
    },
    "node_modules/is-stream": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
-
      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+
      "version": "4.0.1",
+
      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+
      "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
      "dev": true,
      "engines": {
-
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+
        "node": ">=18"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
+
    "node_modules/is-unicode-supported": {
+
      "version": "2.0.0",
+
      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz",
+
      "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
@@ -3810,6 +3874,18 @@
        "node": ">=6"
      }
    },
+
    "node_modules/parse-ms": {
+
      "version": "4.0.0",
+
      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+
      "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=18"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
    "node_modules/parse5": {
      "version": "7.1.2",
      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -4115,6 +4191,21 @@
        "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
      }
    },
+
    "node_modules/pretty-ms": {
+
      "version": "9.0.0",
+
      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.0.0.tgz",
+
      "integrity": "sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==",
+
      "dev": true,
+
      "dependencies": {
+
        "parse-ms": "^4.0.0"
+
      },
+
      "engines": {
+
        "node": ">=18"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
    "node_modules/property-information": {
      "version": "6.4.1",
      "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz",
@@ -4494,12 +4585,12 @@
      }
    },
    "node_modules/strip-final-newline": {
-
      "version": "3.0.0",
-
      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
-
      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+
      "version": "4.0.0",
+
      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
+
      "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
      "dev": true,
      "engines": {
-
        "node": ">=12"
+
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
@@ -5153,6 +5244,74 @@
        }
      }
    },
+
    "node_modules/vitest/node_modules/execa": {
+
      "version": "8.0.1",
+
      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+
      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+
      "dev": true,
+
      "dependencies": {
+
        "cross-spawn": "^7.0.3",
+
        "get-stream": "^8.0.1",
+
        "human-signals": "^5.0.0",
+
        "is-stream": "^3.0.0",
+
        "merge-stream": "^2.0.0",
+
        "npm-run-path": "^5.1.0",
+
        "onetime": "^6.0.0",
+
        "signal-exit": "^4.1.0",
+
        "strip-final-newline": "^3.0.0"
+
      },
+
      "engines": {
+
        "node": ">=16.17"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+
      }
+
    },
+
    "node_modules/vitest/node_modules/get-stream": {
+
      "version": "8.0.1",
+
      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+
      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=16"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
+
    "node_modules/vitest/node_modules/human-signals": {
+
      "version": "5.0.0",
+
      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+
      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=16.17.0"
+
      }
+
    },
+
    "node_modules/vitest/node_modules/is-stream": {
+
      "version": "3.0.0",
+
      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+
      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+
      "dev": true,
+
      "engines": {
+
        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
+
    "node_modules/vitest/node_modules/strip-final-newline": {
+
      "version": "3.0.0",
+
      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+
      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=12"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
    "node_modules/vscode-oniguruma": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-2.0.1.tgz",
@@ -5273,6 +5432,18 @@
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
+
    "node_modules/yoctocolors": {
+
      "version": "2.0.2",
+
      "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.0.2.tgz",
+
      "integrity": "sha512-Ct97huExsu7cWeEjmrXlofevF8CvzUglJ4iGUet5B8xn1oumtAZBpHU4GzYuoE6PVqcZ5hghtBrSlhwHuR1Jmw==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=18"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/sindresorhus"
+
      }
+
    },
    "node_modules/zod": {
      "version": "3.22.4",
      "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
modified package.json
@@ -36,7 +36,7 @@
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-no-only-tests": "^3.1.0",
    "eslint-plugin-svelte": "^2.35.1",
-
    "execa": "^8.0.1",
+
    "execa": "^9.0.0",
    "get-port": "^7.1.0",
    "happy-dom": "^14.3.8",
    "prettier": "^3.2.5",
modified tests/support/fixtures.ts
@@ -6,8 +6,8 @@ import * as Path from "node:path";
import assert from "node:assert";
import { fileURLToPath } from "node:url";
import { test as base, expect } from "@playwright/test";
+
import { execa } from "execa";

-
import * as Process from "./process.js";
import * as issue from "@tests/support/cobs/issue.js";
import * as logLabel from "@tests/support/logPrefix.js";
import * as patch from "@tests/support/cobs/patch.js";
@@ -190,7 +190,7 @@ export async function createSourceBrowsingFixture(
  const projectName = "source-browsing";
  const sourceBrowsingDir = Path.join(tmpDir, "repos", projectName);
  await Fs.mkdir(sourceBrowsingDir, { recursive: true });
-
  await Process.spawn("tar", [
+
  await execa("tar", [
    "-xf",
    Path.join(fixturesDir, `repos/${projectName}.tar.bz2`),
    "-C",
@@ -568,7 +568,7 @@ export async function createCobsFixture(peer: RadiclePeer) {

export async function createMarkdownFixture(peer: RadiclePeer) {
  await Fs.mkdir(Path.join(tmpDir, "repos", "markdown"));
-
  await Process.spawn("tar", [
+
  await execa("tar", [
    "-xf",
    Path.join(fixturesDir, "repos", "markdown.tar.bz2"),
    "-C",
modified tests/support/globalSetup.ts
@@ -13,7 +13,6 @@ import {
  gitOptions,
} from "@tests/support/fixtures.js";
import { createPeerManager } from "@tests/support/peerManager.js";
-
import { killAllProcesses } from "@tests/support/process.js";

export default async function globalSetup(): Promise<() => void> {
  try {
@@ -78,6 +77,5 @@ export default async function globalSetup(): Promise<() => void> {

  return async () => {
    await peerManager.shutdown();
-
    killAllProcesses();
  };
}
modified tests/support/peerManager.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { BaseUrl } from "@httpd-client";
-
import type { ExecaChildProcess, Options as ExecaOptions } from "execa";
-

+
import type * as Execa from "execa";
+
import { execa } from "execa";
import * as Fs from "node:fs/promises";
import * as Os from "node:os";
import * as Path from "node:path";
@@ -10,10 +10,7 @@ import * as Util from "node:util";
import getPort from "get-port";
import matches from "lodash/matches.js";
import waitOn from "wait-on";
-
import { execa } from "execa";
import * as readline from "node:readline/promises";
-

-
import * as Process from "./process.js";
import { randomTag } from "@tests/support/support.js";
import { sleep } from "@app/lib/sleep.js";
import { array, literal, number, object, string, union, z } from "zod";
@@ -151,13 +148,20 @@ export const NodeConfigSchema = object({

export interface NodeConfig extends z.infer<typeof NodeConfigSchema> {}

-
// Options passed to `RadiclePeer#rad` and `RadiclePeer#git` that control how
-
// the process is spawned.
-
interface SpawnOptions extends ExecaOptions {
-
  // Sets the prefix for the commands output in the logs. If `null`, does not
-
  // log any output. Defaults to `<peer name> <binary name>`.
-
  logPrefix?: string | null;
-
}
+
// Specialize the return type of `execa()` to guarantee that `stdout` and
+
// `stderr` are strings.
+
type SpawnResult = Execa.ResultPromise<
+
  SpawnOptions & {
+
    stdout: (line: unknown) => AsyncGenerator<string, void, void>;
+
    stderr: (line: unknown) => AsyncGenerator<string, void, void>;
+
    encoding: "utf8";
+
  }
+
>;
+

+
type SpawnOptions = Omit<
+
  Execa.Options,
+
  "stdin" | "stdout" | "stderr" | "lines" | "encoding"
+
>;

export class RadiclePeer {
  public checkoutPath: string;
@@ -171,10 +175,10 @@ export class RadiclePeer {
  #gitOptions?: Record<string, string>;
  #listenSocketAddr?: string;
  #httpdBaseUrl?: BaseUrl;
-
  #nodeProcess?: ExecaChildProcess;
+
  #nodeProcess?: SpawnResult;
  // Name for easy identification. Used on file system and in logs.
  #name: string;
-
  #childProcesses: ExecaChildProcess[] = [];
+
  #childProcesses: SpawnResult[] = [];

  private constructor(props: {
    checkoutPath: string;
@@ -296,7 +300,6 @@ export class RadiclePeer {

    const { stdout } = this.rad(["node", "events"], {
      cwd: this.#radHome,
-
      logPrefix: null,
    });

    if (!stdout) {
@@ -327,6 +330,8 @@ export class RadiclePeer {
  }

  public async stopNode() {
+
    // Don’t leak unhandled rejections when forcefully killing the process
+
    this.#nodeProcess?.catch(() => {});
    this.#nodeProcess?.kill("SIGTERM");

    await waitOn({
@@ -343,7 +348,11 @@ export class RadiclePeer {
  public async shutdown() {
    // We don’t care about proper cleanup. We just want to make sure that no
    // processes are running anymore.
-
    this.#childProcesses.forEach(p => p.kill("SIGKILL"));
+
    this.#childProcesses.forEach(p => {
+
      // Don’t leak unhandled rejections when forcefully killing the process
+
      p.catch(() => {});
+
      p.kill("SIGKILL");
+
    });
  }

  public get address(): string {
@@ -373,11 +382,11 @@ export class RadiclePeer {
    return this.#httpdBaseUrl;
  }

-
  public git(args: string[] = [], opts?: SpawnOptions): ExecaChildProcess {
+
  public git(args: string[] = [], opts?: SpawnOptions): SpawnResult {
    return this.spawn("git", args, { ...opts });
  }

-
  public rad(args: string[] = [], opts?: SpawnOptions): ExecaChildProcess {
+
  public rad(args: string[] = [], opts?: SpawnOptions): SpawnResult {
    return this.spawn("rad", args, { ...opts });
  }

@@ -385,8 +394,18 @@ export class RadiclePeer {
    cmd: string,
    args: string[] = [],
    opts?: SpawnOptions,
-
  ): ExecaChildProcess {
-
    opts = {
+
  ): SpawnResult {
+
    const prefix = logPrefix(`${this.#name} ${cmd}`);
+
    const outputLog = this.#outputLog;
+

+
    function* logWithPrefix(line: unknown) {
+
      if (typeof line === "string") {
+
        outputLog.write(`${prefix} ${line}\n`, "utf8");
+
      }
+
      yield line;
+
    }
+

+
    const childProcess = execa(cmd, args, {
      ...opts,
      env: {
        GIT_CONFIG_GLOBAL: "/dev/null",
@@ -399,17 +418,12 @@ export class RadiclePeer {
        ...opts?.env,
        ...this.#gitOptions,
      },
-
    };
-
    const childProcess = Process.spawn(cmd, args, opts);
-
    this.#childProcesses.push(childProcess);
+
      encoding: "utf8",
+
      stdout: logWithPrefix,
+
      stderr: logWithPrefix,
+
    });

-
    if (opts.logPrefix !== null) {
-
      void Process.prefixOutput(
-
        childProcess,
-
        opts.logPrefix || `${this.#name} ${cmd}`,
-
        this.#outputLog,
-
      );
-
    }
+
    this.#childProcesses.push(childProcess);

    return childProcess;
  }
deleted tests/support/process.ts
@@ -1,84 +0,0 @@
-
import type { ExecaChildProcess, Options } from "execa";
-

-
import * as Stream from "node:stream";
-
import { StringDecoder } from "string_decoder";
-
import { execa } from "execa";
-

-
import { logPrefix } from "./logPrefix.js";
-

-
// Processes that should be SIGKILLed when the Node process shutsdown.
-
// We add all proxy and node instances that we spawn to this list.
-
const processes: ExecaChildProcess[] = [];
-

-
process.on("exit", killAllProcesses);
-
process.on("SIGINT", killAllProcesses);
-
process.on("SIGTERM", killAllProcesses);
-

-
// Kill all processes with SIGKILL
-
export function killAllProcesses(): void {
-
  for (const process of processes) {
-
    if (process.exitCode === null) {
-
      process.kill("SIGKILL");
-
    }
-
  }
-
}
-

-
// Spawn a process with `execa` and register it.
-
//
-
// The process will be killed by `killAllProcesses`.
-
export function spawn(
-
  bin: string,
-
  args: string[],
-
  options?: Options,
-
): ExecaChildProcess {
-
  const child = execa(bin, args, options);
-
  processes.push(child);
-
  return child;
-
}
-

-
// Forwards piped `stdout` and `stderr` of a child process to `output`
-
// and prefixes it with the given label. The prefix is colored.
-
export function prefixOutput(
-
  childProcess: ExecaChildProcess,
-
  label: string,
-
  output: Stream.Writable,
-
): ExecaChildProcess {
-
  const pref = logPrefix(label);
-
  if (childProcess.stdout) {
-
    const stdoutPrefix = new LinePrefix(pref);
-
    childProcess.stdout.pipe(stdoutPrefix).pipe(output, { end: false });
-
  }
-
  if (childProcess.stderr) {
-
    const stderrPrefix = new LinePrefix(pref);
-
    childProcess.stderr.pipe(stderrPrefix).pipe(output, { end: false });
-
  }
-

-
  return childProcess;
-
}
-

-
// A transform that prefixes each line from the source with the given
-
// string and pushes it to the sink.
-
class LinePrefix extends Stream.Transform {
-
  private buffer: string = "";
-
  private stringDecoder = new StringDecoder();
-

-
  public constructor(private prefix: string) {
-
    super();
-
  }
-

-
  public _transform(data: Buffer, _encoding: string, next: () => void): void {
-
    const str = this.buffer + this.stringDecoder.write(data);
-
    const lines = str.split(/\r?\n/);
-
    this.buffer = lines.pop() || "";
-
    lines.forEach(line => this.push(`${this.prefix}${line}\n`));
-
    next();
-
  }
-

-
  public _flush(done: () => void): void {
-
    const rest = `${this.buffer}${this.stringDecoder.end()}`;
-
    if (rest) {
-
      this.push(`${this.prefix}${rest}\n`);
-
    }
-
    done();
-
  }
-
}
modified tests/support/project.ts
@@ -1,6 +1,5 @@
import type { Locator, Page } from "@playwright/test";
import type { RadiclePeer } from "@tests/support/peerManager";
-
import type { ExecaReturnValue } from "execa";

import * as Path from "node:path";
import { expect } from "@playwright/test";
@@ -52,7 +51,7 @@ export async function createProject(
  return { rid, projectFolder, defaultBranch };
}

-
export function extractPatchId(cmdOutput: ExecaReturnValue<string>) {
+
export function extractPatchId(cmdOutput: { stderr: string }) {
  const match = cmdOutput.stderr.match(/[0-9a-f]{40}/);
  if (match) {
    return match[0];