Skip to content
Source
Sign inSign up

providerInstallation.ts

246 lines9.3 KB

1import {
2 AntigravitySettings,
3 CodexSettings,
4 ProviderDriverKind,
5 type ProviderInstallCancelInput,
6 type ProviderInstallState,
7 type ProviderInstanceId,
8 ProviderSetupError,
9 type ProviderSetupInput,
10} from "@t3tools/contracts";
11import { resolveCommandPath } from "@t3tools/shared/shell";
12import * as Context from "effect/Context";
13import * as Effect from "effect/Effect";
14import * as Layer from "effect/Layer";
15import * as FileSystem from "effect/FileSystem";
16import * as Path from "effect/Path";
17import * as Option from "effect/Option";
18import * as Schema from "effect/Schema";
19import * as Stream from "effect/Stream";
20​
21import * as CodexInstallation from "./CodexInstallation.ts";
22import * as ServerSettings from "../serverSettings.ts";
23import * as AntigravityInstallation from "./AntigravityInstallation.ts";
24import { deriveProviderInstanceConfigMap } from "./ProviderInstanceRegistryHydration.ts";
25import * as ProviderInstanceRegistry from "./ProviderInstanceRegistry.ts";
26import * as ProviderRegistry from "./ProviderRegistry.ts";
27import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
28​
29const ANTIGRAVITY = ProviderDriverKind.make("antigravity");
30const hasBinaryPath = Schema.is(Schema.Struct({ binaryPath: Schema.String }));
31const decodeCodexSettings = Schema.decodeUnknownEffect(CodexSettings);
32const decodeAntigravitySettings = Schema.decodeUnknownEffect(AntigravitySettings);
33​
34export class ProviderInstallation extends Context.Service<
35 ProviderInstallation,
36 {
37 readonly start: (
38 input: ProviderSetupInput,
39 ) => Effect.Effect<ProviderInstallState, ProviderSetupError>;
40 readonly cancel: (
41 input: ProviderInstallCancelInput,
42 ) => Effect.Effect<ProviderInstallState, ProviderSetupError>;
43 readonly subscribe: (
44 input: ProviderSetupInput,
45 ) => Stream.Stream<ProviderInstallState, ProviderSetupError>;
46 readonly remove: (
47 input: ProviderSetupInput,
48 ) => Effect.Effect<ProviderInstallState, ProviderSetupError>;
49 }
50>()("t3/provider/providerInstallation") {}
51​
52const make = Effect.fn("makeProviderInstallation")(function* () {
53 const antigravityInstallation = yield* AntigravityInstallation.AntigravityInstallation;
54 const codexInstallation = yield* CodexInstallation.CodexInstallation;
55 const instances = yield* ProviderInstanceRegistry.ProviderInstanceRegistry;
56 const providers = yield* ProviderRegistry.ProviderRegistry;
57 const settings = yield* ServerSettings.ServerSettingsService;
58 const fileSystem = yield* FileSystem.FileSystem;
59 const path = yield* Path.Path;
60 const scope = yield* Effect.scope;
61 const accountCheckOperations = new Map<ProviderInstanceId, string>();
62​
63 const readEntries = Effect.fn("ProviderInstallation.readEntries")(function* (
64 instanceId: ProviderInstanceId,
65 operation: string,
66 ) {
67 const current = yield* settings.getSettings.pipe(
68 Effect.mapError(
69 () =>
70 new ProviderSetupError({
71 instanceId,
72 operation,
73 detail: "Could not read provider installation settings.",
74 }),
75 ),
76 );
77 return deriveProviderInstanceConfigMap(current);
78 });
79​
80 const requireInstance = Effect.fn("ProviderInstallation.requireInstance")(function* (
81 instanceId: ProviderInstanceId,
82 operation: string,
83 managedOnly = false,
84 ) {
85 const instance = yield* instances.getInstance(instanceId);
86 const isCodex = instance?.driverKind === ProviderDriverKind.make("codex");
87 if (instance?.driverKind !== ANTIGRAVITY && !isCodex) {
88 return yield* new ProviderSetupError({
89 instanceId,
90 operation,
91 detail: "Managed installation is not available for this provider instance.",
92 });
93 }
94 const installation = isCodex ? codexInstallation : antigravityInstallation;
95 const entries = yield* readEntries(instanceId, operation);
96 const invalidConfig = () =>
97 new ProviderSetupError({
98 instanceId,
99 operation,
100 detail: "The provider instance configuration is invalid.",
101 });
102 const config = isCodex
103 ? yield* decodeCodexSettings(entries[instanceId]?.config ?? {}).pipe(
104 Effect.mapError(invalidConfig),
105 )
106 : yield* decodeAntigravitySettings(entries[instanceId]?.config ?? {}).pipe(
107 Effect.mapError(invalidConfig),
108 );
109 if (isCodex && (!("setupMode" in config) || config.setupMode !== "managed")) {
110 return yield* new ProviderSetupError({
111 instanceId,
112 operation,
113 detail: "Choose managed setup to install Codex in T3 Code.",
114 });
115 }
116 if (managedOnly && config.binaryPath && (!isCodex || config.binaryPath !== "codex")) {
117 return yield* new ProviderSetupError({
118 instanceId,
119 operation,
120 detail:
121 "This instance uses a custom executable. Clear its binary path to manage installation in T3 Code.",
122 });
123 }
124 return { installation, driver: instance.driverKind };
125 });
126​
127 const failure =
128 (instanceId: ProviderInstanceId) =>
129 (
130 error:
131 | AntigravityInstallation.AntigravityInstallationError
132 | CodexInstallation.CodexInstallationError,
133 ) =>
134 new ProviderSetupError({ instanceId, operation: error.operation, detail: error.detail });
135​
136 const start = Effect.fn("ProviderInstallation.start")(function* (input: ProviderSetupInput) {
137 const { installation, driver } = yield* requireInstance(input.instanceId, "install", true);
138 const started = yield* installation.start.pipe(Effect.mapError(failure(input.instanceId)));
139 if (
140 driver === ANTIGRAVITY &&
141 started.operationId !== null &&
142 accountCheckOperations.get(input.instanceId) !== started.operationId
143 ) {
144 accountCheckOperations.set(input.instanceId, started.operationId);
145 // Installation outlives the request and may finish while every client is disconnected.
146 yield* installation.changes.pipe(
147 Stream.filter(
148 (state) =>
149 state.operationId !== started.operationId ||
150 state.phase === "succeeded" ||
151 state.phase === "failed" ||
152 state.phase === "cancelled",
153 ),
154 Stream.runHead,
155 Effect.flatMap(
156 Effect.fnUntraced(function* (result) {
157 if (
158 Option.isNone(result) ||
159 result.value.operationId !== started.operationId ||
160 result.value.phase !== "succeeded"
161 )
162 return;
163 const instance = yield* instances.getInstance(input.instanceId);
164 if (
165 instance?.driverKind !== ANTIGRAVITY ||
166 !instance.enabled ||
167 !instance.refreshModels
168 )
169 return;
170 yield* instance.refreshModels().pipe(Effect.ignore({ log: true }));
171 yield* providers.refreshInstance(input.instanceId);
172 }),
173 ),
174 Effect.forkIn(scope),
175 );
176 }
177 return started;
178 });
179​
180 const cancel = Effect.fn("ProviderInstallation.cancel")(function* (
181 input: ProviderInstallCancelInput,
182 ) {
183 const { installation } = yield* requireInstance(input.instanceId, "cancel-install");
184 return yield* installation
185 .cancel(input.operationId)
186 .pipe(Effect.mapError(failure(input.instanceId)));
187 });
188​
189 // Watching an installation is when its owner can act on an update, so the
190 // lookup runs then, in the background, unless update checks are off.
191 const subscribe = (input: ProviderSetupInput) =>
192 Stream.unwrap(
193 requireInstance(input.instanceId, "observe-install").pipe(
194 Effect.tap(({ installation }) =>
195 settings.getSettings.pipe(
196 Effect.flatMap((current) =>
197 current.enableProviderUpdateChecks
198 ? Effect.forkDetach(installation.checkForUpdate)
199 : Effect.void,
200 ),
201 Effect.ignore,
202 ),
203 ),
204 Effect.map(({ installation }) => installation.changes),
205 ),
206 );
207​
208 const remove = Effect.fn("ProviderInstallation.remove")(function* (input: ProviderSetupInput) {
209 const { installation, driver } = yield* requireInstance(
210 input.instanceId,
211 "remove-install",
212 true,
213 );
214 const entries = yield* readEntries(input.instanceId, "remove-install");
215 const protectedPaths = yield* Effect.forEach(
216 Object.values(entries),
217 Effect.fnUntraced(function* (entry) {
218 if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) {
219 return [];
220 }
221 const binaryPath = entry.config.binaryPath.trim();
222 const resolved = yield* resolveCommandPath(binaryPath, {
223 env: yield* mergeProviderInstanceEnvironment(entry.environment),
224 }).pipe(
225 Effect.provideService(FileSystem.FileSystem, fileSystem),
226 Effect.provideService(Path.Path, path),
227 );
228 return resolved === null ? [binaryPath] : [binaryPath, resolved];
229 }),
230 );
231 yield* installation
232 .remove(protectedPaths.flat())
233 .pipe(Effect.mapError(failure(input.instanceId)));
234 const allInstances = yield* instances.listInstances;
235 yield* Effect.forEach(
236 allInstances.filter((instance) => instance.driverKind === driver),
237 (instance) => providers.refreshInstance(instance.instanceId),
238 { discard: true },
239 );
240 return yield* installation.state;
241 });
242​
243 return { start, cancel, subscribe, remove };
244});
245​
246export const layer = Layer.effect(ProviderInstallation, make());