Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/server/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as CliError from "effect/unstable/cli/CliError";

import * as NetService from "@t3tools/shared/Net";
import packageJson from "../package.json" with { type: "json" };
import { antigravityBrowserCommand } from "./cli/antigravityBrowser.ts";
import { authCommand } from "./cli/auth.ts";
import { appCommand } from "./cli/app.ts";
import { connectCommand } from "./cli/connect.ts";
Expand Down Expand Up @@ -70,6 +71,7 @@ export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
claudeHistoryCommand,
servicePreflightCommand,
sshHelperCommand,
antigravityBrowserCommand,
themeCommand,
triageCommand,
cloudEnabled ? connectCommand : connectUnavailableCommand,
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/cli/antigravityBrowser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as TestConsole from "effect/testing/TestConsole";
import { Command } from "effect/unstable/cli";

import { ANTIGRAVITY_AUTH_BROWSER_MARKER } from "../provider/antigravityAuthSupport.ts";
import { antigravityBrowserCommand } from "./antigravityBrowser.ts";

it.effect("writes the Antigravity authorization URL marker to stderr", () =>
Effect.gen(function* () {
const url = "https://accounts.google.com/example?state=opaque";
yield* Command.runWith(antigravityBrowserCommand, { version: "0.0.0" })([url]);

assert.deepEqual(yield* TestConsole.errorLines, [
`${ANTIGRAVITY_AUTH_BROWSER_MARKER}"https://accounts.google.com/example?state=opaque"`,
]);
}).pipe(Effect.provide(Layer.mergeAll(NodeServices.layer, TestConsole.layer))),
);
21 changes: 21 additions & 0 deletions apps/server/src/cli/antigravityBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as Console from "effect/Console";
import { Argument, Command } from "effect/unstable/cli";

import {
ANTIGRAVITY_AUTH_BROWSER_MARKER,
ANTIGRAVITY_BROWSER_HELPER_COMMAND,
} from "../provider/antigravityAuthSupport.ts";

/**
* Hosts the no-browser helper inside the single executable. Script installs
* run the equivalent inline source under Node, while a SEA has no Node
* interpreter for `-e` and invokes this hidden subcommand instead.
*/
export const antigravityBrowserCommand = Command.make(ANTIGRAVITY_BROWSER_HELPER_COMMAND, {
url: Argument.string("url"),
}).pipe(
Command.unlisted,
Command.withHandler(({ url }) =>
Console.error(`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${JSON.stringify(url)}`),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium cli/antigravityBrowser.ts:19

When Antigravity closes the helper's stderr pipe during browser cancellation, this handler exits nonzero with an unhandled EPIPE instead of completing the suppression flow. Console.error writes directly to the global stderr, but this standalone helper does not install the stderr error handling used by the inline helper; Python therefore treats the command as failed and launches a real browser. Add the same EPIPE handling to the helper's stderr stream before writing the marker.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/cli/antigravityBrowser.ts around line 19:

When Antigravity closes the helper's `stderr` pipe during browser cancellation, this handler exits nonzero with an unhandled `EPIPE` instead of completing the suppression flow. `Console.error` writes directly to the global `stderr`, but this standalone helper does not install the `stderr` error handling used by the inline helper; Python therefore treats the command as failed and launches a real browser. Add the same `EPIPE` handling to the helper's `stderr` stream before writing the marker.

),
);
46 changes: 45 additions & 1 deletion apps/server/src/provider/antigravityAuthSupport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import * as NodeChildProcess from "node:child_process";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { ProviderInstanceId } from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import {
HostProcessExecutablePath,
HostProcessIsExecutable,
HostProcessPlatform,
} from "@t3tools/shared/hostProcess";
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
Expand Down Expand Up @@ -511,6 +515,46 @@ it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => {
}),
);

it.effect("uses the hidden browser helper when the host is a single executable", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const temporaryDirectory = yield* fs.makeTempDirectoryScoped();
const executablePath = "/opt/t3-runtime/t3";
let helperCommand: ChildProcess.StandardCommand | undefined;
const profile = yield* prepareAntigravityProfile({
profileDirectory: temporaryDirectory,
platform: "linux",
}).pipe(
Effect.provideService(HostProcessExecutablePath, executablePath),
Effect.provideService(HostProcessIsExecutable, true),
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) => {
if (!ChildProcess.isStandardCommand(command)) return spawner.spawn(command);
helperCommand = command;
const url = command.args.at(-1) ?? "";
return spawner.spawn(
ChildProcess.make(process.execPath, [
"-e",
`process.stderr.write(${JSON.stringify(
`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${JSON.stringify(url)}\n`,
)})`,
]),
);
}),
),
);

expect(helperCommand?.command).toBe(executablePath);
expect(helperCommand?.args).toEqual([
"__antigravity-browser",
"https://example.invalid/t3-antigravity-browser-preflight",
]);
expect(profile.browserCommand).toBe(`'/opt/t3-runtime/t3' '__antigravity-browser' '%s'`);
}),
);

it.effect.skipIf(!symlinksSupported)(
"links the user's global skill directories into the profile without touching real content",
() =>
Expand Down
15 changes: 12 additions & 3 deletions apps/server/src/provider/antigravityAuthSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import * as NodeFSP from "node:fs/promises";
import * as NodePath from "node:path";

import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts";
import { HostProcessExecutablePath, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import {
HostProcessExecutablePath,
HostProcessIsExecutable,
HostProcessPlatform,
} from "@t3tools/shared/hostProcess";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
Expand All @@ -26,6 +30,7 @@ import {
export const ANTIGRAVITY_AUTH_STDOUT_PREFIX =
"Open the following link to authenticate the ACP server: ";
export const ANTIGRAVITY_AUTH_BROWSER_MARKER = "__T3_ANTIGRAVITY_AUTH_URL__";
export const ANTIGRAVITY_BROWSER_HELPER_COMMAND = "__antigravity-browser";
export const ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE =
"Sign in to Antigravity in Settings before you continue.";

Expand Down Expand Up @@ -293,9 +298,13 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
const userHome =
input.userHome ?? resolveAntigravityUserHome(platform, input.baseEnv ?? process.env);
const runtimeExecutablePath = input.runtimeExecutablePath ?? (yield* HostProcessExecutablePath);
const runtimeIsExecutable = yield* HostProcessIsExecutable;
const helperExecutable =
platform === "win32" ? runtimeExecutablePath.replaceAll("\\", "/") : runtimeExecutablePath;
const browserArguments = [helperExecutable, "-e", browserHelperSource, "--", "%s"];
const helperArguments = runtimeIsExecutable
? [ANTIGRAVITY_BROWSER_HELPER_COMMAND]
: ["-e", browserHelperSource, "--"];
const browserArguments = [helperExecutable, ...helperArguments, "%s"];
const browserCommand = browserArguments.map(quoteBrowserArgument).join(" ");
if (
browserCommand.includes(platform === "win32" ? ";" : ":") ||
Expand All @@ -321,7 +330,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
const environment = antigravityEnvironment(profile, input.baseEnv ?? process.env, auth);
yield* Effect.gen(function* () {
const child = yield* spawner.spawn(
ChildProcess.make(helperExecutable, ["-e", browserHelperSource, "--", browserPreflightUrl], {
ChildProcess.make(helperExecutable, [...helperArguments, browserPreflightUrl], {
env: environment,
extendEnv: false,
shell: false,
Expand Down
Loading