authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-10 12:07:00-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 12:07:00-07:00
logc8a603307c2d50f8fb9d302f4b51f20fd9f91b33
tree9fc9a44bcc54e0ae1240e52dd628e5560914d5bc
parent7bc732804222946c0c9b23af057484e4412698fc
signature Signed by PGP key B5690EEEBB952194

refactor(client-runtime): add project clone sources come from host definitions (#17756)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

6 files changed, 85 insertions(+), 136 deletions(-)

apps/mobile/src/components/SourceControlIcon.tsx+12-4
...@@ -1,12 +1,11 @@...@@ -1,12 +1,11 @@
1import Svg, { Circle, Defs, G, LinearGradient, Path, Stop } from "react-native-svg";1import Svg, { Circle, Defs, G, LinearGradient, Path, Rect, Stop } from "react-native-svg";
2import { withUniwind } from "uniwind";2import { withUniwind } from "uniwind";
33
4const ThemedSvg = withUniwind(Svg);4const ThemedSvg = withUniwind(Svg);
55
6export type SourceControlIconKind = "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops";6/** Draws a host's glyph by its client definition's `icon` key; null for a key it has no art for. */
7
8export function SourceControlIcon(props: {7export function SourceControlIcon(props: {
9 readonly kind: SourceControlIconKind;8 readonly kind: string;
10 readonly size?: number;9 readonly size?: number;
11 readonly color?: string;10 readonly color?: string;
12 readonly colorClassName?: string;11 readonly colorClassName?: string;
...@@ -252,5 +251,14 @@ export function SourceControlIcon(props: {...@@ -252,5 +251,14 @@ export function SourceControlIcon(props: {
252 />251 />
253 </Svg>252 </Svg>
254 );253 );
254 case "gitcafe":
255 // GitCafe's own mark from https://git.cafe/favicon.svg, inset like the web icon.
256 return (
257 <Svg width={size} height={size} viewBox="0 0 64 64">
258 <Rect x={4.8} y={4.8} width={54.4} height={54.4} fill="#a78bfa" />
259 </Svg>
260 );
261 default:
262 return null;
255 }263 }
256}264}
apps/mobile/src/features/projects/AddProjectRepositoryRoute.tsx+5-9
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import type { StaticScreenProps } from "@react-navigation/native";1import type { StaticScreenProps } from "@react-navigation/native";
2import { NativeStackScreenOptions } from "../../native/StackHeader";2import { NativeStackScreenOptions } from "../../native/StackHeader";
3import { addProjectRemoteSourceLabel } from "@t3tools/client-runtime/operations/projects";3import {
4 addProjectRemoteSourceLabel,
5 parseAddProjectRemoteSource,
6} from "@t3tools/client-runtime/operations/projects";
47
5import { AddProjectRepositoryScreen } from "./AddProjectScreen";8import { AddProjectRepositoryScreen } from "./AddProjectScreen";
69
...@@ -14,14 +17,7 @@ export function AddProjectRepositoryRoute({...@@ -14,14 +17,7 @@ export function AddProjectRepositoryRoute({
14}: StaticScreenProps<AddProjectRepositoryRouteParams>) {17}: StaticScreenProps<AddProjectRepositoryRouteParams>) {
15 const params = route.params ?? {};18 const params = route.params ?? {};
16 const source = Array.isArray(params.source) ? params.source[0] : params.source;19 const source = Array.isArray(params.source) ? params.source[0] : params.source;
17 const title =20 const title = addProjectRemoteSourceLabel(parseAddProjectRemoteSource(source));
18 source === "github" ||
19 source === "gitlab" ||
20 source === "forgejo" ||
21 source === "bitbucket" ||
22 source === "azure-devops"
23 ? addProjectRemoteSourceLabel(source)
24 : "Git URL";
2521
26 return (22 return (
27 <>23 <>
apps/mobile/src/features/projects/AddProjectScreen.tsx+10-19
...@@ -1,4 +1,3 @@...@@ -1,4 +1,3 @@
1import { SourceControlProviderKind } from "@t3tools/contracts";
2import { MaterialListRow } from "../../components/MaterialListRow";1import { MaterialListRow } from "../../components/MaterialListRow";
3import { SettingsScreen } from "../settings/components/SettingsScreen";2import { SettingsScreen } from "../settings/components/SettingsScreen";
4import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";3import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
...@@ -20,6 +19,7 @@ import {...@@ -20,6 +19,7 @@ import {
20 getNewProjectGitHubTarget,19 getNewProjectGitHubTarget,
21 getNewProjectPathPreview,20 getNewProjectPathPreview,
22 normalizePastedCloneUrl,21 normalizePastedCloneUrl,
22 parseAddProjectRemoteSource,
23 resolveAddProjectPath,23 resolveAddProjectPath,
24 sortAddProjectProviderSources,24 sortAddProjectProviderSources,
25 type AddProjectRemoteSource,25 type AddProjectRemoteSource,
...@@ -49,6 +49,7 @@ import {...@@ -49,6 +49,7 @@ import {
49 type EnvironmentMachineKind,49 type EnvironmentMachineKind,
50 ProjectId,50 ProjectId,
51 resolveEnvironmentMachineKind,51 resolveEnvironmentMachineKind,
52 SourceControlProviderKind,
52} from "@t3tools/contracts";53} from "@t3tools/contracts";
53import { CommonActions, StackActions, useNavigation } from "@react-navigation/native";54import { CommonActions, StackActions, useNavigation } from "@react-navigation/native";
54import { SymbolView } from "../../components/AppSymbol";55import { SymbolView } from "../../components/AppSymbol";
...@@ -71,6 +72,7 @@ import { AppText as Text, AppTextInput as TextInput } from "../../components/App...@@ -71,6 +72,7 @@ import { AppText as Text, AppTextInput as TextInput } from "../../components/App
71import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";72import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
72import { ErrorBanner } from "../../components/ErrorBanner";73import { ErrorBanner } from "../../components/ErrorBanner";
73import { SourceControlIcon } from "../../components/SourceControlIcon";74import { SourceControlIcon } from "../../components/SourceControlIcon";
75import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
74import { ThemedSwitch } from "../../components/ThemedSwitch";76import { ThemedSwitch } from "../../components/ThemedSwitch";
75import { uuidv4 } from "../../lib/uuid";77import { uuidv4 } from "../../lib/uuid";
76import { useAtomCommand } from "../../state/use-atom-command";78import { useAtomCommand } from "../../state/use-atom-command";
...@@ -123,18 +125,7 @@ function stringParam(value: string | string[] | undefined): string | null {...@@ -123,18 +125,7 @@ function stringParam(value: string | string[] | undefined): string | null {
123}125}
124126
125function sourceFromParam(value: string | string[] | undefined): AddProjectRemoteSource {127function sourceFromParam(value: string | string[] | undefined): AddProjectRemoteSource {
126 const source = stringParam(value);128 return parseAddProjectRemoteSource(stringParam(value));
127 if (
128 source === "url" ||
129 source === "github" ||
130 source === "gitlab" ||
131 source === "forgejo" ||
132 source === "bitbucket" ||
133 source === "azure-devops"
134 ) {
135 return source;
136 }
137 return "url";
138}129}
139130
140function SectionTitle(props: { readonly children: string }) {131function SectionTitle(props: { readonly children: string }) {
...@@ -499,7 +490,7 @@ function SourceControlRow(props: {...@@ -499,7 +490,7 @@ function SourceControlRow(props: {
499 />490 />
500 ) : (491 ) : (
501 <SourceControlIcon492 <SourceControlIcon
502 kind={props.source}493 kind={sourceControlClients.get(props.source).icon}
503 size={Platform.OS === "android" ? 24 : 18}494 size={Platform.OS === "android" ? 24 : 18}
504 colorClassName="accent-icon"495 colorClassName="accent-icon"
505 />496 />
...@@ -659,13 +650,13 @@ export function AddProjectSourceScreen() {...@@ -659,13 +650,13 @@ export function AddProjectSourceScreen() {
659 key={candidate}650 key={candidate}
660 source={candidate}651 source={candidate}
661 selectedEnvironmentId={selectedEnvironment.environmentId}652 selectedEnvironmentId={selectedEnvironment.environmentId}
662 ready={canCloneProject && readiness[candidate].ready}653 ready={canCloneProject && readiness(candidate).ready}
663 hint={654 hint={
664 !canCloneProject655 !canCloneProject
665 ? "This connection cannot clone projects."656 ? "This connection cannot clone projects."
666 : readiness[candidate].ready657 : readiness(candidate).ready
667 ? addProjectRemoteSourcePathHint(candidate)658 ? addProjectRemoteSourcePathHint(candidate)
668 : (readiness[candidate].hint ?? "")659 : (readiness(candidate).hint ?? "")
669 }660 }
670 isFirst={false}661 isFirst={false}
671 />662 />
...@@ -813,7 +804,7 @@ export function AddProjectRepositoryScreen(props: {...@@ -813,7 +804,7 @@ export function AddProjectRepositoryScreen(props: {
813 const result = await lookupRepositoryQuery({804 const result = await lookupRepositoryQuery({
814 environmentId: environment.environmentId,805 environmentId: environment.environmentId,
815 input: {806 input: {
816 provider: SourceControlProviderKind.make(provider),807 provider,
817 repository: repositoryInput.trim(),808 repository: repositoryInput.trim(),
818 },809 },
819 });810 });
...@@ -1153,7 +1144,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s...@@ -1153,7 +1144,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s
1153 }1144 }
1154 icon={1145 icon={
1155 <SourceControlIcon1146 <SourceControlIcon
1156 kind="github"1147 kind={sourceControlClients.get("github").icon}
1157 size={Platform.OS === "android" ? 24 : 18}1148 size={Platform.OS === "android" ? 24 : 18}
1158 colorClassName="accent-icon"1149 colorClassName="accent-icon"
1159 />1150 />
apps/mobile/src/state/thread-pr-presentation.ts+3-3
...@@ -4,7 +4,7 @@ import type {...@@ -4,7 +4,7 @@ import type {
4 ThreadPullRequestLink,4 ThreadPullRequestLink,
5 VcsStatusResult,5 VcsStatusResult,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl";7import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
88
9import {9import {
10 resolveThreadCurrentPullRequestLink,10 resolveThreadCurrentPullRequestLink,
...@@ -39,7 +39,7 @@ export function presentThreadPr(...@@ -39,7 +39,7 @@ export function presentThreadPr(
39 pr: ThreadPr,39 pr: ThreadPr,
40 provider: VcsStatusResult["sourceControlProvider"] | null | undefined,40 provider: VcsStatusResult["sourceControlProvider"] | null | undefined,
41): ThreadPrPresentation {41): ThreadPrPresentation {
42 const presentation = resolveChangeRequestPresentation(provider);42 const { changeRequest } = sourceControlClients.get(provider?.kind);
43 const isDraft = pr.state === "open" && pr.isDraft === true;43 const isDraft = pr.state === "open" && pr.isDraft === true;
44 return {44 return {
45 kind: "pull-request",45 kind: "pull-request",
...@@ -50,7 +50,7 @@ export function presentThreadPr(...@@ -50,7 +50,7 @@ export function presentThreadPr(
50 updatedAt: pr.updatedAt ?? null,50 updatedAt: pr.updatedAt ?? null,
51 url: pr.url,51 url: pr.url,
52 label: String(pr.number),52 label: String(pr.number),
53 accessibilityLabel: `#${pr.number} ${presentation.longName} ${isDraft ? "draft" : pr.state}`,53 accessibilityLabel: `#${pr.number} ${changeRequest.singular} ${isDraft ? "draft" : pr.state}`,
54 textClassName: isDraft ? "text-foreground-muted" : PR_STATE_TEXT_CLASS[pr.state],54 textClassName: isDraft ? "text-foreground-muted" : PR_STATE_TEXT_CLASS[pr.state],
55 };55 };
56}56}
packages/client-runtime/src/operations/projects.test.ts+6-3
...@@ -219,9 +219,12 @@ describe("add project shared logic", () => {...@@ -219,9 +219,12 @@ describe("add project shared logic", () => {
219 };219 };
220220
221 const readiness = buildAddProjectRemoteSourceReadiness(discovery);221 const readiness = buildAddProjectRemoteSourceReadiness(discovery);
222 expect(readiness.url.ready).toBe(true);222 expect(readiness("url").ready).toBe(true);
223 expect(readiness.github.ready).toBe(true);223 expect(readiness(SourceControlProviderKind.make("github")).ready).toBe(true);
224 expect(readiness.gitlab).toEqual({ ready: false, hint: "Run glab auth login" });224 expect(readiness(SourceControlProviderKind.make("gitlab"))).toEqual({
225 ready: false,
226 hint: "Run glab auth login",
227 });
225 expect(sortAddProjectProviderSources(readiness)[0]).toBe("github");228 expect(sortAddProjectProviderSources(readiness)[0]).toBe("github");
226 });229 });
227230
packages/client-runtime/src/operations/projects.ts+49-98
...@@ -24,14 +24,10 @@ import {...@@ -24,14 +24,10 @@ import {
24 resolveProjectPathForDispatch,24 resolveProjectPathForDispatch,
25} from "../state/projects.ts";25} from "../state/projects.ts";
26import type { EnvironmentProject } from "../state/models.ts";26import type { EnvironmentProject } from "../state/models.ts";
27import { sourceControlClients } from "../sourceControlClients.ts";
2728
28export type AddProjectRemoteProviderKind =29/** A host to clone from, or `url` for a pasted clone URL. */
29 | "github"30export type AddProjectRemoteSource = SourceControlProviderKind | "url";
30 | "gitlab"
31 | "forgejo"
32 | "bitbucket"
33 | "azure-devops";
34export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url";
3531
36export function canCreateProjectInEnvironment(32export function canCreateProjectInEnvironment(
37 connectionPhase: EnvironmentConnectionPhase | null | undefined,33 connectionPhase: EnvironmentConnectionPhase | null | undefined,
...@@ -52,10 +48,15 @@ export function availableScratchWorkspaceRoot(...@@ -52,10 +48,15 @@ export function availableScratchWorkspaceRoot(
52 : null;48 : null;
53}49}
5450
55export type AddProjectRemoteSourceReadiness = Record<51export interface AddProjectRemoteSourceReadinessEntry {
56 AddProjectRemoteSource,52 readonly ready: boolean;
57 { readonly ready: boolean; readonly hint: string | null }53 readonly hint: string | null;
58>;54}
55
56/** Whether each clone source can be used on an environment, and why not. */
57export type AddProjectRemoteSourceReadiness = (
58 source: AddProjectRemoteSource,
59) => AddProjectRemoteSourceReadinessEntry;
5960
60export type AddProjectCloneFlow =61export type AddProjectCloneFlow =
61 | {62 | {
...@@ -72,62 +73,31 @@ export type AddProjectCloneFlow =...@@ -72,62 +73,31 @@ export type AddProjectCloneFlow =
72 readonly remoteUrl: string;73 readonly remoteUrl: string;
73 };74 };
7475
75const ADD_PROJECT_REMOTE_SOURCES: ReadonlyArray<AddProjectRemoteSource> = [76/** The hosts the clone picker offers, in the order the built-in definitions list them. */
76 "url",77const ADD_PROJECT_REMOTE_PROVIDER_SOURCES: ReadonlyArray<SourceControlProviderKind> =
77 "github",78 sourceControlClients.definitions.map((definition) => definition.kind);
78 "gitlab",
79 "forgejo",
80 "bitbucket",
81 "azure-devops",
82];
83
84const ADD_PROJECT_REMOTE_PROVIDER_SOURCES: ReadonlyArray<AddProjectRemoteProviderKind> = [
85 "github",
86 "gitlab",
87 "forgejo",
88 "bitbucket",
89 "azure-devops",
90];
9179
92export function addProjectRemoteSourceLabel(source: AddProjectRemoteSource): string {80export function addProjectRemoteSourceLabel(source: AddProjectRemoteSource): string {
93 switch (source) {81 return source === "url" ? "Git URL" : sourceControlClients.get(source).pickerLabel;
94 case "github":
95 return "GitHub";
96 case "forgejo":
97 return "Forgejo / Gitea";
98 case "gitlab":
99 return "GitLab";
100 case "bitbucket":
101 return "Bitbucket";
102 case "azure-devops":
103 return "Azure DevOps";
104 case "url":
105 return "Git URL";
106 }
107}82}
10883
109export function addProjectRemoteSourcePathHint(source: AddProjectRemoteSource): string {84export function addProjectRemoteSourcePathHint(source: AddProjectRemoteSource): string {
110 switch (source) {85 return source === "url" ? "URL" : sourceControlClients.get(source).repositoryPathHint;
111 case "forgejo":
112 case "github":
113 return "owner/repo";
114 case "gitlab":
115 return "group/project";
116 case "bitbucket":
117 return "workspace/repository";
118 case "azure-devops":
119 return "project/repository";
120 case "url":
121 return "URL";
122 }
123}86}
12487
125export function addProjectRemoteSourceProvider(88export function addProjectRemoteSourceProvider(
126 source: AddProjectRemoteSource,89 source: AddProjectRemoteSource,
127): AddProjectRemoteProviderKind | null {90): SourceControlProviderKind | null {
128 return source === "url" ? null : source;91 return source === "url" ? null : source;
129}92}
13093
94/** A clone source named in a route or link, or `url` for anything this client does not ship. */
95export function parseAddProjectRemoteSource(
96 value: string | null | undefined,
97): AddProjectRemoteSource {
98 return (value ? sourceControlClients.find(value)?.kind : undefined) ?? "url";
99}
100
131const GITHUB_REPOSITORY_SHORTHAND =101const GITHUB_REPOSITORY_SHORTHAND =
132 /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+(?:\.git)?$/;102 /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+(?:\.git)?$/;
133103
...@@ -139,18 +109,18 @@ export function normalizePastedCloneUrl(input: string): string {...@@ -139,18 +109,18 @@ export function normalizePastedCloneUrl(input: string): string {
139 return `https://github.com/${repository}`;109 return `https://github.com/${repository}`;
140}110}
141111
142/** GitHub and Forgejo default to HTTPS; other providers retain their existing SSH default. */112/** The clone URL for the transport the repository's host defaults to. */
143export function getDefaultCloneUrl(113export function getDefaultCloneUrl(
144 repository: Pick<SourceControlRepositoryInfo, "provider" | "url" | "sshUrl">,114 repository: Pick<SourceControlRepositoryInfo, "provider" | "url" | "sshUrl">,
145): string {115): string {
146 return repository.provider === "github" || repository.provider === "forgejo"116 return sourceControlClients.get(repository.provider).defaultCloneTransport === "https"
147 ? repository.url117 ? repository.url
148 : repository.sshUrl;118 : repository.sshUrl;
149}119}
150120
151export function sortAddProjectProviderSources(121export function sortAddProjectProviderSources(
152 readinessBySource: AddProjectRemoteSourceReadiness,122 readiness: AddProjectRemoteSourceReadiness,
153): ReadonlyArray<AddProjectRemoteProviderKind> {123): ReadonlyArray<SourceControlProviderKind> {
154 return Arr.sort(124 return Arr.sort(
155 ADD_PROJECT_REMOTE_PROVIDER_SOURCES,125 ADD_PROJECT_REMOTE_PROVIDER_SOURCES,
156 Order.mapInput(126 Order.mapInput(
...@@ -158,61 +128,41 @@ export function sortAddProjectProviderSources(...@@ -158,61 +128,41 @@ export function sortAddProjectProviderSources(
158 ready: Order.flip(Order.Boolean),128 ready: Order.flip(Order.Boolean),
159 label: Order.String,129 label: Order.String,
160 }),130 }),
161 (source: AddProjectRemoteProviderKind) => ({131 (source: SourceControlProviderKind) => ({
162 ready: readinessBySource[source].ready,132 ready: readiness(source).ready,
163 label: addProjectRemoteSourceLabel(source),133 label: addProjectRemoteSourceLabel(source),
164 }),134 }),
165 ),135 ),
166 );136 );
167}137}
168138
139const READY: AddProjectRemoteSourceReadinessEntry = { ready: true, hint: null };
140const UNAVAILABLE: AddProjectRemoteSourceReadinessEntry = {
141 ready: false,
142 hint: "Provider status unavailable. Open Source Control settings and rescan.",
143};
144
169export function buildAddProjectRemoteSourceReadiness(145export function buildAddProjectRemoteSourceReadiness(
170 discovery: SourceControlDiscoveryResult | null,146 discovery: SourceControlDiscoveryResult | null,
171): AddProjectRemoteSourceReadiness {147): AddProjectRemoteSourceReadiness {
172 const unavailable = {
173 ready: false,
174 hint: "Provider status unavailable. Open Source Control settings and rescan.",
175 } as const;
176 const readiness: AddProjectRemoteSourceReadiness = {
177 url: { ready: true, hint: null },
178 github: unavailable,
179 gitlab: unavailable,
180 forgejo: unavailable,
181 bitbucket: unavailable,
182 "azure-devops": unavailable,
183 };
184
185 if (!discovery) {
186 return readiness;
187 }
188
189 const providerByKind = new Map(148 const providerByKind = new Map(
190 discovery.sourceControlProviders.map((provider) => [provider.kind, provider]),149 (discovery?.sourceControlProviders ?? []).map((provider) => [provider.kind, provider]),
191 );150 );
192 for (const source of ADD_PROJECT_REMOTE_SOURCES) {151 return (source) => {
193 const kind = addProjectRemoteSourceProvider(source);152 if (source === "url") return READY;
194 if (!kind) continue;153 const provider = providerByKind.get(source);
195 const provider = providerByKind.get(SourceControlProviderKind.make(kind));154 if (!provider) return UNAVAILABLE;
196 if (!provider) {155 if (provider.status !== "available") return { ready: false, hint: provider.installHint };
197 readiness[source] = unavailable;
198 continue;
199 }
200 if (provider.status !== "available") {
201 readiness[source] = { ready: false, hint: provider.installHint };
202 continue;
203 }
204 if (provider.auth.status === "unauthenticated") {156 if (provider.auth.status === "unauthenticated") {
205 readiness[source] = {157 return {
206 ready: false,158 ready: false,
207 hint:159 hint:
208 Option.getOrNull(provider.auth.detail) ??160 Option.getOrNull(provider.auth.detail) ??
209 `${provider.label} is not authenticated. Open Source Control settings for setup guidance.`,161 `${provider.label} is not authenticated. Open Source Control settings for setup guidance.`,
210 };162 };
211 continue;
212 }163 }
213 readiness[source] = { ready: true, hint: null };164 return READY;
214 }165 };
215 return readiness;
216}166}
217167
218export function getAddProjectInitialQuery(baseDirectory: string | null | undefined): string {168export function getAddProjectInitialQuery(baseDirectory: string | null | undefined): string {
...@@ -283,8 +233,9 @@ export function getNewProjectPathPreview(newProjectsRoot: string, name: string):...@@ -283,8 +233,9 @@ export function getNewProjectPathPreview(newProjectsRoot: string, name: string):
283export function getNewProjectGitHubTarget(233export function getNewProjectGitHubTarget(
284 discovery: SourceControlDiscoveryResult | null,234 discovery: SourceControlDiscoveryResult | null,
285): { readonly account: string | null } | null {235): { readonly account: string | null } | null {
286 if (!buildAddProjectRemoteSourceReadiness(discovery).github.ready) return null;236 const kind = SourceControlProviderKind.make("github");
287 const github = discovery?.sourceControlProviders.find((provider) => provider.kind === "github");237 if (!buildAddProjectRemoteSourceReadiness(discovery)(kind).ready) return null;
238 const github = discovery?.sourceControlProviders.find((provider) => provider.kind === kind);
288 return { account: github ? Option.getOrNull(github.auth.account) : null };239 return { account: github ? Option.getOrNull(github.auth.account) : null };
289}240}
290241