Julius Marmingerefactor git manager52681638 months agoHistory
178 lines · 4.6 KB · TypeScript
RawDownload
import { Effect, Layer } from "effect"; import { runProcess } from "../../processRunner";import { GitHubCliError } from "../Errors.ts";import { GitHubCli, type GitHubCliShape } from "../Services/GitHubCli.ts"; const DEFAULT_TIMEOUT_MS = 30_000; function normalizeGitHubCliError(operation: "execute" | "stdout", error: unknown): GitHubCliError {  if (error instanceof Error) {    if (error.message.includes("Command not found: gh")) {      return new GitHubCliError({        operation,        detail: "GitHub CLI (`gh`) is required but not available on PATH.",        cause: error,      });    }     const lower = error.message.toLowerCase();    if (      lower.includes("authentication failed") ||      lower.includes("not logged in") ||      lower.includes("gh auth login") ||      lower.includes("no oauth token")    ) {      return new GitHubCliError({        operation,        detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.",        cause: error,      });    }     return new GitHubCliError({      operation,      detail: `GitHub CLI command failed: ${error.message}`,      cause: error,    });  }   return new GitHubCliError({    operation,    detail: "GitHub CLI command failed.",    cause: error,  });} function parseOpenPullRequests(raw: string): ReadonlyArray<{  number: number;  title: string;  url: string;  baseRefName: string;  headRefName: string;}> {  const trimmed = raw.trim();  if (trimmed.length === 0) return [];   const parsed: unknown = JSON.parse(trimmed);  if (!Array.isArray(parsed)) {    throw new Error("GitHub CLI returned non-array JSON.");  }   const result: Array<{    number: number;    title: string;    url: string;    baseRefName: string;    headRefName: string;  }> = [];  for (const entry of parsed) {    if (!entry || typeof entry !== "object") {      continue;    }    const record = entry as Record<string, unknown>;    const number = record.number;    const title = record.title;    const url = record.url;    const baseRefName = record.baseRefName;    const headRefName = record.headRefName;    if (      typeof number !== "number" ||      !Number.isInteger(number) ||      number <= 0 ||      typeof title !== "string" ||      typeof url !== "string" ||      typeof baseRefName !== "string" ||      typeof headRefName !== "string"    ) {      continue;    }    result.push({      number,      title,      url,      baseRefName,      headRefName,    });  }   return result;} const makeGitHubCli = Effect.sync(() => {  const execute: GitHubCliShape["execute"] = (input) =>    Effect.tryPromise({      try: () =>        runProcess("gh", input.args, {          cwd: input.cwd,          timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,        }),      catch: (error) => normalizeGitHubCliError("execute", error),    });   const service = {    execute,    listOpenPullRequests: (input) =>      execute({        cwd: input.cwd,        args: [          "pr",          "list",          "--head",          input.headBranch,          "--state",          "open",          "--limit",          String(input.limit ?? 1),          "--json",          "number,title,url,baseRefName,headRefName",        ],      }).pipe(        Effect.map((result) => result.stdout),        Effect.flatMap((raw) =>          Effect.try({            try: () => parseOpenPullRequests(raw),            catch: (error: unknown) =>              new GitHubCliError({                operation: "listOpenPullRequests",                detail:                  error instanceof Error                    ? `GitHub CLI returned invalid PR list JSON: ${error.message}`                    : "GitHub CLI returned invalid PR list JSON.",                ...(error !== undefined ? { cause: error } : {}),              }),          }),        ),      ),    createPullRequest: (input) =>      execute({        cwd: input.cwd,        args: [          "pr",          "create",          "--base",          input.baseBranch,          "--head",          input.headBranch,          "--title",          input.title,          "--body-file",          input.bodyFile,        ],      }).pipe(Effect.asVoid),    getDefaultBranch: (input) =>      execute({        cwd: input.cwd,        args: ["repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"],      }).pipe(        Effect.map((value) => {          const trimmed = value.stdout.trim();          return trimmed.length > 0 ? trimmed : null;        }),      ),  } satisfies GitHubCliShape;   return service;}); export const GitHubCliLive = Layer.effect(GitHubCli, makeGitHubCli);