1import * as ConfigProvider from "effect/ConfigProvider";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import type { EnvironmentVariable } from "../../Auth/AuthProvider.ts";
5import { getEnv } from "../../Auth/Env.ts";
6import { resolveProfileName } from "../../Auth/Resolve.ts";
7import { loadConfigProvider } from "../../Util/ConfigProvider.ts";
8import { AlchemistInvalidInput } from "../Errors.ts";
9import {
10 collectAuthProviders,
11 DEFAULT_ENTRYPOINT,
12 type Target,
13} from "../Session.ts";
14
15export interface CheckEnvironmentInput extends Target {
16 /** Providers to check. Omitted means every registered provider. */
17 readonly providers?: ReadonlyArray<string>;
18}
19
20export interface EnvironmentCheck {
21 readonly provider: string;
22 readonly status: "satisfied" | "missing" | "no-contract";
23 readonly missing: ReadonlyArray<{
24 readonly alternatives: ReadonlyArray<string>;
25 }>;
26}
27
28export interface EnvironmentCheckResult {
29 readonly checks: ReadonlyArray<EnvironmentCheck>;
30 readonly satisfied: boolean;
31}
32
33const satisfied = (variable: EnvironmentVariable) =>
34 Effect.gen(function* () {
35 for (const name of [variable.name, ...(variable.alternatives ?? [])]) {
36 const value = yield* getEnv(name);
37 if (value !== undefined && value.length > 0) return true;
38 }
39 return false;
40 });
41
42/**
43 * Verify the environment variables each registered provider's CI contract
44 * requires are present.
45 */
46export const checkEnvironment = Effect.fn(
47 "Alchemist.provider.checkEnvironment",
48)(function* (input: CheckEnvironmentInput) {
49 const profile = yield* resolveProfileName(
50 Option.fromNullishOr(input.envFile),
51 input.profile,
52 );
53 const registry = yield* collectAuthProviders({
54 main: input.entrypoint ?? DEFAULT_ENTRYPOINT,
55 envFile: Option.fromNullishOr(input.envFile),
56 profile,
57 });
58 const known = Object.keys(registry).sort();
59 const names = yield* input.providers?.length
60 ? Effect.forEach(input.providers, (requested) => {
61 const name = known.find(
62 (candidate) => candidate.toLowerCase() === requested.toLowerCase(),
63 );
64 return name === undefined
65 ? Effect.fail(
66 new AlchemistInvalidInput({
67 field: "providers",
68 message: `Unknown provider '${requested}'. Registered: ${known.join(", ")}.`,
69 }),
70 )
71 : Effect.succeed(name);
72 })
73 : Effect.succeed(known);
74 const checks = yield* Effect.forEach(names, (name) =>
75 Effect.gen(function* () {
76 const contract = registry[name]!.environment;
77 const missing: Array<{ alternatives: ReadonlyArray<string> }> = [];
78 for (const variable of contract) {
79 if (variable.required && !(yield* satisfied(variable))) {
80 missing.push({
81 alternatives: [variable.name, ...(variable.alternatives ?? [])],
82 });
83 }
84 }
85 return {
86 provider: name,
87 status:
88 contract.length === 0
89 ? ("no-contract" as const)
90 : missing.length === 0
91 ? ("satisfied" as const)
92 : ("missing" as const),
93 missing,
94 } satisfies EnvironmentCheck;
95 }),
96 ).pipe(
97 Effect.provide(
98 ConfigProvider.layer(
99 yield* loadConfigProvider(Option.fromNullishOr(input.envFile)),
100 ),
101 ),
102 );
103 return {
104 checks,
105 satisfied: checks.every((check) => check.status !== "missing"),
106 } satisfies EnvironmentCheckResult;
107});