- Move GitManager and GitHubCli contracts/layers to their new modules - Update server wiring and tests for the relocated pluggable git setup
Showing the changes to apps/server/src/sourceControl (5 of 12 files). Show all
Unified · Split
| @@ -0,0 +1,226 @@ | |||
| 1 | import { assert, it } from "@effect/vitest"; | ||
| 2 | import { Effect } from "effect"; | ||
| 3 | import { afterEach, expect, vi } from "vitest"; | ||
| 4 | |||
| 5 | vi.mock("../processRunner", () => ({ | ||
| 6 | runProcess: vi.fn(), | ||
| 7 | })); | ||
| 8 | |||
| 9 | import { runProcess } from "../processRunner.ts"; | ||
| 10 | import * as GitHubCli from "./GitHubCli.ts"; | ||
| 11 | |||
| 12 | const mockedRunProcess = vi.mocked(runProcess); | ||
| 13 | const layer = it.layer(GitHubCli.layer); | ||
| 14 | |||
| 15 | afterEach(() => { | ||
| 16 | mockedRunProcess.mockReset(); | ||
| 17 | }); | ||
| 18 | |||
| 19 | layer("GitHubCli.layer", (it) => { | ||
| 20 | it.effect("parses pull request view output", () => | ||
| 21 | Effect.gen(function* () { | ||
| 22 | mockedRunProcess.mockResolvedValueOnce({ | ||
| 23 | stdout: JSON.stringify({ | ||
| 24 | number: 42, | ||
| 25 | title: "Add PR thread creation", | ||
| 26 | url: "https://github.com/pingdotgg/codething-mvp/pull/42", | ||
| 27 | baseRefName: "main", | ||
| 28 | headRefName: "feature/pr-threads", | ||
| 29 | state: "OPEN", | ||
| 30 | mergedAt: null, | ||
| 31 | isCrossRepository: true, | ||
| 32 | headRepository: { | ||
| 33 | nameWithOwner: "octocat/codething-mvp", | ||
| 34 | }, | ||
| 35 | headRepositoryOwner: { | ||
| 36 | login: "octocat", | ||
| 37 | }, | ||
| 38 | }), | ||
| 39 | stderr: "", | ||
| 40 | code: 0, | ||
| 41 | signal: null, | ||
| 42 | timedOut: false, | ||
| 43 | }); | ||
| 44 | |||
| 45 | const result = yield* Effect.gen(function* () { | ||
| 46 | const gh = yield* GitHubCli.GitHubCli; | ||
| 47 | return yield* gh.getPullRequest({ | ||
| 48 | cwd: "/repo", | ||
| 49 | reference: "#42", | ||
| 50 | }); | ||
| 51 | }); | ||
| 52 | |||
| 53 | assert.deepStrictEqual(result, { | ||
| 54 | number: 42, | ||
| 55 | title: "Add PR thread creation", | ||
| 56 | url: "https://github.com/pingdotgg/codething-mvp/pull/42", | ||
| 57 | baseRefName: "main", | ||
| 58 | headRefName: "feature/pr-threads", | ||
| 59 | state: "open", | ||
| 60 | isCrossRepository: true, | ||
| 61 | headRepositoryNameWithOwner: "octocat/codething-mvp", | ||
| 62 | headRepositoryOwnerLogin: "octocat", | ||
| 63 | }); | ||
| 64 | expect(mockedRunProcess).toHaveBeenCalledWith( | ||
| 65 | "gh", | ||
| 66 | [ | ||
| 67 | "pr", | ||
| 68 | "view", | ||
| 69 | "#42", | ||
| 70 | "--json", | ||
| 71 | "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner", | ||
| 72 | ], | ||
| 73 | expect.objectContaining({ cwd: "/repo" }), | ||
| 74 | ); | ||
| 75 | }), | ||
| 76 | ); | ||
| 77 | |||
| 78 | it.effect("trims pull request fields decoded from gh json", () => | ||
| 79 | Effect.gen(function* () { | ||
| 80 | mockedRunProcess.mockResolvedValueOnce({ | ||
| 81 | stdout: JSON.stringify({ | ||
| 82 | number: 42, | ||
| 83 | title: " Add PR thread creation \n", | ||
| 84 | url: " https://github.com/pingdotgg/codething-mvp/pull/42 ", | ||
| 85 | baseRefName: " main ", | ||
| 86 | headRefName: "\tfeature/pr-threads\t", | ||
| 87 | state: "OPEN", | ||
| 88 | mergedAt: null, | ||
| 89 | isCrossRepository: true, | ||
| 90 | headRepository: { | ||
| 91 | nameWithOwner: " octocat/codething-mvp ", | ||
| 92 | }, | ||
| 93 | headRepositoryOwner: { | ||
| 94 | login: " octocat ", | ||
| 95 | }, | ||
| 96 | }), | ||
| 97 | stderr: "", | ||
| 98 | code: 0, | ||
| 99 | signal: null, | ||
| 100 | timedOut: false, | ||
| 101 | }); | ||
| 102 | |||
| 103 | const result = yield* Effect.gen(function* () { | ||
| 104 | const gh = yield* GitHubCli.GitHubCli; | ||
| 105 | return yield* gh.getPullRequest({ | ||
| 106 | cwd: "/repo", | ||
| 107 | reference: "#42", | ||
| 108 | }); | ||
| 109 | }); | ||
| 110 | |||
| 111 | assert.deepStrictEqual(result, { | ||
| 112 | number: 42, | ||
| 113 | title: "Add PR thread creation", | ||
| 114 | url: "https://github.com/pingdotgg/codething-mvp/pull/42", | ||
| 115 | baseRefName: "main", | ||
| 116 | headRefName: "feature/pr-threads", | ||
| 117 | state: "open", | ||
| 118 | isCrossRepository: true, | ||
| 119 | headRepositoryNameWithOwner: "octocat/codething-mvp", | ||
| 120 | headRepositoryOwnerLogin: "octocat", | ||
| 121 | }); | ||
| 122 | }), | ||
| 123 | ); | ||
| 124 | |||
| 125 | it.effect("skips invalid entries when parsing pr lists", () => | ||
| 126 | Effect.gen(function* () { | ||
| 127 | mockedRunProcess.mockResolvedValueOnce({ | ||
| 128 | stdout: JSON.stringify([ | ||
| 129 | { | ||
| 130 | number: 0, | ||
| 131 | title: "invalid", | ||
| 132 | url: "https://github.com/pingdotgg/codething-mvp/pull/0", | ||
| 133 | baseRefName: "main", | ||
| 134 | headRefName: "feature/invalid", | ||
| 135 | }, | ||
| 136 | { | ||
| 137 | number: 43, | ||
| 138 | title: " Valid PR ", | ||
| 139 | url: " https://github.com/pingdotgg/codething-mvp/pull/43 ", | ||
| 140 | baseRefName: " main ", | ||
| 141 | headRefName: " feature/pr-list ", | ||
| 142 | headRepository: { | ||
| 143 | nameWithOwner: " ", | ||
| 144 | }, | ||
| 145 | headRepositoryOwner: { | ||
| 146 | login: " ", | ||
| 147 | }, | ||
| 148 | }, | ||
| 149 | ]), | ||
| 150 | stderr: "", | ||
| 151 | code: 0, | ||
| 152 | signal: null, | ||
| 153 | timedOut: false, | ||
| 154 | }); | ||
| 155 | |||
| 156 | const result = yield* Effect.gen(function* () { | ||
| 157 | const gh = yield* GitHubCli.GitHubCli; | ||
| 158 | return yield* gh.listOpenPullRequests({ | ||
| 159 | cwd: "/repo", | ||
| 160 | headSelector: "feature/pr-list", | ||
| 161 | }); | ||
| 162 | }); | ||
| 163 | |||
| 164 | assert.deepStrictEqual(result, [ | ||
| 165 | { | ||
| 166 | number: 43, | ||
| 167 | title: "Valid PR", | ||
| 168 | url: "https://github.com/pingdotgg/codething-mvp/pull/43", | ||
| 169 | baseRefName: "main", | ||
| 170 | headRefName: "feature/pr-list", | ||
| 171 | state: "open", | ||
| 172 | }, | ||
| 173 | ]); | ||
| 174 | }), | ||
| 175 | ); | ||
| 176 | |||
| 177 | it.effect("reads repository clone URLs", () => | ||
| 178 | Effect.gen(function* () { | ||
| 179 | mockedRunProcess.mockResolvedValueOnce({ | ||
| 180 | stdout: JSON.stringify({ | ||
| 181 | nameWithOwner: "octocat/codething-mvp", | ||
| 182 | url: "https://github.com/octocat/codething-mvp", | ||
| 183 | sshUrl: "git@github.com:octocat/codething-mvp.git", | ||
| 184 | }), | ||
| 185 | stderr: "", | ||
| 186 | code: 0, | ||
| 187 | signal: null, | ||
| 188 | timedOut: false, | ||
| 189 | }); | ||
| 190 | |||
| 191 | const result = yield* Effect.gen(function* () { | ||
| 192 | const gh = yield* GitHubCli.GitHubCli; | ||
| 193 | return yield* gh.getRepositoryCloneUrls({ | ||
| 194 | cwd: "/repo", | ||
| 195 | repository: "octocat/codething-mvp", | ||
| 196 | }); | ||
| 197 | }); | ||
| 198 | |||
| 199 | assert.deepStrictEqual(result, { | ||
| 200 | nameWithOwner: "octocat/codething-mvp", | ||
| 201 | url: "https://github.com/octocat/codething-mvp", | ||
| 202 | sshUrl: "git@github.com:octocat/codething-mvp.git", | ||
| 203 | }); | ||
| 204 | }), | ||
| 205 | ); | ||
| 206 | |||
| 207 | it.effect("surfaces a friendly error when the pull request is not found", () => | ||
| 208 | Effect.gen(function* () { | ||
| 209 | mockedRunProcess.mockRejectedValueOnce( | ||
| 210 | new Error( | ||
| 211 | "GraphQL: Could not resolve to a PullRequest with the number of 4888. (repository.pullRequest)", | ||
| 212 | ), | ||
| 213 | ); | ||
| 214 | |||
| 215 | const error = yield* Effect.gen(function* () { | ||
| 216 | const gh = yield* GitHubCli.GitHubCli; | ||
| 217 | return yield* gh.getPullRequest({ | ||
| 218 | cwd: "/repo", | ||
| 219 | reference: "4888", | ||
| 220 | }); | ||
| 221 | }).pipe(Effect.flip); | ||
| 222 | |||
| 223 | assert.equal(error.message.includes("Pull request not found"), true); | ||
| 224 | }), | ||
| 225 | ); | ||
| 226 | }); | ||
| @@ -0,0 +1,298 @@ | |||
| 1 | import { Context, Effect, Layer, Result, Schema, SchemaIssue } from "effect"; | ||
| 2 | |||
| 3 | import { GitHubCliError, TrimmedNonEmptyString } from "@t3tools/contracts"; | ||
| 4 | |||
| 5 | import type { ProcessRunResult } from "../processRunner.ts"; | ||
| 6 | import { runProcess } from "../processRunner.ts"; | ||
| 7 | import { | ||
| 8 | decodeGitHubPullRequestJson, | ||
| 9 | decodeGitHubPullRequestListJson, | ||
| 10 | formatGitHubJsonDecodeError, | ||
| 11 | } from "../git/githubPullRequests.ts"; | ||
| 12 | |||
| 13 | const DEFAULT_TIMEOUT_MS = 30_000; | ||
| 14 | |||
| 15 | export interface GitHubPullRequestSummary { | ||
| 16 | readonly number: number; | ||
| 17 | readonly title: string; | ||
| 18 | readonly url: string; | ||
| 19 | readonly baseRefName: string; | ||
| 20 | readonly headRefName: string; | ||
| 21 | readonly state?: "open" | "closed" | "merged"; | ||
| 22 | readonly isCrossRepository?: boolean; | ||
| 23 | readonly headRepositoryNameWithOwner?: string | null; | ||
| 24 | readonly headRepositoryOwnerLogin?: string | null; | ||
| 25 | } | ||
| 26 | |||
| 27 | export interface GitHubRepositoryCloneUrls { | ||
| 28 | readonly nameWithOwner: string; | ||
| 29 | readonly url: string; | ||
| 30 | readonly sshUrl: string; | ||
| 31 | } | ||
| 32 | |||
| 33 | export interface GitHubCliShape { | ||
| 34 | readonly execute: (input: { | ||
| 35 | readonly cwd: string; | ||
| 36 | readonly args: ReadonlyArray<string>; | ||
| 37 | readonly timeoutMs?: number; | ||
| 38 | }) => Effect.Effect<ProcessRunResult, GitHubCliError>; | ||
| 39 | |||
| 40 | readonly listOpenPullRequests: (input: { | ||
| 41 | readonly cwd: string; | ||
| 42 | readonly headSelector: string; | ||
| 43 | readonly limit?: number; | ||
| 44 | }) => Effect.Effect<ReadonlyArray<GitHubPullRequestSummary>, GitHubCliError>; | ||
| 45 | |||
| 46 | readonly getPullRequest: (input: { | ||
| 47 | readonly cwd: string; | ||
| 48 | readonly reference: string; | ||
| 49 | }) => Effect.Effect<GitHubPullRequestSummary, GitHubCliError>; | ||
| 50 | |||
| 51 | readonly getRepositoryCloneUrls: (input: { | ||
| 52 | readonly cwd: string; | ||
| 53 | readonly repository: string; | ||
| 54 | }) => Effect.Effect<GitHubRepositoryCloneUrls, GitHubCliError>; | ||
| 55 | |||
| 56 | readonly createPullRequest: (input: { | ||
| 57 | readonly cwd: string; | ||
| 58 | readonly baseBranch: string; | ||
| 59 | readonly headSelector: string; | ||
| 60 | readonly title: string; | ||
| 61 | readonly bodyFile: string; | ||
| 62 | }) => Effect.Effect<void, GitHubCliError>; | ||
| 63 | |||
| 64 | readonly getDefaultBranch: (input: { | ||
| 65 | readonly cwd: string; | ||
| 66 | }) => Effect.Effect<string | null, GitHubCliError>; | ||
| 67 | |||
| 68 | readonly checkoutPullRequest: (input: { | ||
| 69 | readonly cwd: string; | ||
| 70 | readonly reference: string; | ||
| 71 | readonly force?: boolean; | ||
| 72 | }) => Effect.Effect<void, GitHubCliError>; | ||
| 73 | } | ||
| 74 | |||
| 75 | export class GitHubCli extends Context.Service<GitHubCli, GitHubCliShape>()( | ||
| 76 | "t3/source-control/GitHubCli", | ||
| 77 | ) {} | ||
| 78 | |||
| 79 | function normalizeGitHubCliError(operation: "execute" | "stdout", error: unknown): GitHubCliError { | ||
| 80 | if (error instanceof Error) { | ||
| 81 | if (error.message.includes("Command not found: gh")) { | ||
| 82 | return new GitHubCliError({ | ||
| 83 | operation, | ||
| 84 | detail: "GitHub CLI (`gh`) is required but not available on PATH.", | ||
| 85 | cause: error, | ||
| 86 | }); | ||
| 87 | } | ||
| 88 | |||
| 89 | const lower = error.message.toLowerCase(); | ||
| 90 | if ( | ||
| 91 | lower.includes("authentication failed") || | ||
| 92 | lower.includes("not logged in") || | ||
| 93 | lower.includes("gh auth login") || | ||
| 94 | lower.includes("no oauth token") | ||
| 95 | ) { | ||
| 96 | return new GitHubCliError({ | ||
| 97 | operation, | ||
| 98 | detail: "GitHub CLI is not authenticated. Run `gh auth login` and retry.", | ||
| 99 | cause: error, | ||
| 100 | }); | ||
| 101 | } | ||
| 102 | |||
| 103 | if ( | ||
| 104 | lower.includes("could not resolve to a pullrequest") || | ||
| 105 | lower.includes("repository.pullrequest") || | ||
| 106 | lower.includes("no pull requests found for branch") || | ||
| 107 | lower.includes("pull request not found") | ||
| 108 | ) { | ||
| 109 | return new GitHubCliError({ | ||
| 110 | operation, | ||
| 111 | detail: "Pull request not found. Check the PR number or URL and try again.", | ||
| 112 | cause: error, | ||
| 113 | }); | ||
| 114 | } | ||
| 115 | |||
| 116 | return new GitHubCliError({ | ||
| 117 | operation, | ||
| 118 | detail: `GitHub CLI command failed: ${error.message}`, | ||
| 119 | cause: error, | ||
| 120 | }); | ||
| 121 | } | ||
| 122 | |||
| 123 | return new GitHubCliError({ | ||
| 124 | operation, | ||
| 125 | detail: "GitHub CLI command failed.", | ||
| 126 | cause: error, | ||
| 127 | }); | ||
| 128 | } | ||
| 129 | |||
| 130 | const RawGitHubRepositoryCloneUrlsSchema = Schema.Struct({ | ||
| 131 | nameWithOwner: TrimmedNonEmptyString, | ||
| 132 | url: TrimmedNonEmptyString, | ||
| 133 | sshUrl: TrimmedNonEmptyString, | ||
| 134 | }); | ||
| 135 | |||
| 136 | function normalizeRepositoryCloneUrls( | ||
| 137 | raw: Schema.Schema.Type<typeof RawGitHubRepositoryCloneUrlsSchema>, | ||
| 138 | ): GitHubRepositoryCloneUrls { | ||
| 139 | return { | ||
| 140 | nameWithOwner: raw.nameWithOwner, | ||
| 141 | url: raw.url, | ||
| 142 | sshUrl: raw.sshUrl, | ||
| 143 | }; | ||
| 144 | } | ||
| 145 | |||
| 146 | function decodeGitHubJson<S extends Schema.Top>( | ||
| 147 | raw: string, | ||
| 148 | schema: S, | ||
| 149 | operation: "listOpenPullRequests" | "getPullRequest" | "getRepositoryCloneUrls", | ||
| 150 | invalidDetail: string, | ||
| 151 | ): Effect.Effect<S["Type"], GitHubCliError, S["DecodingServices"]> { | ||
| 152 | return Schema.decodeEffect(Schema.fromJsonString(schema))(raw).pipe( | ||
| 153 | Effect.mapError( | ||
| 154 | (error) => | ||
| 155 | new GitHubCliError({ | ||
| 156 | operation, | ||
| 157 | detail: `${invalidDetail}: ${SchemaIssue.makeFormatterDefault()(error.issue)}`, | ||
| 158 | cause: error, | ||
| 159 | }), | ||
| 160 | ), | ||
| 161 | ); | ||
| 162 | } | ||
| 163 | |||
| 164 | export const make = Effect.sync(() => { | ||
| 165 | const execute: GitHubCliShape["execute"] = (input) => | ||
| 166 | Effect.tryPromise({ | ||
| 167 | try: () => | ||
| 168 | runProcess("gh", input.args, { | ||
| 169 | cwd: input.cwd, | ||
| 170 | timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, | ||
| 171 | }), | ||
| 172 | catch: (error) => normalizeGitHubCliError("execute", error), | ||
| 173 | }); | ||
| 174 | |||
| 175 | return GitHubCli.of({ | ||
| 176 | execute, | ||
| 177 | listOpenPullRequests: (input) => | ||
| 178 | execute({ | ||
| 179 | cwd: input.cwd, | ||
| 180 | args: [ | ||
| 181 | "pr", | ||
| 182 | "list", | ||
| 183 | "--head", | ||
| 184 | input.headSelector, | ||
| 185 | "--state", | ||
| 186 | "open", | ||
| 187 | "--limit", | ||
| 188 | String(input.limit ?? 1), | ||
| 189 | "--json", | ||
| 190 | "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner", | ||
| 191 | ], | ||
| 192 | }).pipe( | ||
| 193 | Effect.map((result) => result.stdout.trim()), | ||
| 194 | Effect.flatMap((raw) => | ||
| 195 | raw.length === 0 | ||
| 196 | ? Effect.succeed([]) | ||
| 197 | : Effect.sync(() => decodeGitHubPullRequestListJson(raw)).pipe( | ||
| 198 | Effect.flatMap((decoded) => { | ||
| 199 | if (!Result.isSuccess(decoded)) { | ||
| 200 | return Effect.fail( | ||
| 201 | new GitHubCliError({ | ||
| 202 | operation: "listOpenPullRequests", | ||
| 203 | detail: `GitHub CLI returned invalid PR list JSON: ${formatGitHubJsonDecodeError(decoded.failure)}`, | ||
| 204 | cause: decoded.failure, | ||
| 205 | }), | ||
| 206 | ); | ||
| 207 | } | ||
| 208 | |||
| 209 | return Effect.succeed( | ||
| 210 | decoded.success.map(({ updatedAt: _updatedAt, ...summary }) => summary), | ||
| 211 | ); | ||
| 212 | }), | ||
| 213 | ), | ||
| 214 | ), | ||
| 215 | ), | ||
| 216 | getPullRequest: (input) => | ||
| 217 | execute({ | ||
| 218 | cwd: input.cwd, | ||
| 219 | args: [ | ||
| 220 | "pr", | ||
| 221 | "view", | ||
| 222 | input.reference, | ||
| 223 | "--json", | ||
| 224 | "number,title,url,baseRefName,headRefName,state,mergedAt,isCrossRepository,headRepository,headRepositoryOwner", | ||
| 225 | ], | ||
| 226 | }).pipe( | ||
| 227 | Effect.map((result) => result.stdout.trim()), | ||
| 228 | Effect.flatMap((raw) => | ||
| 229 | Effect.sync(() => decodeGitHubPullRequestJson(raw)).pipe( | ||
| 230 | Effect.flatMap((decoded) => { | ||
| 231 | if (!Result.isSuccess(decoded)) { | ||
| 232 | return Effect.fail( | ||
| 233 | new GitHubCliError({ | ||
| 234 | operation: "getPullRequest", | ||
| 235 | detail: `GitHub CLI returned invalid pull request JSON: ${formatGitHubJsonDecodeError(decoded.failure)}`, | ||
| 236 | cause: decoded.failure, | ||
| 237 | }), | ||
| 238 | ); | ||
| 239 | } | ||
| 240 | |||
| 241 | return Effect.succeed( | ||
| 242 | (({ updatedAt: _updatedAt, ...summary }) => summary)(decoded.success), | ||
| 243 | ); | ||
| 244 | }), | ||
| 245 | ), | ||
| 246 | ), | ||
| 247 | ), | ||
| 248 | getRepositoryCloneUrls: (input) => | ||
| 249 | execute({ | ||
| 250 | cwd: input.cwd, | ||
| 251 | args: ["repo", "view", input.repository, "--json", "nameWithOwner,url,sshUrl"], | ||
| 252 | }).pipe( | ||
| 253 | Effect.map((result) => result.stdout.trim()), | ||
| 254 | Effect.flatMap((raw) => | ||
| 255 | decodeGitHubJson( | ||
| 256 | raw, | ||
| 257 | RawGitHubRepositoryCloneUrlsSchema, | ||
| 258 | "getRepositoryCloneUrls", | ||
| 259 | "GitHub CLI returned invalid repository JSON.", | ||
| 260 | ), | ||
| 261 | ), | ||
| 262 | Effect.map(normalizeRepositoryCloneUrls), | ||
| 263 | ), | ||
| 264 | createPullRequest: (input) => | ||
| 265 | execute({ | ||
| 266 | cwd: input.cwd, | ||
| 267 | args: [ | ||
| 268 | "pr", | ||
| 269 | "create", | ||
| 270 | "--base", | ||
| 271 | input.baseBranch, | ||
| 272 | "--head", | ||
| 273 | input.headSelector, | ||
| 274 | "--title", | ||
| 275 | input.title, | ||
| 276 | "--body-file", | ||
| 277 | input.bodyFile, | ||
| 278 | ], | ||
| 279 | }).pipe(Effect.asVoid), | ||
| 280 | getDefaultBranch: (input) => | ||
| 281 | execute({ | ||
| 282 | cwd: input.cwd, | ||
| 283 | args: ["repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], | ||
| 284 | }).pipe( | ||
| 285 | Effect.map((value) => { | ||
| 286 | const trimmed = value.stdout.trim(); | ||
| 287 | return trimmed.length > 0 ? trimmed : null; | ||
| 288 | }), | ||
| 289 | ), | ||
| 290 | checkoutPullRequest: (input) => | ||
| 291 | execute({ | ||
| 292 | cwd: input.cwd, | ||
| 293 | args: ["pr", "checkout", input.reference, ...(input.force ? ["--force"] : [])], | ||
| 294 | }).pipe(Effect.asVoid), | ||
| 295 | }); | ||
| 296 | }); | ||
| 297 | |||
| 298 | export const layer = Layer.effect(GitHubCli, make); | ||
| @@ -1,7 +1,7 @@ | |||
| 1 | import { assert, it } from "@effect/vitest"; | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import { DateTime, Effect, Layer, Option } from "effect"; | 2 | import { DateTime, Effect, Layer, Option } from "effect"; |
| 3 | 3 | ||
| 4 | import { GitHubCli, type GitHubCliShape } from "../git/Services/GitHubCli.ts"; | 4 | import { GitHubCli, type GitHubCliShape } from "./GitHubCli.ts"; |
| 5 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; | 5 | import * as GitHubSourceControlProvider from "./GitHubSourceControlProvider.ts"; |
| 6 | 6 | ||
| 7 | const processResult = (stdout: string) => ({ | 7 | const processResult = (stdout: string) => ({ |
| @@ -6,7 +6,7 @@ import { · 5 unmodified lines | |||
| 6 | type GitHubCliError, | 6 | type GitHubCliError, |
| 7 | } from "@t3tools/contracts"; | 7 | } from "@t3tools/contracts"; |
| 8 | 8 | ||
| 9 | import { GitHubCli, type GitHubPullRequestSummary } from "../git/Services/GitHubCli.ts"; | 9 | import { GitHubCli, type GitHubPullRequestSummary } from "./GitHubCli.ts"; |
| 10 | import { decodeGitHubPullRequestListJson } from "../git/githubPullRequests.ts"; | 10 | import { decodeGitHubPullRequestListJson } from "../git/githubPullRequests.ts"; |
| 11 | import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts"; | 11 | import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts"; |
| 12 | 12 | ||
| @@ -1,7 +1,7 @@ | |||
| 1 | import { assert, it } from "@effect/vitest"; | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import { Effect, Layer } from "effect"; | 2 | import { Effect, Layer } from "effect"; |
| 3 | 3 | ||
| 4 | import { GitHubCli } from "../git/Services/GitHubCli.ts"; | 4 | import { GitHubCli } from "./GitHubCli.ts"; |
| 5 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | 5 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 6 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; | 6 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 7 | 7 | ||