// @effect-diagnostics nodeBuiltinImport:off - Effect has no incremental digest or free-space query. import * as EffectNodeStream from "@effect/platform-node/NodeStream"; import { ProviderDriverKind, type ProviderInstallState } from "@t3tools/contracts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime"; import * as Clock from "effect/Clock"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Schedule from "effect/Schedule"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import * as NodeCrypto from "node:crypto"; import * as NodeFSP from "node:fs/promises"; import * as EffectAcpErrors from "effect-acp/errors"; import * as ServerConfig from "../config.ts"; import { openZipArchive } from "../zipArchive.ts"; import { makeAntigravityAcpRuntime } from "./acp/AntigravityAcpSupport.ts"; import { buildAntigravityAcpSpawnInput, prepareAntigravityProfile, } from "./antigravityAuthSupport.ts"; import { resolveAntigravityReleaseAsset, type AntigravityReleaseAsset, } from "./antigravityRelease.ts"; const DRIVER = ProviderDriverKind.make("antigravity"); const DOWNLOAD_TIMEOUT = "45 minutes"; const VALIDATION_TIMEOUT = "90 seconds"; const FREE_SPACE_MARGIN = 256 * 1024 * 1024; const RECORD_MAX_BYTES = 8 * 1024; const RELEASE_RECORD = ".install-complete.json"; const ReleaseId = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/u)); const ActiveRelease = Schema.Struct({ releaseId: ReleaseId }); const InstalledRelease = Schema.Struct({ releaseId: ReleaseId, version: Schema.String, executable: Schema.Struct({ name: Schema.String, bytes: Schema.Number }), harness: Schema.Struct({ name: Schema.String, bytes: Schema.Number }), }); type InstalledRelease = typeof InstalledRelease.Type; const encodeActiveRelease = Schema.encodeEffect(Schema.fromJsonString(ActiveRelease)); const encodeInstalledRelease = Schema.encodeEffect(Schema.fromJsonString(InstalledRelease)); export class AntigravityInstallationError extends Schema.TaggedError()( "AntigravityInstallationError", { operation: Schema.String, detail: Schema.String, cause: Schema.optional(Schema.Defect()), }, ) { override get message() { return this.detail; } } const isInstallationError = Schema.is(AntigravityInstallationError); const isAcpProcessExitedError = Schema.is(EffectAcpErrors.AcpProcessExitedError); export interface AntigravityExecutable { readonly executablePath: string; readonly harnessPath: string; readonly source: "override" | "managed" | "path"; readonly version: string | null; readonly managedVersionDirectory: string | null; } interface AntigravityInstallationService { readonly managedDirectory: string; readonly resolve: ( binaryPath?: string, environment?: NodeJS.ProcessEnv, ) => Effect.Effect; /** Hold the lease until the spawned process has exited. */ readonly acquire: ( binaryPath?: string, environment?: NodeJS.ProcessEnv, ) => Effect.Effect; readonly start: Effect.Effect; readonly cancel: ( operationId: string, ) => Effect.Effect; readonly state: Effect.Effect; readonly changes: Stream.Stream; readonly remove: ( protectedBinaryPaths?: ReadonlyArray, ) => Effect.Effect; } export class AntigravityInstallation extends Context.Service< AntigravityInstallation, AntigravityInstallationService >()("t3/provider/AntigravityInstallation") { static readonly layer = Layer.effect( AntigravityInstallation, Effect.gen(function* () { const config = yield* ServerConfig.ServerConfig; return yield* makeAntigravityInstallation({ baseDir: config.baseDir }); }), ); } export interface AntigravityInstallationOptions { readonly baseDir: string; readonly releaseAsset?: AntigravityReleaseAsset | null; readonly validate?: ( executable: AntigravityExecutable, expectedVersion: string, ) => Effect.Effect; } const installationError = (operation: string, detail: string, cause?: unknown) => new AntigravityInstallationError({ operation, detail, ...(cause === undefined ? {} : { cause }), }); const wrapFailure = (operation: string, detail: string) => (cause: unknown) => isInstallationError(cause) ? cause : installationError(operation, detail, cause); function executableNames(platform: NodeJS.Platform) { return platform === "win32" ? { executable: "agy_acp_server.exe", harness: "localharness_external.exe" } : { executable: "agy_acp_server.par", harness: "localharness_external" }; } function isRunning(state: ProviderInstallState) { return ( state.phase === "downloading" || state.phase === "extracting" || state.phase === "verifying" ); } export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.make")(function* ( options: AntigravityInstallationOptions, ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const crypto = yield* Crypto.Crypto; const http = yield* HttpClient.HttpClient; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serviceScope = yield* Effect.scope; const platform = yield* HostProcess.Platform; const arch = yield* HostProcess.Architecture; const environment = yield* HostProcess.Environment; const releaseAsset = options.releaseAsset === undefined ? resolveAntigravityReleaseAsset(platform, arch) : options.releaseAsset; const names = executableNames(platform); const managedDirectory = path.join( options.baseDir, "tools", "antigravity-acp", `${platform}-${arch}`, ); const versionsDirectory = path.join(managedDirectory, "versions"); const activePath = path.join(managedDirectory, "active.json"); const gate = yield* Semaphore.make(1); const leases = new Map(); let running: { readonly operationId: string; readonly fiber: Fiber.Fiber } | undefined; const state = yield* SubscriptionRef.make({ driver: DRIVER, operationId: null, phase: "idle", downloadedBytes: 0, totalBytes: releaseAsset?.archiveBytes ?? null, version: releaseAsset?.version ?? null, installedVersion: null, canRemove: false, message: null, }); const readRecord = Effect.fn("AntigravityInstallation.readRecord")(function* ( filePath: string, schema: Schema.Codec, ) { const info = yield* fs.stat(filePath); if (info.type !== "File" || Number(info.size) > RECORD_MAX_BYTES) { return yield* installationError( "resolve", "The managed runtime record is invalid. Reinstall Antigravity.", ); } const contents = yield* fs.readFileString(filePath); return yield* Schema.decodeEffect(Schema.fromJsonString(schema))(contents); }); const executableFile = Effect.fn("AntigravityInstallation.executableFile")(function* ( filePath: string, bytes?: number, ) { const info = yield* fs.stat(filePath).pipe(Effect.option); return ( Option.isSome(info) && info.value.type === "File" && (bytes === undefined || Number(info.value.size) === bytes) && (platform === "win32" || (info.value.mode & 0o111) !== 0) ); }); const completedRelease = Effect.fn("AntigravityInstallation.completedRelease")(function* ( releaseId: string, ) { const directory = path.join(versionsDirectory, releaseId); const record = yield* readRecord(path.join(directory, RELEASE_RECORD), InstalledRelease); if ( record.releaseId !== releaseId || record.executable.name !== names.executable || record.harness.name !== names.harness || !Number.isSafeInteger(record.executable.bytes) || record.executable.bytes <= 0 || !Number.isSafeInteger(record.harness.bytes) || record.harness.bytes <= 0 || !record.version.trim() || !(yield* executableFile(path.join(directory, names.executable), record.executable.bytes)) || !(yield* executableFile(path.join(directory, names.harness), record.harness.bytes)) ) { return yield* installationError( "resolve", "The managed Antigravity runtime is incomplete. Reinstall it.", ); } return { executablePath: path.join(directory, names.executable), harnessPath: path.join(directory, names.harness), source: "managed", version: record.version, managedVersionDirectory: directory, } satisfies AntigravityExecutable; }); const fromExternal = Effect.fn("AntigravityInstallation.fromExternal")(function* ( candidate: string, source: "override" | "path", ) { if (!(yield* executableFile(candidate))) return null; const executablePath = yield* fs.realPath(candidate); const directory = path.dirname(executablePath); const harnessPath = path.join(directory, names.harness); if (!(yield* executableFile(harnessPath))) return null; const realVersions = yield* fs.realPath(versionsDirectory).pipe(Effect.option); if ( Option.isSome(realVersions) && path.dirname(directory) === realVersions.value && /^[a-f0-9]{64}$/u.test(path.basename(directory)) ) { const installed = yield* completedRelease(path.basename(directory)); return { ...installed, executablePath, harnessPath, source } satisfies AntigravityExecutable; } return { executablePath, harnessPath, source, version: null, managedVersionDirectory: null, } satisfies AntigravityExecutable; }); const pathCandidates = (binary: string, processEnvironment = environment) => { const pathValue = platform === "win32" ? Object.entries(processEnvironment).findLast(([key]) => key.toUpperCase() === "PATH")?.[1] : processEnvironment.PATH; return (pathValue ?? "") .split(platform === "win32" ? ";" : ":") .map((directory) => directory.trim().replace(/^"|"$/gu, "")) .filter((directory) => directory.length > 0) .map((directory) => path.resolve(directory, binary)); }; const resolve: AntigravityInstallationService["resolve"] = Effect.fn( "AntigravityInstallation.resolve", )( function* (binaryPath?: string, processEnvironment?: NodeJS.ProcessEnv) { const override = binaryPath?.trim(); if (override) { const candidates = path.isAbsolute(override) || override.includes("/") || override.includes("\\") ? [path.resolve(override)] : pathCandidates(override, processEnvironment); for (const candidate of candidates) { const selected = yield* fromExternal(candidate, "override"); if (selected) return selected; } return yield* installationError( "resolve", "The custom Antigravity executable or its localharness_external sibling is missing or not executable.", ); } if (yield* fs.exists(activePath)) { const active = yield* readRecord(activePath, ActiveRelease); return yield* completedRelease(active.releaseId); } for (const candidate of pathCandidates(names.executable, processEnvironment)) { const selected = yield* fromExternal(candidate, "path"); if (selected) return selected; } return yield* installationError( "resolve", releaseAsset ? "Antigravity ACP runtime is not installed. Install it in this environment's provider settings or set a custom ACP executable path. The Antigravity app, IDE, and CLI are separate installations." : `Google does not publish an Antigravity runtime for ${platform}-${arch}. Use a supported environment or a custom executable.`, ); }, Effect.mapError( wrapFailure( "resolve", "Could not read the Antigravity installation. Reinstall it or set a custom executable path.", ), ), ); const acquire = (binaryPath?: string, processEnvironment?: NodeJS.ProcessEnv) => Effect.acquireRelease( gate.withPermit( Effect.gen(function* () { const executable = yield* resolve(binaryPath, processEnvironment); const directory = executable.managedVersionDirectory; if (directory) leases.set(directory, (leases.get(directory) ?? 0) + 1); return executable; }), ), (executable) => gate.withPermit( Effect.sync(() => { const directory = executable.managedVersionDirectory; if (!directory) return; const remaining = (leases.get(directory) ?? 1) - 1; if (remaining > 0) leases.set(directory, remaining); else leases.delete(directory); }), ), ); const validate = options.validate ?? Effect.fn("AntigravityInstallation.validate")( function* (executable: AntigravityExecutable, expectedVersion: string) { const validationScope = yield* Effect.scope; const profileDirectory = yield* Effect.acquireRelease( fs.makeTempDirectory({ prefix: "t3-antigravity-validate-" }), (directory) => fs.remove(directory, { recursive: true, force: true }).pipe( // Windows may retain DLL handles briefly after taskkill returns. Effect.retry({ times: platform === "win32" ? 10 : 0, schedule: Schedule.spaced("100 millis"), while: (error) => error.reason._tag === "PermissionDenied" || error.reason._tag === "Busy" || error.reason._tag === "Unknown", }), Effect.orDie, ), ); const profile = yield* prepareAntigravityProfile({ profileDirectory, platform, baseEnv: environment, // A shallow scoped profile cleans up unpacked files within Windows' path limit. tempDirectory: profileDirectory, }); const runtime = yield* makeAntigravityAcpRuntime({ runtimeVersion: expectedVersion, spawn: buildAntigravityAcpSpawnInput({ installation: executable, profile, cwd: profileDirectory, baseEnv: environment, }), cwd: profileDirectory, childProcessSpawner: platform === "win32" ? ChildProcessSpawner.make( Effect.fnUntraced(function* (command) { const child = yield* spawner.spawn(command); // Stop PyInstaller's tree before ACP closes pipes; after launcher exit, taskkill cannot reach children holding DLLs open. yield* Scope.addFinalizer( validationScope, child.kill({ forceKillAfter: "1 second" }).pipe(Effect.orDie), ); return child; }), ) : spawner, }); const initialized = yield* runtime.initialize(); if ( initialized.agentInfo?.name !== "antigravity-acp" || initialized.agentInfo.version !== expectedVersion || // Antigravity 1.1.1 may report protocol 2 with legacy ACP; choose wire format from the response shape. (initialized.protocolVersion !== 1 && initialized.protocolVersion !== 2) || initialized.agentCapabilities?.loadSession !== true || !initialized.agentCapabilities.sessionCapabilities?.resume || !initialized.agentCapabilities.auth?.logout || !initialized.authMethods?.some((method) => method.id === "oauth-personal") ) { return yield* installationError( "verify", "The downloaded runtime did not identify as the expected Google Antigravity release.", ); } }, Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(Crypto.Crypto, crypto), Effect.mapError((cause) => { if (isInstallationError(cause)) return cause; const detail = isAcpProcessExitedError(cause) && /\b(?:AF_INET6|enforce_kernel_ipv6_support)\b/.test(cause.stderr ?? "") ? "Antigravity requires IPv6 socket support. Enable IPv6 in this environment. On WSL, remove ipv6.disable=1 from .wslconfig and run wsl --shutdown." : "The downloaded Antigravity runtime could not start in this environment."; return installationError("verify", detail, cause); }), ); const install = Effect.fn("AntigravityInstallation.install")( function* (asset: AntigravityReleaseAsset) { yield* resolveNodeExecutable("Antigravity", environment).pipe( Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path), Effect.provideService(HostProcess.Platform, platform), Effect.mapError((cause) => installationError("verify", nodeRuntimeUnavailableMessage("Antigravity"), cause), ), ); const report = (phase: ProviderInstallState["phase"], message: string | null) => SubscriptionRef.update(state, (current) => ({ ...current, phase, message })); yield* fs.makeDirectory(versionsDirectory, { recursive: true }); yield* SubscriptionRef.update(state, (current) => ({ ...current, canRemove: true })); const destination = path.join(versionsDirectory, asset.sha256); const activate = Effect.fn("AntigravityInstallation.activate")( function* () { const pointerDirectory = yield* fs.makeTempDirectoryScoped({ directory: managedDirectory, prefix: "active.json.", }); const pointerPath = path.join(pointerDirectory, "contents.tmp"); yield* fs.writeFileString( pointerPath, yield* encodeActiveRelease({ releaseId: asset.sha256 }), { flag: "wx", mode: 0o600 }, ); yield* fs.rename(pointerPath, activePath); yield* SubscriptionRef.update( state, (current) => ({ ...current, phase: "succeeded", installedVersion: asset.version, message: null, }) satisfies ProviderInstallState, ); }, Effect.scoped, Effect.mapError( wrapFailure( "activate", "Could not activate Antigravity. The previous runtime is unchanged. Check for locked files and try again.", ), ), Effect.uninterruptible, ); if (yield* fs.exists(destination)) { const existing = yield* completedRelease(asset.sha256); if (existing.version !== asset.version) { return yield* installationError( "verify", "The existing managed release has the wrong version. Remove it before reinstalling.", ); } yield* report("verifying", "Checking the installed runtime."); yield* validate(existing, asset.version).pipe( Effect.scoped, Effect.timeout(VALIDATION_TIMEOUT), ); yield* activate(); return; } const available = yield* Effect.tryPromise(() => NodeFSP.statfs(versionsDirectory, { bigint: true }), ).pipe(Effect.option); const required = asset.archiveBytes + asset.executable.bytes + asset.harness.bytes + FREE_SPACE_MARGIN; if ( Option.isSome(available) && available.value.bavail * available.value.bsize < BigInt(required) ) { return yield* installationError( "download", `Antigravity needs at least ${Math.ceil(required / 1024 / 1024)} MiB of free space to install.`, ); } const staging = yield* fs.makeTempDirectoryScoped({ directory: versionsDirectory, prefix: ".install-", }); const archivePath = path.join(staging, "download.zip"); const pairDirectory = path.join(staging, "runtime"); yield* fs.makeDirectory(pairDirectory); const hash = NodeCrypto.createHash("sha256"); let downloadedBytes = 0; let lastProgressAt = yield* Clock.currentTimeMillis; yield* Effect.gen(function* () { const response = yield* http .execute(HttpClientRequest.get(asset.url)) .pipe(Effect.flatMap(HttpClientResponse.filterStatusOk)); // dl.google.com may gzip the zip: content-length is encoded, while byte/hash checks use decoded data. const contentLength = response.headers["content-length"]; const contentEncoding = response.headers["content-encoding"]?.trim().toLowerCase(); const identityBody = contentEncoding === undefined || contentEncoding === "identity"; if ( identityBody && contentLength !== undefined && Number(contentLength) !== asset.archiveBytes ) { return yield* installationError( "download", "The Antigravity download size did not match the pinned release.", ); } yield* response.stream.pipe( Stream.tap((chunk) => Effect.gen(function* () { downloadedBytes += chunk.byteLength; if (downloadedBytes > asset.archiveBytes) { return yield* installationError( "download", "The Antigravity download exceeded the pinned release size.", ); } hash.update(chunk); const now = yield* Clock.currentTimeMillis; if (now - lastProgressAt >= 250 || downloadedBytes === asset.archiveBytes) { lastProgressAt = now; yield* SubscriptionRef.update(state, (current) => ({ ...current, downloadedBytes, })); } }), ), Stream.run(fs.sink(archivePath, { flag: "wx", mode: 0o600 })), ); }).pipe(Effect.timeout(DOWNLOAD_TIMEOUT)); if (downloadedBytes !== asset.archiveBytes || hash.digest("hex") !== asset.sha256) { return yield* installationError( "download", "The Antigravity download failed its size or SHA-256 check. Nothing was installed.", ); } yield* report("extracting", "Extracting the verified runtime."); yield* Effect.gen(function* () { const archive = yield* openZipArchive(archivePath, (detail, cause) => installationError("extract", detail, cause), ); if (archive.entryCount !== 2) { return yield* installationError( "extract", "The archive must contain exactly the Antigravity executable and its harness.", ); } const seen = new Set(); for (;;) { const entry = yield* archive.next; if (!entry) break; const expected = [asset.executable, asset.harness].find( (file) => file.name === entry.fileName, ); const unixType = (entry.externalFileAttributes >>> 16) & 0o170000; if ( !expected || seen.has(entry.fileName) || entry.fileName.includes("/") || entry.fileName.includes("\\") || (unixType !== 0 && unixType !== 0o100000) || (entry.externalFileAttributes & 0x10) !== 0 || (entry.generalPurposeBitFlag & 1) !== 0 || ![0, 8].includes(entry.compressionMethod) || entry.uncompressedSize !== expected.bytes ) { return yield* installationError( "extract", "The archive contains an unexpected, unsafe, or incorrectly sized member.", ); } seen.add(entry.fileName); yield* Effect.gen(function* () { const readable = yield* archive.streamEntry(entry); let extractedBytes = 0; yield* EffectNodeStream.fromReadable({ evaluate: () => readable, onError: wrapFailure("extract", "Could not extract the Antigravity runtime."), }).pipe( Stream.tap((chunk) => Effect.gen(function* () { extractedBytes += chunk.byteLength; if (extractedBytes > expected.bytes) { return yield* installationError( "extract", "An archive member exceeded its pinned size.", ); } }), ), Stream.run( fs.sink(path.join(pairDirectory, entry.fileName), { flag: "wx", mode: 0o700 }), ), ); if (extractedBytes !== expected.bytes) { return yield* installationError("extract", "An archive member was truncated."); } }).pipe(Effect.scoped); } if (!seen.has(asset.executable.name) || !seen.has(asset.harness.name)) { return yield* installationError( "extract", "The archive is missing the Antigravity executable or its harness.", ); } }).pipe(Effect.scoped); yield* fs.remove(archivePath); if (platform !== "win32") { yield* fs.chmod(path.join(pairDirectory, asset.executable.name), 0o755); yield* fs.chmod(path.join(pairDirectory, asset.harness.name), 0o755); } yield* report("verifying", "Checking the downloaded runtime."); yield* validate( { executablePath: path.join(pairDirectory, asset.executable.name), harnessPath: path.join(pairDirectory, asset.harness.name), source: "managed", version: asset.version, managedVersionDirectory: pairDirectory, }, asset.version, ).pipe(Effect.scoped, Effect.timeout(VALIDATION_TIMEOUT)); const record: InstalledRelease = { releaseId: asset.sha256, version: asset.version, executable: asset.executable, harness: asset.harness, }; yield* fs.writeFileString( path.join(pairDirectory, RELEASE_RECORD), yield* encodeInstalledRelease(record), { flag: "wx", mode: 0o600 }, ); yield* fs.rename(pairDirectory, destination).pipe( Effect.catch((cause) => completedRelease(asset.sha256).pipe( Effect.flatMap((existing) => existing.version === asset.version ? validate(existing, asset.version).pipe( Effect.scoped, Effect.timeout(VALIDATION_TIMEOUT), ) : Effect.fail( installationError( "activate", "Another installation published a different Antigravity release.", ), ), ), Effect.mapError(() => installationError( "activate", "Could not publish the Antigravity runtime. The previous release is unchanged. Try again.", cause, ), ), ), ), ); yield* activate(); }, Effect.scoped, Effect.mapError( wrapFailure( "install", "Could not install Antigravity. Check free disk space and directory access, then try again.", ), ), ); const start = gate .withPermit( Effect.gen(function* () { const current = yield* SubscriptionRef.get(state); if (isRunning(current)) return current; if (!releaseAsset) { return yield* installationError( "start", `Google does not publish an Antigravity runtime for ${platform}-${arch}. Use a supported remote environment or a custom executable.`, ); } const operationId = yield* crypto.randomUUIDv4(); const next: ProviderInstallState = { driver: DRIVER, operationId, phase: "downloading", downloadedBytes: 0, totalBytes: releaseAsset.archiveBytes, version: releaseAsset.version, installedVersion: current.installedVersion, canRemove: current.canRemove, message: "Downloading Google's official Antigravity runtime.", }; yield* SubscriptionRef.set(state, next); const work = install(releaseAsset).pipe( Effect.onExit((exit) => Exit.isFailure(exit) ? SubscriptionRef.update(state, (value) => { if (value.operationId !== operationId || value.phase === "succeeded") return value; const error = Cause.findErrorOption(exit.cause); const cancelled = Cause.hasInterruptsOnly(exit.cause); return { ...value, phase: cancelled ? "cancelled" : "failed", message: cancelled ? "Installation cancelled. The previous runtime is unchanged." : Option.isSome(error) ? error.value.detail : "Could not finish the Antigravity installation. Check disk space and directory access.", } satisfies ProviderInstallState; }) : Effect.void, ), Effect.ignoreCause, Effect.ensuring( Effect.sync(() => { if (running?.operationId === operationId) running = undefined; }), ), ); const fiber = yield* Effect.forkIn(Effect.interruptible(work), serviceScope); running = { operationId, fiber }; return next; }).pipe(Effect.uninterruptible), ) .pipe(Effect.mapError(wrapFailure("start", "Could not start the Antigravity installation."))); const cancel = Effect.fn("AntigravityInstallation.cancel")(function* (operationId: string) { return yield* gate.withPermit( Effect.gen(function* () { const current = yield* SubscriptionRef.get(state); if (current.operationId !== operationId) { return yield* installationError( "cancel", "This installation is no longer current. Refresh its status before cancelling.", ); } if (running?.operationId === operationId && isRunning(current)) { yield* Fiber.interrupt(running.fiber); } return yield* SubscriptionRef.get(state); }), ); }); const remove = Effect.fn("AntigravityInstallation.remove")( function* (protectedBinaryPaths: ReadonlyArray = []) { yield* gate.withPermit( Effect.gen(function* () { if (isRunning(yield* SubscriptionRef.get(state)) || leases.size > 0) { return yield* installationError( "remove", "Stop Antigravity sessions and sign-in flows before removing its managed runtime.", ); } const realManaged = yield* fs.realPath(managedDirectory).pipe(Effect.option); if (Option.isSome(realManaged)) { for (const binaryPath of protectedBinaryPaths) { if (!binaryPath.trim()) continue; const selected = yield* resolve(binaryPath).pipe(Effect.option); if (Option.isSome(selected) && selected.value.managedVersionDirectory) { return yield* installationError( "remove", "A provider instance has a custom path inside this managed runtime. Clear that path before removing it.", ); } const resolved = yield* fs.realPath(binaryPath).pipe(Effect.option); const candidate = Option.getOrElse(resolved, () => path.resolve(binaryPath)); if (candidate.startsWith(`${realManaged.value}${path.sep}`)) { return yield* installationError( "remove", "A provider instance has a custom path inside this managed runtime. Clear that path before removing it.", ); } } } yield* fs.remove(managedDirectory, { recursive: true, force: true }); yield* SubscriptionRef.update( state, (current) => ({ ...current, operationId: null, phase: "idle", downloadedBytes: 0, installedVersion: null, canRemove: false, message: null, }) satisfies ProviderInstallState, ); }).pipe(Effect.uninterruptible), ); }, Effect.mapError( wrapFailure( "remove", "Could not remove the managed Antigravity runtime. Check for open processes and try again.", ), ), ); yield* Effect.gen(function* () { const canRemove = yield* fs.exists(managedDirectory); yield* SubscriptionRef.update(state, (current) => ({ ...current, canRemove })); if (!(yield* fs.exists(activePath))) return; const active = yield* readRecord(activePath, ActiveRelease); const installed = yield* completedRelease(active.releaseId); yield* SubscriptionRef.update(state, (current) => ({ ...current, installedVersion: installed.version, })); }).pipe( Effect.catch(() => SubscriptionRef.update( state, (current) => ({ ...current, phase: "failed", message: "The managed Antigravity runtime is incomplete. Remove it and reinstall.", }) satisfies ProviderInstallState, ), ), ); return AntigravityInstallation.of({ managedDirectory, resolve, acquire, start, cancel, state: SubscriptionRef.get(state), changes: SubscriptionRef.changes(state), remove, }); });