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
42export type ReplayWorkspaceFiles = Readonly<Record<string, string>>;
43
44const makeCheckpointWorkspaceEffect = Effect.fn("makeCheckpointWorkspace")(function* (
45 fixtureName: string,
46 files: ReplayWorkspaceFiles = {},
47) {
48 const fs = yield* FileSystem.FileSystem;
49 const path = yield* Path.Path;
50 const cwd = yield* fs.makeTempDirectory({
51 prefix: `t3-orchestrator-v2-${fixtureName}-`,
52 });
53 yield* runGit(cwd, ["init"]);
54 yield* runGit(cwd, ["config", "user.name", "T3 Code Test"]);
55 yield* runGit(cwd, ["config", "user.email", "t3code-test@example.com"]);
56 yield* fs.writeFileString(path.join(cwd, "README.md"), `# ${fixtureName}\n`);
57 for (const [relativePath, contents] of Object.entries(files)) {
58 const filePath = path.join(cwd, relativePath);
59 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
60 yield* fs.writeFileString(filePath, contents);
61 }
62 yield* runGit(cwd, ["add", "README.md", ...Object.keys(files)]);
63 yield* runGit(cwd, ["commit", "-m", "initial"]);
64 return cwd;
65});
66
67const removeCheckpointWorkspaceEffect = Effect.fn("removeCheckpointWorkspace")(function* (
68 cwd: string,
69) {
70 const fs = yield* FileSystem.FileSystem;
71 yield* fs.remove(cwd, { recursive: true });
72});
73
74export const checkpointWorkspace = (fixtureName: string, files?: ReplayWorkspaceFiles) =>
75 Effect.acquireRelease(makeCheckpointWorkspaceEffect(fixtureName, files), (cwd) =>
76 removeCheckpointWorkspaceEffect(cwd).pipe(Effect.orDie),
77 ).pipe(Effect.provide(NodeServices.layer));
78
79export async function makeCheckpointWorkspace(
80 fixtureName: string,
81 files?: ReplayWorkspaceFiles,
82): Promise<string> {
83 return await Effect.runPromise(
84 makeCheckpointWorkspaceEffect(fixtureName, files).pipe(Effect.provide(NodeServices.layer)),
85 );
86}