1import * as NodeServices from "@effect/platform-node/NodeServices";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";
5import * as PlatformError from "effect/PlatformError";
6import * as Schema from "effect/Schema";
7import * as ChildProcess from "effect/process/ChildProcess";
8import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
9
10class ReplayFixtureGitCommandError extends Schema.TaggedError<ReplayFixtureGitCommandError>()(
11 "ReplayFixtureGitCommandError",
12 {
13 command: Schema.String,
14 exitCode: Schema.Number,
15 },
16) {
17 override get message(): string {
18 return `${this.command} failed with exit ${this.exitCode}.`;
19 }
20}
21
22function runGit(
23 cwd: string,
24 args: ReadonlyArray<string>,
25): Effect.Effect<
26 void,
27 ReplayFixtureGitCommandError | PlatformError.PlatformError,
28 ChildProcessSpawner.ChildProcessSpawner
29> {
30 return Effect.gen(function* () {
31 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
32 const exitCode = yield* spawner.exitCode(ChildProcess.make("git", args, { cwd }));
33 if (Number(exitCode) !== 0) {
34 return yield* new ReplayFixtureGitCommandError({
35 command: `git ${args.join(" ")}`,
36 exitCode: Number(exitCode),
37 });
38 }
39 });
40}
41
42/** Workspace-relative path to file contents, committed with the initial README. */
43export type ReplayWorkspaceFiles = Readonly<Record<string, string>>;
44
45const makeCheckpointWorkspaceEffect = Effect.fn("makeCheckpointWorkspace")(function* (
46 fixtureName: string,
47 files: ReplayWorkspaceFiles = {},
48) {
49 const fs = yield* FileSystem.FileSystem;
50 const path = yield* Path.Path;
51 const cwd = yield* fs.makeTempDirectory({
52 prefix: `t3-orchestrator-v2-${fixtureName}-`,
53 });
54 yield* runGit(cwd, ["init"]);
55 yield* runGit(cwd, ["config", "user.name", "T3 Code Test"]);
56 yield* runGit(cwd, ["config", "user.email", "t3code-test@example.com"]);
57 yield* fs.writeFileString(path.join(cwd, "README.md"), `# ${fixtureName}\n`);
58 for (const [relativePath, contents] of Object.entries(files)) {
59 const filePath = path.join(cwd, relativePath);
60 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
61 yield* fs.writeFileString(filePath, contents);
62 }
63 yield* runGit(cwd, ["add", "README.md", ...Object.keys(files)]);
64 yield* runGit(cwd, ["commit", "-m", "initial"]);
65 return cwd;
66});
67
68const removeCheckpointWorkspaceEffect = Effect.fn("removeCheckpointWorkspace")(function* (
69 cwd: string,
70) {
71 const fs = yield* FileSystem.FileSystem;
72 yield* fs.remove(cwd, { recursive: true });
73});
74
75export const checkpointWorkspace = (fixtureName: string, files?: ReplayWorkspaceFiles) =>
76 Effect.acquireRelease(makeCheckpointWorkspaceEffect(fixtureName, files), (cwd) =>
77 removeCheckpointWorkspaceEffect(cwd).pipe(Effect.orDie),
78 ).pipe(Effect.provide(NodeServices.layer));
79
80export async function makeCheckpointWorkspace(
81 fixtureName: string,
82 files?: ReplayWorkspaceFiles,
83): Promise<string> {
84 return await Effect.runPromise(
85 makeCheckpointWorkspaceEffect(fixtureName, files).pipe(Effect.provide(NodeServices.layer)),
86 );
87}