1// @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's symlink has no type argument, and Windows needs a junction to link without elevation.
2import * as NodeFSP from "node:fs/promises";
3
4import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts";
5import * as HostProcess from "@t3tools/shared/HostProcess";
6import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime";
7import * as Crypto from "effect/Crypto";
8import * as Effect from "effect/Effect";
9import * as Hex from "effect/encoding/Hex";
10import * as FileSystem from "effect/FileSystem";
11import * as Path from "effect/Path";
12import type * as PlatformError from "effect/PlatformError";
13import * as Schema from "effect/Schema";
14import * as Stream from "effect/Stream";
15import * as ChildProcess from "effect/process/ChildProcess";
16import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
17import * as AcpErrors from "effect-acp/errors";
18
19import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText";
20import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
21import {
22 antigravityUserSkillDirectories,
23 resolveAntigravityUserHome,
24} from "./Drivers/AntigravitySkills.ts";
25
26export const ANTIGRAVITY_AUTH_STDOUT_PREFIX =
27 "Open the following link to authenticate the ACP server: ";
28export const ANTIGRAVITY_AUTH_BROWSER_MARKER = "__T3_ANTIGRAVITY_AUTH_URL__";
29export const ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE =
30 "Sign in to Antigravity in Settings before you continue.";
31
32const maxAuthorizationUrlLength = 16_384;
33const maxBrowserHelperLineLength =
34 Math.max(ANTIGRAVITY_AUTH_BROWSER_MARKER.length, ANTIGRAVITY_AUTH_STDOUT_PREFIX.length) +
35 maxAuthorizationUrlLength +
36 2;
37const maxStdoutLineBytes = 16 * 1024 * 1024;
38const authPrefixBytes = new TextEncoder().encode(ANTIGRAVITY_AUTH_STDOUT_PREFIX);
39const decodeUrl = Schema.decodeUnknownEffect(Schema.URLFromString);
40const decodeBrowserHelperUrl = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String));
41const ProfileSettingsFile = Schema.Struct({
42 auth: Schema.Struct({ type: Schema.String }),
43 gcp: Schema.optional(
44 Schema.Struct({
45 project: Schema.optional(Schema.String),
46 location: Schema.optional(Schema.String),
47 }),
48 ),
49});
50const encodeProfileSettings = Schema.encodeSync(Schema.fromJsonString(ProfileSettingsFile));
51const isAcpRequestError = Schema.is(AcpErrors.AcpRequestError);
52const isAcpTransportError = Schema.is(AcpErrors.AcpTransportError);
53
54// Python splits BROWSER on the platform path separator before it parses quotes.
55// Keep this source free of both colons and semicolons. EPIPE must still exit 0
56// so Python does not fall back to an OS browser after cancellation.
57const browserHelperSource =
58 `process.stderr.on("error",()=>process.exit(0)).write(` +
59 `"${ANTIGRAVITY_AUTH_BROWSER_MARKER}"+JSON.stringify(process.argv[1])+"\\n",` +
60 `()=>process.exit(0))`;
61const browserPreflightUrl = "https://example.invalid/t3-antigravity-browser-preflight";
62
63const removedEnvironmentKeys = new Set([
64 "GEMINI_API_KEY",
65 "GOOGLE_API_KEY",
66 "GOOGLE_APPLICATION_CREDENTIALS",
67 "GOOGLE_CLOUD_PROJECT",
68 "GOOGLE_CLOUD_LOCATION",
69 "GOOGLE_CLOUD_QUOTA_PROJECT",
70 "GOOGLE_GENAI_USE_VERTEXAI",
71 "GCLOUD_PROJECT",
72 "CLOUDSDK_CORE_PROJECT",
73 "AGY_ACP_CCPA_PROJECT",
74 "AGY_ACP_ENABLE_OAUTH",
75 "GEMINI_HOME",
76 "AGY_ACP_FORCE_FILE_STORAGE",
77 "ANTIGRAVITY_HARNESS_PATH",
78 "BROWSER",
79 "PYTHONUNBUFFERED",
80 "ELECTRON_RUN_AS_NODE",
81]);
82
83export interface AntigravityProfile {
84 readonly platform: NodeJS.Platform;
85 readonly geminiHome: string;
86 readonly acpDirectory: string;
87 readonly tokenPath: string;
88 /** Parent of the per-process temp directories PyInstaller unpacks into. */
89 readonly tempDirectory: string;
90 readonly browserCommand: string;
91}
92
93/**
94 * Credentials for the non-personal ACP auth methods. The agent reads the API
95 * key from its environment and the GCP project and location from
96 * `settings.json` in the profile. Empty strings mean "not set".
97 */
98export interface AntigravityAuthConfig {
99 readonly authMethod: AntigravityAuthMethod;
100 readonly apiKey: string;
101 readonly gcpProject: string;
102 readonly gcpLocation: string;
103}
104
105export const ANTIGRAVITY_PERSONAL_AUTH: AntigravityAuthConfig = {
106 authMethod: "oauth-personal",
107 apiKey: "",
108 gcpProject: "",
109 gcpLocation: "",
110};
111
112/** True for the two methods that open a Google sign-in page. */
113export function antigravityAuthUsesBrowser(authMethod: AntigravityAuthMethod): boolean {
114 return authMethod === "oauth-personal" || authMethod === "oauth-business";
115}
116
117/** Label shown on the provider card once the method has authenticated. */
118export function antigravityAuthLabel(authMethod: AntigravityAuthMethod): string {
119 switch (authMethod) {
120 case "oauth-personal":
121 return "Google account";
122 case "oauth-business":
123 return "Gemini Enterprise";
124 case "gemini-api-key":
125 return "Gemini API key";
126 case "agent-platform":
127 return "Agent Platform";
128 }
129}
130
131/**
132 * Explains what is missing before a non-personal method can authenticate, or
133 * null when the config is complete. Personal sign-in never needs config.
134 */
135export function antigravityAuthConfigIssue(auth: AntigravityAuthConfig): string | null {
136 switch (auth.authMethod) {
137 case "oauth-personal":
138 return null;
139 case "oauth-business":
140 return auth.gcpProject && auth.gcpLocation
141 ? null
142 : "Gemini Enterprise needs a GCP project and location in the Antigravity provider settings.";
143 case "gemini-api-key":
144 return auth.apiKey ? null : "Enter a Gemini API key in the Antigravity provider settings.";
145 case "agent-platform":
146 return auth.apiKey || (auth.gcpProject && auth.gcpLocation)
147 ? null
148 : "Agent Platform needs an API key, or a GCP project and location, in the Antigravity provider settings.";
149 }
150}
151
152/**
153 * `settings.json` content for the agent's profile. `auth.type` names the
154 * selected method so a native logout clears only that method's credentials
155 * instead of every stored token. The GCP block feeds Enterprise and Agent
156 * Platform. Never holds a credential.
157 */
158export function antigravityProfileSettings(auth: AntigravityAuthConfig): string {
159 const gcp = {
160 ...(auth.gcpProject ? { project: auth.gcpProject } : {}),
161 ...(auth.gcpLocation ? { location: auth.gcpLocation } : {}),
162 };
163 return `${encodeProfileSettings({
164 auth: { type: auth.authMethod },
165 ...(Object.keys(gcp).length > 0 ? { gcp } : {}),
166 })}\n`;
167}
168
169export interface AntigravityAuthorizationUrl {
170 readonly authorizationUrl: string;
171 readonly redirectUri: string;
172 readonly state: string;
173}
174
175function authSupportError(detail: string) {
176 return new AcpErrors.AcpTransportError({ detail, cause: undefined });
177}
178
179/** Recognizes native auth failures and interactive login blocked by T3. */
180export function isAntigravitySignInRequiredError(error: unknown): boolean {
181 return (
182 (isAcpRequestError(error) && error.code === -32000) ||
183 (isAcpTransportError(error) && error.detail === ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE)
184 );
185}
186
187export interface AntigravityInstanceDirectories {
188 /** GEMINI_HOME for the agent. Holds the instance's Google sign-in. */
189 readonly profile: string;
190 /**
191 * Parent of the per-process directories the agent unpacks into. It sits
192 * beside the profile, not inside it: the agent unpacks members up to 120
193 * characters deep, and the profile's longer name would push them past
194 * Windows' 260-character path limit.
195 */
196 readonly runtimeTemp: string;
197}
198
199/** Hashes the instance ID so case-only differences stay separate on case-insensitive filesystems. */
200export const resolveAntigravityInstanceDirectories = Effect.fn(
201 "resolveAntigravityInstanceDirectories",
202)(function* (stateDir: string, instanceId: ProviderInstanceId) {
203 const crypto = yield* Crypto.Crypto;
204 const path = yield* Path.Path;
205 const key = Hex.encode(yield* crypto.digest("SHA-256", new TextEncoder().encode(instanceId)));
206 return {
207 profile: path.join(stateDir, "providers", "antigravity", key),
208 runtimeTemp: path.join(stateDir, "antigravity-tmp", key.slice(0, 12)),
209 } satisfies AntigravityInstanceDirectories;
210});
211
212function quoteBrowserArgument(value: string): string {
213 return `'${value.replaceAll("'", `'"'"'`)}'`;
214}
215
216function antigravityEnvironment(
217 profile: AntigravityProfile,
218 baseEnv: NodeJS.ProcessEnv,
219 auth: AntigravityAuthConfig,
220 runtimeTempDirectory?: string,
221) {
222 const environment: NodeJS.ProcessEnv = {};
223 for (const [key, value] of Object.entries(baseEnv)) {
224 // Windows treats environment keys as case-insensitive. Remove aliases too.
225 if (!removedEnvironmentKeys.has(key.toUpperCase())) environment[key] = value;
226 }
227 // Only the configured method's credential reaches the agent. The agent
228 // prefers GOOGLE_API_KEY over the GCP pair for Agent Platform, so the pair
229 // goes through settings.json instead of the environment.
230 const credential =
231 auth.authMethod === "gemini-api-key" && auth.apiKey
232 ? { GEMINI_API_KEY: auth.apiKey }
233 : auth.authMethod === "agent-platform" && auth.apiKey
234 ? { GOOGLE_API_KEY: auth.apiKey }
235 : {};
236 // The agent is a PyInstaller one-file bundle. It unpacks about 1 GB into
237 // the system temp directory per launch and a force kill leaves that behind.
238 // Point it at a T3-owned directory so the driver can reclaim the space.
239 const tempDirectory = runtimeTempDirectory ?? profile.tempDirectory;
240 return {
241 ...environment,
242 ...credential,
243 GEMINI_HOME: profile.geminiHome,
244 AGY_ACP_FORCE_FILE_STORAGE: "1",
245 BROWSER: profile.browserCommand,
246 PYTHONUNBUFFERED: "1",
247 ELECTRON_RUN_AS_NODE: "1",
248 ...(profile.platform === "win32"
249 ? { TEMP: tempDirectory, TMP: tempDirectory }
250 : { TMPDIR: tempDirectory }),
251 };
252}
253
254/**
255 * The agent reads its user-global skills under `GEMINI_HOME`, which T3 points
256 * at the private profile. Link the two skill directories back to the user's
257 * real `~/.gemini` so global skills load, while MCP servers, hooks, and
258 * credentials stay isolated. Best effort: a link that cannot be made only
259 * costs global skills, never the session. A real directory at the link path
260 * is the user's own content and is left alone.
261 */
262const linkAntigravityUserSkills = Effect.fn("linkAntigravityUserSkills")(function* (input: {
263 readonly profileDirectory: string;
264 readonly userHome: string;
265 readonly platform: NodeJS.Platform;
266}): Effect.fn.Return<void, never, FileSystem.FileSystem | Path.Path> {
267 const fs = yield* FileSystem.FileSystem;
268 const path = yield* Path.Path;
269 const links = antigravityUserSkillDirectories(path, input.profileDirectory);
270 const targets = antigravityUserSkillDirectories(path, path.join(input.userHome, ".gemini"));
271 for (const [link, target] of [
272 [links[0], targets[0]],
273 [links[1], targets[1]],
274 ] as const) {
275 yield* Effect.gen(function* () {
276 const existing = yield* fs.readLink(link).pipe(
277 Effect.map((value): string | undefined => path.resolve(path.dirname(link), value)),
278 Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined),
279 );
280 if (existing === target) return;
281 if (existing !== undefined) {
282 yield* fs.remove(link);
283 }
284 yield* fs.makeDirectory(path.dirname(link), { recursive: true });
285 yield* Effect.tryPromise(() =>
286 NodeFSP.symlink(target, link, input.platform === "win32" ? "junction" : "dir"),
287 );
288 }).pipe(
289 // A non-symlink at the link path fails `readLink`; anything else is a
290 // filesystem refusal. Both leave the profile usable.
291 Effect.catch((error) =>
292 Effect.logWarning("Antigravity user skills are not linked into the profile.", {
293 link,
294 target,
295 error,
296 }),
297 ),
298 );
299 }
300});
301
302/** Prepares a private profile without reading or copying Google credentials. */
303export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(function* (input: {
304 readonly profileDirectory: string;
305 readonly baseEnv?: NodeJS.ProcessEnv;
306 readonly runtimeExecutablePath?: string;
307 readonly platform?: NodeJS.Platform;
308 readonly auth?: AntigravityAuthConfig;
309 /** Home the agent expands `~` against. Defaults to the launch environment's. */
310 readonly userHome?: string;
311 /** Parent of per-process temp directories. Defaults to one inside the profile. */
312 readonly tempDirectory?: string;
313}) {
314 const auth = input.auth ?? ANTIGRAVITY_PERSONAL_AUTH;
315 const fs = yield* FileSystem.FileSystem;
316 const path = yield* Path.Path;
317 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
318 const platform = input.platform ?? (yield* HostProcess.Platform);
319 const userHome =
320 input.userHome ??
321 resolveAntigravityUserHome(
322 platform,
323 input.baseEnv ?? process.env,
324 yield* HostProcess.HomeDirectory,
325 );
326 const runtimeExecutablePath =
327 input.runtimeExecutablePath ??
328 (yield* resolveNodeExecutable("Antigravity sign-in", input.baseEnv).pipe(
329 Effect.mapError(
330 (cause) =>
331 new AcpErrors.AcpTransportError({
332 detail: nodeRuntimeUnavailableMessage("Antigravity sign-in"),
333 cause,
334 }),
335 ),
336 ));
337 const helperExecutable =
338 platform === "win32" ? runtimeExecutablePath.replaceAll("\\", "/") : runtimeExecutablePath;
339 const browserArguments = [helperExecutable, "-e", browserHelperSource, "--", "%s"];
340 const browserCommand = browserArguments.map(quoteBrowserArgument).join(" ");
341 if (
342 browserCommand.includes(platform === "win32" ? ";" : ":") ||
343 helperExecutable.includes("\r") ||
344 helperExecutable.includes("\n") ||
345 helperExecutable.includes("\0") ||
346 helperExecutable.includes("%s")
347 ) {
348 return yield* authSupportError(
349 "The T3 runtime path cannot be used to suppress Antigravity browser launches.",
350 );
351 }
352
353 const geminiHome = path.resolve(input.profileDirectory);
354 const acpDirectory = path.join(geminiHome, "antigravity-acp");
355 const tempDirectory = input.tempDirectory ?? path.join(acpDirectory, "tmp");
356 const profile: AntigravityProfile = {
357 platform,
358 geminiHome,
359 acpDirectory,
360 tokenPath: path.join(acpDirectory, "acp_token.json"),
361 tempDirectory,
362 browserCommand,
363 };
364 const environment = antigravityEnvironment(profile, input.baseEnv ?? process.env, auth);
365 yield* Effect.gen(function* () {
366 const child = yield* spawner.spawn(
367 ChildProcess.make(helperExecutable, ["-e", browserHelperSource, "--", browserPreflightUrl], {
368 env: environment,
369 extendEnv: false,
370 shell: false,
371 }),
372 );
373 const [stdout, stderr, exitCode] = yield* Effect.all(
374 [
375 collectUint8StreamText({ stream: child.stdout, maxBytes: 4_096 }),
376 collectUint8StreamText({ stream: child.stderr, maxBytes: 4_096 }),
377 child.exitCode,
378 ],
379 { concurrency: "unbounded" },
380 );
381 if (
382 Number(exitCode) !== 0 ||
383 stdout.bytes !== 0 ||
384 stdout.truncated ||
385 stderr.truncated ||
386 stderr.text !== `${ANTIGRAVITY_AUTH_BROWSER_MARKER}"${browserPreflightUrl}"\n`
387 ) {
388 return yield* authSupportError("Antigravity browser suppression could not be verified.");
389 }
390 }).pipe(
391 Effect.scoped,
392 Effect.timeoutOrElse({
393 duration: "5 seconds",
394 orElse: () =>
395 Effect.fail(authSupportError("Antigravity browser suppression verification timed out.")),
396 }),
397 Effect.mapError((error) =>
398 error._tag === "AcpTransportError"
399 ? error
400 : authSupportError("Antigravity browser suppression could not be verified."),
401 ),
402 );
403
404 for (const directory of [geminiHome, acpDirectory, tempDirectory]) {
405 yield* fs
406 .makeDirectory(directory, { recursive: true, mode: 0o700 })
407 .pipe(
408 Effect.mapError(() =>
409 authSupportError("The Antigravity profile directory could not be created."),
410 ),
411 );
412 if (platform !== "win32") {
413 yield* fs
414 .chmod(directory, 0o700)
415 .pipe(
416 Effect.mapError(() =>
417 authSupportError("The Antigravity profile directory permissions could not be set."),
418 ),
419 );
420 }
421 }
422 // Rewriting on every launch keeps a method, project, or location edit in
423 // Settings effective. The agent also records auth.type here after a
424 // sign-in, which matches the value written below.
425 yield* fs
426 .writeFileString(path.join(acpDirectory, "settings.json"), antigravityProfileSettings(auth))
427 .pipe(
428 Effect.mapError(() =>
429 authSupportError("The Antigravity profile settings could not be written."),
430 ),
431 );
432 yield* linkAntigravityUserSkills({ profileDirectory: geminiHome, userHome, platform });
433 return profile;
434});
435
436/** Applies the same subscription-only launch settings to every ACP process. */
437export function buildAntigravityAcpSpawnInput(input: {
438 readonly installation: {
439 readonly executablePath: string;
440 readonly harnessPath: string;
441 };
442 readonly profile: AntigravityProfile;
443 readonly cwd: string;
444 readonly baseEnv?: NodeJS.ProcessEnv;
445 readonly auth?: AntigravityAuthConfig;
446 /** Per-process temp directory. Defaults to the profile's shared temp directory. */
447 readonly runtimeTempDirectory?: string;
448}): AcpSessionRuntime.AcpSpawnInput {
449 return {
450 command: input.installation.executablePath,
451 args: input.profile.platform === "linux" ? ["--uid="] : [],
452 cwd: input.cwd,
453 env: {
454 ...antigravityEnvironment(
455 input.profile,
456 input.baseEnv ?? process.env,
457 input.auth ?? ANTIGRAVITY_PERSONAL_AUTH,
458 input.runtimeTempDirectory,
459 ),
460 ANTIGRAVITY_HARNESS_PATH: input.installation.harnessPath,
461 },
462 extendEnv: false,
463 };
464}
465
466/** Reads only the public authorization request, never an OAuth token file. */
467export const parseAntigravityAuthorizationUrl = Effect.fn("parseAntigravityAuthorizationUrl")(
468 function* (
469 authorizationUrl: string,
470 ): Effect.fn.Return<AntigravityAuthorizationUrl, AcpErrors.AcpError> {
471 const invalidUrl = () =>
472 authSupportError("Antigravity returned an invalid Google sign-in URL.");
473 if (authorizationUrl.length > maxAuthorizationUrlLength || /\s/.test(authorizationUrl)) {
474 return yield* invalidUrl();
475 }
476 const url = yield* decodeUrl(authorizationUrl).pipe(Effect.mapError(invalidUrl));
477 const state = url.searchParams.get("state");
478 const redirectUri = url.searchParams.get("redirect_uri");
479 if (
480 url.origin !== "https://accounts.google.com" ||
481 url.pathname !== "/o/oauth2/v2/auth" ||
482 url.username !== "" ||
483 url.password !== "" ||
484 url.hash !== "" ||
485 url.searchParams.getAll("state").length !== 1 ||
486 url.searchParams.getAll("redirect_uri").length !== 1 ||
487 url.searchParams.getAll("response_type").length !== 1 ||
488 url.searchParams.get("response_type") !== "code" ||
489 state === null ||
490 state.length === 0 ||
491 state.length > 512 ||
492 /\s/.test(state) ||
493 redirectUri === null ||
494 !/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}\/$/.test(redirectUri)
495 ) {
496 return yield* invalidUrl();
497 }
498 const redirect = yield* decodeUrl(redirectUri).pipe(Effect.mapError(invalidUrl));
499 if (Number(redirect.port) < 1_024) return yield* invalidUrl();
500 return { authorizationUrl, redirectUri, state };
501 },
502);
503
504export function makeAntigravityStdoutTransform(
505 input: {
506 readonly onAuthorizationUrl?: (
507 authorizationUrl: string,
508 ) => Effect.Effect<void, AcpErrors.AcpError>;
509 } = {},
510) {
511 const handleLine = Effect.fn("antigravityAuthSupport.handleStdoutLine")(function* (
512 line: Uint8Array,
513 ) {
514 if (!authPrefixBytes.every((byte, index) => line[index] === byte)) return [line];
515 const message = new TextDecoder().decode(line).replace(/\r?\n$/, "");
516 const request = yield* parseAntigravityAuthorizationUrl(
517 message.slice(ANTIGRAVITY_AUTH_STDOUT_PREFIX.length),
518 );
519 if (!input.onAuthorizationUrl) {
520 return yield* authSupportError(ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE);
521 }
522 yield* input.onAuthorizationUrl(request.authorizationUrl);
523 return [];
524 });
525
526 return (
527 stdout: ChildProcessSpawner.ChildProcessHandle["stdout"],
528 ): Stream.Stream<Uint8Array, PlatformError.PlatformError | AcpErrors.AcpError> =>
529 Stream.suspend(() => {
530 let pending: Uint8Array[] = [];
531 let pendingBytes = 0;
532 const finishLine = () => {
533 const line = Buffer.concat(pending, pendingBytes);
534 pending = [];
535 pendingBytes = 0;
536 return line;
537 };
538 return stdout.pipe(
539 Stream.mapEffect(
540 Effect.fn("antigravityAuthSupport.splitStdoutLines")(function* (chunk: Uint8Array) {
541 const lines: Uint8Array[] = [];
542 let offset = 0;
543 while (offset < chunk.byteLength) {
544 const newline = chunk.indexOf(10, offset);
545 const end = newline === -1 ? chunk.byteLength : newline + 1;
546 const part = chunk.subarray(offset, end);
547 if (pendingBytes + part.byteLength > maxStdoutLineBytes) {
548 return yield* authSupportError(
549 "Antigravity sent a protocol line that is too large.",
550 );
551 }
552 pending.push(part);
553 pendingBytes += part.byteLength;
554 if (newline !== -1) lines.push(finishLine());
555 offset = end;
556 }
557 return lines;
558 }),
559 ),
560 Stream.flatMap(Stream.fromIterable),
561 Stream.concat(
562 Stream.suspend(() => (pendingBytes > 0 ? Stream.succeed(finishLine()) : Stream.empty)),
563 ),
564 Stream.mapEffect(handleLine),
565 Stream.flatMap(Stream.fromIterable),
566 );
567 });
568}
569
570/** Receives native 1.1.1 sign-in URLs and T3 browser-helper URLs without logging stderr. */
571export function makeAntigravityStderrHandler(
572 input: {
573 readonly onAuthorizationUrl?: (
574 authorizationUrl: string,
575 ) => Effect.Effect<void, AcpErrors.AcpError>;
576 } = {},
577) {
578 let pending = "";
579 const handleLine = (line: string) => {
580 const message = line.endsWith("\r") ? line.slice(0, -1) : line;
581 if (message.length > maxBrowserHelperLineLength) {
582 return Effect.void;
583 }
584 const url = message.startsWith(ANTIGRAVITY_AUTH_STDOUT_PREFIX)
585 ? Effect.succeed(message.slice(ANTIGRAVITY_AUTH_STDOUT_PREFIX.length))
586 : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)
587 ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length))
588 : undefined;
589 if (url === undefined) return Effect.void;
590 return url.pipe(
591 Effect.flatMap(parseAntigravityAuthorizationUrl),
592 Effect.matchEffect({
593 onFailure: () => Effect.void,
594 onSuccess: (request) =>
595 input.onAuthorizationUrl
596 ? input.onAuthorizationUrl(request.authorizationUrl)
597 : Effect.fail(authSupportError(ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE)),
598 }),
599 );
600 };
601
602 return Effect.fn("antigravityAuthSupport.handleStderr")(function* (text: string) {
603 const lines = `${pending}${text}`.split("\n");
604 pending = lines.pop() ?? "";
605 if (pending.length > maxBrowserHelperLineLength) pending = "";
606 yield* Effect.forEach(lines, handleLine, { discard: true });
607 });
608}