fix(agents): fix antigravity settings path resolution and CLI launch in PowerShell

Resolve Antigravity IDE configuration directory from installed executable metadata and product.json before falling back to legacy paths.

Launch Antigravity CLI via System.Diagnostics.Process and wait for process exit when running under Windows PowerShell to avoid console I/O contention.

Add unit tests for executable product metadata resolution and PowerShell CLI process synchronization.
This commit is contained in:
lzt404 committed 2026-09-21 21:58:01 +08:00
1 parent 615188b390
commit 84c29782b0
3 files changed
+240 -14

No files matched your search

+34 -2
View File
@@ -28,13 +28,45 @@ pub(crate) fn antigravity_config_paths(client: AgentClient, home: &Path) -> Vec<
let directory = agent_configuration_environment("XDG_CONFIG_HOME")
.filter(|path| path.is_absolute())
.unwrap_or_else(|| home.join(".config"));
vec![antigravity_ide_settings_path(
&directory,
find_antigravity_ide(home).as_deref(),
)]
}
fn antigravity_ide_settings_path(directory: &Path, executable: Option<&Path>) -> PathBuf {
if let Some(executable) = executable {
let product = antigravity_resources(executable)
.and_then(|resources| fs::read_to_string(resources.join("product.json")).ok())
.and_then(|text| serde_json::from_str::<Value>(&text).ok());
let name = product
.as_ref()
.and_then(|value| value.get("nameShort"))
.and_then(Value::as_str)
.filter(|name| matches!(*name, "Antigravity IDE" | "Antigravity"))
.or_else(|| {
executable
.ancestors()
.find_map(|path| match path.file_name()?.to_str()? {
"Antigravity IDE.exe" | "Antigravity IDE.app" | "antigravity-ide" => {
Some("Antigravity IDE")
}
"Antigravity.exe" | "Antigravity.app" | "antigravity" => {
Some("Antigravity")
}
_ => None,
})
})
.unwrap_or("Antigravity IDE");
return directory.join(name).join("User/settings.json");
}
let current = directory.join("Antigravity IDE/User/settings.json");
let legacy = directory.join("Antigravity/User/settings.json");
vec![if !current.exists() && legacy.exists() {
if !current.exists() && legacy.exists() {
legacy
} else {
current
}]
}
}
pub(crate) fn find_antigravity_cli(home: &Path) -> Option<PathBuf> {
+70 -1
View File
@@ -87,6 +87,63 @@ fn antigravity_clients_have_independent_paths_and_launch_targets() {
));
}
#[test]
fn antigravity_ide_settings_follow_the_installed_product() {
for (name, other) in [
("Antigravity IDE", "Antigravity"),
("Antigravity", "Antigravity IDE"),
] {
let home = Home::new();
let directory = home.0.join("configuration");
let executable = home.0.join("installation/Contents/MacOS/Electron");
let resources = antigravity_resources(&executable).unwrap();
home.save(&resources.join("product.json"), &json!({"nameShort":name}));
let other_settings = directory.join(other).join("User/settings.json");
home.save(&other_settings, &json!({"editor.fontSize":17}));
let selected = directory.join(name).join("User/settings.json");
assert!(!selected.exists());
assert_eq!(
antigravity_ide_settings_path(&directory, Some(&executable)),
selected
);
home.save(&selected, &json!({}));
assert_eq!(
antigravity_ide_settings_path(&directory, Some(&executable)),
selected
);
assert_eq!(
fs::read_to_string(other_settings).unwrap(),
r#"{"editor.fontSize":17}"#
);
}
}
#[test]
fn antigravity_ide_settings_use_installation_name_without_product_metadata() {
let home = Home::new();
let directory = home.0.join("configuration");
for (binary, name) in [
("Antigravity IDE.exe", "Antigravity IDE"),
("Antigravity.exe", "Antigravity"),
(
"Antigravity IDE.app/Contents/MacOS/Electron",
"Antigravity IDE",
),
("Antigravity.app/Contents/MacOS/Electron", "Antigravity"),
("antigravity-ide", "Antigravity IDE"),
("antigravity", "Antigravity"),
] {
let executable = home.0.join("installation").join(binary);
assert_eq!(
antigravity_ide_settings_path(&directory, Some(&executable)),
directory.join(name).join("User/settings.json")
);
}
let legacy = directory.join("Antigravity/User/settings.json");
home.save(&legacy, &json!({}));
assert_eq!(antigravity_ide_settings_path(&directory, None), legacy);
}
#[test]
fn antigravity_updates_and_disable_preserve_external_settings() {
for client in [AgentClient::AntigravityIde, AgentClient::AntigravityCli] {
@@ -270,7 +327,18 @@ fn antigravity_ide_discovery_and_template_use_native_server() {
fs::write(&executable, []).unwrap();
home.save(
&resources.join("product.json"),
&json!({"ideVersion":"2.5.5"}),
&json!({"ideVersion":"2.5.5", "nameShort":"Antigravity IDE"}),
);
let legacy = home
.0
.join("AppData/Roaming/Antigravity/User/settings.json");
home.save(&legacy, &json!({"editor.fontSize":17}));
let expected = home
.0
.join("AppData/Roaming/Antigravity IDE/User/settings.json");
assert_eq!(
antigravity_config_paths(AgentClient::AntigravityIde, &home.0),
vec![expected.clone()]
);
assert_eq!(find_antigravity_ide(&home.0), Some(executable.clone()));
assert_eq!(
@@ -295,6 +363,7 @@ fn antigravity_ide_discovery_and_template_use_native_server() {
})
.unwrap();
assert_eq!(updates.len(), 1);
assert_eq!(updates[0].path, expected);
let value = parse(&updates[0].path, Some(&updates[0].after)).unwrap();
assert_eq!(
Path::new(value[IDE_ENV_KEY][IDE_SERVER_ENV].as_str().unwrap()),
+136 -11
View File
@@ -1284,6 +1284,63 @@ fn launch_cli_agent(
})
}
#[cfg(target_os = "windows")]
fn windows_powershell_cli_script(
executable: &Path,
working_directory: &Path,
arguments: &[String],
) -> String {
let directory = windows_powershell_single_quoted_literal(&path_to_string(working_directory));
let executable = windows_powershell_single_quoted_literal(&path_to_string(executable));
if arguments
.first()
.is_some_and(|arg| arg == "--cpa-antigravity-cli")
{
let arguments = arguments
.iter()
.map(|argument| {
let mut quoted = String::from("\"");
let mut backslashes = 0;
for ch in argument.chars() {
if ch == '\\' {
backslashes += 1;
continue;
}
quoted.push_str(&"\\".repeat(if ch == '"' {
backslashes * 2 + 1
} else {
backslashes
}));
backslashes = 0;
quoted.push(ch);
}
quoted.push_str(&"\\".repeat(backslashes * 2));
quoted.push('"');
quoted
})
.collect::<Vec<_>>()
.join(" ");
return format!(
"Set-Location -LiteralPath {directory}; \
$cpaLaunchInfo = New-Object System.Diagnostics.ProcessStartInfo; \
$cpaLaunchInfo.FileName = {executable}; \
$cpaLaunchInfo.WorkingDirectory = {directory}; \
$cpaLaunchInfo.UseShellExecute = $false; \
$cpaLaunchInfo.Arguments = {}; \
$cpaLaunchProcess = [System.Diagnostics.Process]::Start($cpaLaunchInfo); \
try {{ $cpaLaunchProcess.WaitForExit(); $global:LASTEXITCODE = $cpaLaunchProcess.ExitCode }} \
finally {{ $cpaLaunchProcess.Dispose() }}",
windows_powershell_single_quoted_literal(&arguments),
);
}
let arguments = arguments
.iter()
.map(|argument| windows_powershell_single_quoted_literal(argument))
.collect::<Vec<_>>()
.join(" ");
format!("Set-Location -LiteralPath {directory}; & {executable} {arguments}")
}
#[cfg(target_os = "windows")]
fn launch_cli_agent(
executable: &Path,
@@ -1331,17 +1388,7 @@ fn launch_cli_agent(
}
"powershell" => {
let mut command = Command::new(windows_powershell_executable());
let arguments = arguments
.iter()
.map(|argument| windows_powershell_single_quoted_literal(argument))
.collect::<Vec<_>>()
.join(" ");
let script = format!(
"Set-Location -LiteralPath {}; & {} {}",
windows_powershell_single_quoted_literal(&path_to_string(working_directory)),
windows_powershell_single_quoted_literal(&path_to_string(executable)),
arguments,
);
let script = windows_powershell_cli_script(executable, working_directory, arguments);
command.args(["-NoLogo", "-NoProfile", "-NoExit", "-Command", &script]);
command.creation_flags(CREATE_NEW_CONSOLE);
command
@@ -1411,6 +1458,84 @@ mod tests {
assert_eq!(normalize_agent_terminal(" auto "), "auto");
}
#[cfg(windows)]
#[test]
fn antigravity_powershell_waits_for_gui_helper_and_preserves_arguments() {
let directory = env::temp_dir()
.join(format!(
"cpa-powershell-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos(),
))
.join("用户's [workspace] $value");
fs::create_dir_all(&directory).unwrap();
let executable = directory.join("GUI helper.exe");
let compiled = directory.parent().unwrap().join("helper.exe");
let source = r#"
using System;
using System.IO;
using System.Threading;
class GuiHelper {
static void Main(string[] args) {
Thread.Sleep(350);
File.WriteAllLines(Path.Combine(Environment.CurrentDirectory, "arguments.txt"), args);
File.WriteAllText(Path.Combine(Environment.CurrentDirectory, "finished.txt"), "done");
}
}
"#;
let script = format!(
"Add-Type -TypeDefinition {} -OutputAssembly {} -OutputType WindowsApplication -ErrorAction Stop",
windows_powershell_single_quoted_literal(source),
windows_powershell_single_quoted_literal(&path_to_string(&compiled)),
);
let run = |script: &str| {
let mut command = Command::new(windows_powershell_executable());
command.args([
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-Command",
script,
]);
configure_background_command(&mut command);
let result = command_output_with_timeout(&mut command, Duration::from_secs(20))
.unwrap()
.expect("PowerShell timed out");
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
};
run(&script);
fs::rename(compiled, &executable).unwrap();
let arguments = vec![
"--cpa-antigravity-cli".into(),
path_to_string(&directory),
"trailing slash \\".into(),
"quote \" and \\\"".into(),
"literal $value & [x] 'quoted'".into(),
String::new(),
];
let script = format!(
"{}; if (![IO.File]::Exists({})) {{ throw 'GUI helper is still running' }}",
windows_powershell_cli_script(&executable, &directory, &arguments),
windows_powershell_single_quoted_literal(&path_to_string(
&directory.join("finished.txt")
)),
);
run(&script);
let received = fs::read_to_string(directory.join("arguments.txt")).unwrap();
assert_eq!(
received.lines().collect::<Vec<_>>(),
arguments.iter().map(String::as_str).collect::<Vec<_>>()
);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn desktop_process_matching_is_scoped_to_the_exact_bundle() {
let application = Path::new("/Applications/Claude Desktop.app/Contents/MacOS/Claude");