Showing the changes to apps/server/src/sourceControl/SourceControlDiscovery.ts (1 of 33 files). Show all
Unified · Split
| @@ -13,20 +13,22 @@ import * as VcsProcess from "../vcs/VcsProcess.ts"; · 12 unmodified lines | |||
| 13 | 13 | ||
| 14 | interface DiscoveryProbe { | 14 | interface DiscoveryProbe { |
| 15 | readonly label: string; | 15 | readonly label: string; |
| 16 | readonly executable: string; | 16 | readonly executable?: string; |
| 17 | readonly versionArgs: ReadonlyArray<string>; | 17 | readonly versionArgs?: ReadonlyArray<string>; |
| 18 | readonly implemented: boolean; | 18 | readonly implemented: boolean; |
| 19 | readonly installHint: string; | 19 | readonly installHint: string; |
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | type VcsProbe = DiscoveryProbe & { | 22 | type VcsProbe = DiscoveryProbe & { |
| 23 | readonly kind: VcsDriverKind; | 23 | readonly kind: VcsDriverKind; |
| 24 | readonly executable: string; | ||
| 25 | readonly versionArgs: ReadonlyArray<string>; | ||
| 24 | }; | 26 | }; |
| 25 | 27 | ||
| 26 | type ProviderProbe = DiscoveryProbe & { | 28 | type ProviderProbe = DiscoveryProbe & { |
| 27 | readonly kind: SourceControlProviderKind; | 29 | readonly kind: SourceControlProviderKind; |
| 28 | readonly authArgs: ReadonlyArray<string>; | 30 | readonly authArgs?: ReadonlyArray<string>; |
| 29 | readonly parseAuth: (input: AuthProbeInput) => SourceControlProviderAuth; | 31 | readonly parseAuth?: (input: AuthProbeInput) => SourceControlProviderAuth; |
| 30 | }; | 32 | }; |
| 31 | 33 | ||
| 32 | interface AuthProbeInput { | 34 | interface AuthProbeInput { |
| @@ -38,7 +40,7 @@ interface AuthProbeInput { · 5 unmodified lines | |||
| 38 | interface DiscoveryProbeResult<Kind extends string> { | 40 | interface DiscoveryProbeResult<Kind extends string> { |
| 39 | readonly kind: Kind; | 41 | readonly kind: Kind; |
| 40 | readonly label: string; | 42 | readonly label: string; |
| 41 | readonly executable: string; | 43 | readonly executable?: string; |
| 42 | readonly implemented: boolean; | 44 | readonly implemented: boolean; |
| 43 | readonly status: "available" | "missing"; | 45 | readonly status: "available" | "missing"; |
| 44 | readonly version: Option.Option<string>; | 46 | readonly version: Option.Option<string>; |
| @@ -101,12 +103,8 @@ const SOURCE_CONTROL_PROVIDER_PROBES: ReadonlyArray<ProviderProbe> = [ · 56 unmodified lines | |||
| 101 | { | 103 | { |
| 102 | kind: "bitbucket", | 104 | kind: "bitbucket", |
| 103 | label: "Bitbucket", | 105 | label: "Bitbucket", |
| 104 | executable: "bb", | ||
| 105 | versionArgs: ["--version"], | ||
| 106 | authArgs: ["auth", "status"], | ||
| 107 | parseAuth: parseBitbucketAuth, | ||
| 108 | implemented: false, | 106 | implemented: false, |
| 109 | installHint: "Install a Bitbucket CLI (`bb`) and authenticate it for your Bitbucket workspace.", | 107 | installHint: "Bitbucket provider support is not available yet.", |
| 110 | }, | 108 | }, |
| 111 | ]; | 109 | ]; |
| 112 | 110 | ||
| @@ -266,37 +264,6 @@ function parseAzureAuth(input: AuthProbeInput): SourceControlProviderAuth { · 153 unmodified lines | |||
| 266 | }); | 264 | }); |
| 267 | } | 265 | } |
| 268 | 266 | ||
| 269 | function parseBitbucketAuth(input: AuthProbeInput): SourceControlProviderAuth { | ||
| 270 | const output = combinedAuthOutput(input); | ||
| 271 | const account = matchFirst(output, [ | ||
| 272 | /Logged in to .* as\s+([^\s(]+)/iu, | ||
| 273 | /Logged in as\s+([^\s(]+)/iu, | ||
| 274 | /account:\s*([^\s(]+)/iu, | ||
| 275 | /user:\s*([^\s(]+)/iu, | ||
| 276 | /username:\s*([^\s(]+)/iu, | ||
| 277 | ]); | ||
| 278 | const host = parseCliHost(output) ?? "bitbucket.org"; | ||
| 279 | |||
| 280 | if (input.exitCode !== 0) { | ||
| 281 | return providerAuth({ | ||
| 282 | status: "unauthenticated", | ||
| 283 | host, | ||
| 284 | detail: | ||
| 285 | firstSafeAuthLine(output) ?? "Authenticate the Bitbucket CLI before enabling Bitbucket.", | ||
| 286 | }); | ||
| 287 | } | ||
| 288 | |||
| 289 | if (account) { | ||
| 290 | return providerAuth({ status: "authenticated", account, host }); | ||
| 291 | } | ||
| 292 | |||
| 293 | return providerAuth({ | ||
| 294 | status: "unknown", | ||
| 295 | host, | ||
| 296 | detail: firstSafeAuthLine(output) ?? "Bitbucket CLI auth status could not be parsed.", | ||
| 297 | }); | ||
| 298 | } | ||
| 299 | |||
| 300 | export interface SourceControlDiscoveryShape { | 267 | export interface SourceControlDiscoveryShape { |
| 301 | readonly discover: Effect.Effect<SourceControlDiscoveryResult>; | 268 | readonly discover: Effect.Effect<SourceControlDiscoveryResult>; |
| 302 | } | 269 | } |
| @@ -314,12 +281,27 @@ export const layer = Layer.effect( · 11 unmodified lines | |||
| 314 | 281 | ||
| 315 | const probe = <Kind extends VcsDriverKind | SourceControlProviderKind>( | 282 | const probe = <Kind extends VcsDriverKind | SourceControlProviderKind>( |
| 316 | input: DiscoveryProbe & { readonly kind: Kind }, | 283 | input: DiscoveryProbe & { readonly kind: Kind }, |
| 317 | ): Effect.Effect<DiscoveryProbeResult<Kind>> => | 284 | ): Effect.Effect<DiscoveryProbeResult<Kind>> => { |
| 318 | process | 285 | const executable = input.executable; |
| 286 | const versionArgs = input.versionArgs; | ||
| 287 | |||
| 288 | if (!executable || !versionArgs) { | ||
| 289 | return Effect.succeed({ | ||
| 290 | kind: input.kind, | ||
| 291 | label: input.label, | ||
| 292 | implemented: input.implemented, | ||
| 293 | status: "missing" as const, | ||
| 294 | version: Option.none<string>(), | ||
| 295 | installHint: input.installHint, | ||
| 296 | detail: Option.some(input.installHint), | ||
| 297 | } satisfies DiscoveryProbeResult<Kind>); | ||
| 298 | } | ||
| 299 | |||
| 300 | return process | ||
| 319 | .run({ | 301 | .run({ |
| 320 | operation: "source-control.discovery.probe", | 302 | operation: "source-control.discovery.probe", |
| 321 | command: input.executable, | 303 | command: executable, |
| 322 | args: input.versionArgs, | 304 | args: versionArgs, |
| 323 | cwd: config.cwd, | 305 | cwd: config.cwd, |
| 324 | timeoutMs: 5_000, | 306 | timeoutMs: 5_000, |
| 325 | maxOutputBytes: 8_000, | 307 | maxOutputBytes: 8_000, |
| @@ -331,7 +313,7 @@ export const layer = Layer.effect( · 5 unmodified lines | |||
| 331 | ({ | 313 | ({ |
| 332 | kind: input.kind, | 314 | kind: input.kind, |
| 333 | label: input.label, | 315 | label: input.label, |
| 334 | executable: input.executable, | 316 | executable, |
| 335 | implemented: input.implemented, | 317 | implemented: input.implemented, |
| 336 | status: "available" as const, | 318 | status: "available" as const, |
| 337 | version: Option.orElse(firstNonEmptyLine(result.stdout), () => | 319 | version: Option.orElse(firstNonEmptyLine(result.stdout), () => |
| @@ -345,7 +327,7 @@ export const layer = Layer.effect( · 7 unmodified lines | |||
| 345 | Effect.succeed({ | 327 | Effect.succeed({ |
| 346 | kind: input.kind, | 328 | kind: input.kind, |
| 347 | label: input.label, | 329 | label: input.label, |
| 348 | executable: input.executable, | 330 | executable, |
| 349 | implemented: input.implemented, | 331 | implemented: input.implemented, |
| 350 | status: "missing" as const, | 332 | status: "missing" as const, |
| 351 | version: Option.none<string>(), | 333 | version: Option.none<string>(), |
| @@ -354,10 +336,22 @@ export const layer = Layer.effect( · 2 unmodified lines | |||
| 354 | } satisfies DiscoveryProbeResult<Kind>), | 336 | } satisfies DiscoveryProbeResult<Kind>), |
| 355 | ), | 337 | ), |
| 356 | ); | 338 | ); |
| 339 | }; | ||
| 357 | 340 | ||
| 358 | const probeProvider = (input: ProviderProbe) => | 341 | const probeProvider = (input: ProviderProbe) => |
| 359 | probe(input).pipe( | 342 | probe(input).pipe( |
| 360 | Effect.flatMap((item) => { | 343 | Effect.flatMap((item) => { |
| 344 | const executable = input.executable; | ||
| 345 | const authArgs = input.authArgs; | ||
| 346 | const parseAuth = input.parseAuth; | ||
| 347 | |||
| 348 | if (!executable || !authArgs || !parseAuth) { | ||
| 349 | return Effect.succeed({ | ||
| 350 | ...item, | ||
| 351 | auth: unknownAuth(input.installHint), | ||
| 352 | } satisfies SourceControlProviderDiscoveryItem); | ||
| 353 | } | ||
| 354 | |||
| 361 | if (item.status !== "available") { | 355 | if (item.status !== "available") { |
| 362 | return Effect.succeed({ | 356 | return Effect.succeed({ |
| 363 | ...item, | 357 | ...item, |
| @@ -368,8 +362,8 @@ export const layer = Layer.effect( · 4 unmodified lines | |||
| 368 | return process | 362 | return process |
| 369 | .run({ | 363 | .run({ |
| 370 | operation: "source-control.discovery.auth", | 364 | operation: "source-control.discovery.auth", |
| 371 | command: input.executable, | 365 | command: executable, |
| 372 | args: input.authArgs, | 366 | args: authArgs, |
| 373 | cwd: config.cwd, | 367 | cwd: config.cwd, |
| 374 | allowNonZeroExit: true, | 368 | allowNonZeroExit: true, |
| 375 | timeoutMs: 5_000, | 369 | timeoutMs: 5_000, |
| @@ -381,7 +375,7 @@ export const layer = Layer.effect( · 5 unmodified lines | |||
| 381 | (result) => | 375 | (result) => |
| 382 | ({ | 376 | ({ |
| 383 | ...item, | 377 | ...item, |
| 384 | auth: input.parseAuth(result), | 378 | auth: parseAuth(result), |
| 385 | }) satisfies SourceControlProviderDiscoveryItem, | 379 | }) satisfies SourceControlProviderDiscoveryItem, |
| 386 | ), | 380 | ), |
| 387 | Effect.catch((cause) => | 381 | Effect.catch((cause) => |