Showing the changes to apps/server/src/sourceControl/GitHubSourceControlProvider.ts (1 of 59 files). Show all
Unified · Split
| @@ -5,11 +5,15 @@ import { · 4 unmodified lines | ||
| 5 | 5 | type ChangeRequestState, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | |
| 8 | −import { GitHubCli, type GitHubCliError, type GitHubPullRequestSummary } from "./GitHubCli.ts"; | |
| 9 | −import { decodeGitHubPullRequestListJson } from "./gitHubPullRequests.ts"; | |
| 10 | −import { SourceControlProvider, type SourceControlProviderShape } from "./SourceControlProvider.ts"; | |
| 8 | +import * as GitHubCli from "./GitHubCli.ts"; | |
| 9 | +import * as GitHubPullRequests from "./gitHubPullRequests.ts"; | |
| 10 | +import * as SourceControlProvider from "./SourceControlProvider.ts"; | |
| 11 | +import * as SourceControlProviderDiscovery from "./SourceControlProviderDiscovery.ts"; | |
| 11 | 12 | |
| 12 | −function providerError(operation: string, cause: GitHubCliError): SourceControlProviderError { | |
| 13 | +function providerError( | |
| 14 | + operation: string, | |
| 15 | + cause: GitHubCli.GitHubCliError, | |
| 16 | +): SourceControlProviderError { | |
| 13 | 17 | return new SourceControlProviderError({ |
| 14 | 18 | provider: "github", |
| 15 | 19 | operation, |
| @@ -18,7 +22,7 @@ function providerError(operation: string, cause: GitHubCliError): SourceControlP · 2 unmodified lines | ||
| 18 | 22 | }); |
| 19 | 23 | } |
| 20 | 24 | |
| 21 | −function toChangeRequest(summary: GitHubPullRequestSummary): ChangeRequest { | |
| 25 | +function toChangeRequest(summary: GitHubCli.GitHubPullRequestSummary): ChangeRequest { | |
| 22 | 26 | return { |
| 23 | 27 | provider: "github", |
| 24 | 28 | number: summary.number, |
| @@ -40,75 +44,119 @@ function toChangeRequest(summary: GitHubPullRequestSummary): ChangeRequest { · 15 unmodified lines | ||
| 40 | 44 | }; |
| 41 | 45 | } |
| 42 | 46 | |
| 47 | +function parseGitHubAuth(input: SourceControlProviderDiscovery.SourceControlAuthProbeInput) { | |
| 48 | + const output = SourceControlProviderDiscovery.combinedAuthOutput(input); | |
| 49 | + const account = SourceControlProviderDiscovery.matchFirst(output, [ | |
| 50 | + /Logged in to .* account\s+([^\s(]+)/iu, | |
| 51 | + /Logged in to .* as\s+([^\s(]+)/iu, | |
| 52 | + ]); | |
| 53 | + const host = SourceControlProviderDiscovery.parseCliHost(output); | |
| 54 | + | |
| 55 | + if (input.exitCode !== 0) { | |
| 56 | + return SourceControlProviderDiscovery.providerAuth({ | |
| 57 | + status: "unauthenticated", | |
| 58 | + host, | |
| 59 | + detail: | |
| 60 | + SourceControlProviderDiscovery.firstSafeAuthLine(output) ?? | |
| 61 | + "Run `gh auth login` to authenticate GitHub CLI.", | |
| 62 | + }); | |
| 63 | + } | |
| 64 | + | |
| 65 | + if (account) { | |
| 66 | + return SourceControlProviderDiscovery.providerAuth({ status: "authenticated", account, host }); | |
| 67 | + } | |
| 68 | + | |
| 69 | + return SourceControlProviderDiscovery.providerAuth({ | |
| 70 | + status: "unknown", | |
| 71 | + host, | |
| 72 | + detail: | |
| 73 | + SourceControlProviderDiscovery.firstSafeAuthLine(output) ?? | |
| 74 | + "GitHub CLI auth status could not be parsed.", | |
| 75 | + }); | |
| 76 | +} | |
| 77 | + | |
| 78 | +export const discovery = { | |
| 79 | + type: "cli", | |
| 80 | + kind: "github", | |
| 81 | + label: "GitHub", | |
| 82 | + executable: "gh", | |
| 83 | + versionArgs: ["--version"], | |
| 84 | + authArgs: ["auth", "status"], | |
| 85 | + parseAuth: parseGitHubAuth, | |
| 86 | + installHint: | |
| 87 | + "Install the GitHub command-line tool (`gh`) via https://cli.github.com/ or your package manager (for example `brew install gh`).", | |
| 88 | +} satisfies SourceControlProviderDiscovery.SourceControlCliDiscoverySpec; | |
| 89 | + | |
| 43 | 90 | export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { |
| 44 | − const github = yield* GitHubCli; | |
| 91 | + const github = yield* GitHubCli.GitHubCli; | |
| 45 | 92 | |
| 46 | − const listChangeRequests: SourceControlProviderShape["listChangeRequests"] = (input) => { | |
| 47 | − if (input.state === "open") { | |
| 93 | + const listChangeRequests: SourceControlProvider.SourceControlProviderShape["listChangeRequests"] = | |
| 94 | + (input) => { | |
| 95 | + if (input.state === "open") { | |
| 96 | + return github | |
| 97 | + .listOpenPullRequests({ | |
| 98 | + cwd: input.cwd, | |
| 99 | + headSelector: input.headSelector, | |
| 100 | + ...(input.limit !== undefined ? { limit: input.limit } : {}), | |
| 101 | + }) | |
| 102 | + .pipe( | |
| 103 | + Effect.map((items) => items.map(toChangeRequest)), | |
| 104 | + Effect.mapError((error) => providerError("listChangeRequests", error)), | |
| 105 | + ); | |
| 106 | + } | |
| 107 | + | |
| 108 | + const stateArg: ChangeRequestState | "all" = input.state; | |
| 48 | 109 | return github |
| 49 | − .listOpenPullRequests({ | |
| 110 | + .execute({ | |
| 50 | 111 | cwd: input.cwd, |
| 51 | − headSelector: input.headSelector, | |
| 52 | − ...(input.limit !== undefined ? { limit: input.limit } : {}), | |
| 112 | + args: [ | |
| 113 | + "pr", | |
| 114 | + "list", | |
| 115 | + "--head", | |
| 116 | + input.headSelector, | |
| 117 | + "--state", | |
| 118 | + stateArg, | |
| 119 | + "--limit", | |
| 120 | + String(input.limit ?? 20), | |
| 121 | + "--json", | |
| 122 | + "number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner", | |
| 123 | + ], | |
| 53 | 124 | }) |
| 54 | 125 | .pipe( |
| 55 | − Effect.map((items) => items.map(toChangeRequest)), | |
| 56 | − Effect.mapError((error) => providerError("listChangeRequests", error)), | |
| 126 | + Effect.flatMap((result) => { | |
| 127 | + const raw = result.stdout.trim(); | |
| 128 | + if (raw.length === 0) { | |
| 129 | + return Effect.succeed([]); | |
| 130 | + } | |
| 131 | + return Effect.sync(() => GitHubPullRequests.decodeGitHubPullRequestListJson(raw)).pipe( | |
| 132 | + Effect.flatMap((decoded) => | |
| 133 | + Result.isSuccess(decoded) | |
| 134 | + ? Effect.succeed( | |
| 135 | + decoded.success.map((item) => ({ | |
| 136 | + ...toChangeRequest(item), | |
| 137 | + updatedAt: item.updatedAt, | |
| 138 | + })), | |
| 139 | + ) | |
| 140 | + : Effect.fail( | |
| 141 | + new SourceControlProviderError({ | |
| 142 | + provider: "github", | |
| 143 | + operation: "listChangeRequests", | |
| 144 | + detail: "GitHub CLI returned invalid change request JSON.", | |
| 145 | + cause: decoded.failure, | |
| 146 | + }), | |
| 147 | + ), | |
| 148 | + ), | |
| 149 | + ); | |
| 150 | + }), | |
| 151 | + Effect.mapError((error) => | |
| 152 | + Schema.is(SourceControlProviderError)(error) | |
| 153 | + ? error | |
| 154 | + : providerError("listChangeRequests", error), | |
| 155 | + ), | |
| 57 | 156 | ); |
| 58 | − } | |
| 59 | − | |
| 60 | − const stateArg: ChangeRequestState | "all" = input.state; | |
| 61 | − return github | |
| 62 | − .execute({ | |
| 63 | − cwd: input.cwd, | |
| 64 | − args: [ | |
| 65 | − "pr", | |
| 66 | − "list", | |
| 67 | − "--head", | |
| 68 | − input.headSelector, | |
| 69 | − "--state", | |
| 70 | − stateArg, | |
| 71 | − "--limit", | |
| 72 | − String(input.limit ?? 20), | |
| 73 | − "--json", | |
| 74 | − "number,title,url,baseRefName,headRefName,state,mergedAt,updatedAt,isCrossRepository,headRepository,headRepositoryOwner", | |
| 75 | − ], | |
| 76 | − }) | |
| 77 | − .pipe( | |
| 78 | − Effect.flatMap((result) => { | |
| 79 | − const raw = result.stdout.trim(); | |
| 80 | − if (raw.length === 0) { | |
| 81 | − return Effect.succeed([]); | |
| 82 | − } | |
| 83 | − return Effect.sync(() => decodeGitHubPullRequestListJson(raw)).pipe( | |
| 84 | − Effect.flatMap((decoded) => | |
| 85 | − Result.isSuccess(decoded) | |
| 86 | − ? Effect.succeed( | |
| 87 | − decoded.success.map((item) => ({ | |
| 88 | − ...toChangeRequest(item), | |
| 89 | − updatedAt: item.updatedAt, | |
| 90 | − })), | |
| 91 | − ) | |
| 92 | − : Effect.fail( | |
| 93 | − new SourceControlProviderError({ | |
| 94 | − provider: "github", | |
| 95 | − operation: "listChangeRequests", | |
| 96 | − detail: "GitHub CLI returned invalid change request JSON.", | |
| 97 | − cause: decoded.failure, | |
| 98 | − }), | |
| 99 | − ), | |
| 100 | − ), | |
| 101 | − ); | |
| 102 | − }), | |
| 103 | − Effect.mapError((error) => | |
| 104 | − Schema.is(SourceControlProviderError)(error) | |
| 105 | − ? error | |
| 106 | − : providerError("listChangeRequests", error), | |
| 107 | − ), | |
| 108 | − ); | |
| 109 | − }; | |
| 157 | + }; | |
| 110 | 158 | |
| 111 | − return SourceControlProvider.of({ | |
| 159 | + return SourceControlProvider.SourceControlProvider.of({ | |
| 112 | 160 | kind: "github", |
| 113 | 161 | listChangeRequests, |
| 114 | 162 | getChangeRequest: (input) => |
| @@ -145,4 +193,4 @@ export const make = Effect.fn("makeGitHubSourceControlProvider")(function* () { · 30 unmodified lines | ||
| 145 | 193 | }); |
| 146 | 194 | }); |
| 147 | 195 | |
| 148 | −export const layer = Layer.effect(SourceControlProvider, make()); | |
| 196 | +export const layer = Layer.effect(SourceControlProvider.SourceControlProvider, make()); | |