fix: cross-platform support + auto token from state.vscdb
- User-Agent now matches actual OS (macOS/Windows/Linux) - grep -oP replaced with grep -oE for macOS BSD compat - Port-killer gated with cfg(unix)/cfg(windows) - zg binary: macOS uses launchctl, Windows uses schtasks - Data dir mismatch fixed in mitm-redirect.sh - Windows setup-windows.ps1 ProjectDir fixed - README: token path, prerequisites updated - setup-linux.sh: pre-flight dependency checks - OAuth token auto-read from Antigravity state.vscdb - Version bump to 1.0.1
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2361,7 +2361,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerogravity"
|
name = "zerogravity"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zerogravity"
|
name = "zerogravity"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "OpenAI-compatible proxy for Google Antigravity"
|
description = "OpenAI-compatible proxy for Google Antigravity"
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ Explain to the user what this project unlocks — not what it _is_, but what bec
|
|||||||
- **Linux:** `./scripts/setup-linux.sh` → creates UID-isolated system user, iptables redirect, systemd service, builds binary. Then `sudo ./scripts/mitm-redirect.sh install` for MITM interception. Start with `zg start`.
|
- **Linux:** `./scripts/setup-linux.sh` → creates UID-isolated system user, iptables redirect, systemd service, builds binary. Then `sudo ./scripts/mitm-redirect.sh install` for MITM interception. Start with `zg start`.
|
||||||
- **macOS:** `./scripts/setup-macos.sh` → launchd plist, config dirs, builds binary. Start with `zg start`.
|
- **macOS:** `./scripts/setup-macos.sh` → launchd plist, config dirs, builds binary. Start with `zg start`.
|
||||||
- **Windows:** `powershell -ExecutionPolicy Bypass -File scripts\setup-windows.ps1` (as Admin) → scheduled task, config dirs, builds binary. Start with `schtasks /run /tn "ZeroGravity Proxy"`.
|
- **Windows:** `powershell -ExecutionPolicy Bypass -File scripts\setup-windows.ps1` (as Admin) → scheduled task, config dirs, builds binary. Start with `schtasks /run /tn "ZeroGravity Proxy"`.
|
||||||
- **Prerequisites:** Rust toolchain (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`). Linux also needs `iptables`.
|
- **Prerequisites:** Rust toolchain (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`). Linux also needs `iptables`, `gcc`, `jq`, and `curl`.
|
||||||
- **Auth:** They need an OAuth token from Antigravity. If the app is installed on the same machine, auto-refresh works via `state.vscdb`. Otherwise: `ZEROGRAVITY_TOKEN=ya29.xxx` env var, or `~/.config/zerogravity-token` file, or runtime `POST /v1/token`.
|
- **Auth:** They need an OAuth token from Antigravity. If the app is installed on the same machine, auto-refresh works via `state.vscdb`. Otherwise: `ZEROGRAVITY_TOKEN=ya29.xxx` env var, or `~/.config/zerogravity/token` file, or runtime `POST /v1/token`.
|
||||||
- **Test it:** `zg test "say hi"` or `curl http://localhost:8741/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"gemini-3-flash","messages":[{"role":"user","content":"hi"}]}'`
|
- **Test it:** `zg test "say hi"` or `curl http://localhost:8741/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"gemini-3-flash","messages":[{"role":"user","content":"hi"}]}'`
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -102,7 +102,7 @@ zg start
|
|||||||
The proxy needs an OAuth token:
|
The proxy needs an OAuth token:
|
||||||
|
|
||||||
1. **Env var**: `ZEROGRAVITY_TOKEN=ya29.xxx`
|
1. **Env var**: `ZEROGRAVITY_TOKEN=ya29.xxx`
|
||||||
2. **Token file**: `~/.config/zerogravity-token`
|
2. **Token file**: `~/.config/zerogravity/token`
|
||||||
3. **Runtime**: `curl -X POST http://localhost:8741/v1/token -d '{"token":"ya29.xxx"}'`
|
3. **Runtime**: `curl -X POST http://localhost:8741/v1/token -d '{"token":"ya29.xxx"}'`
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -130,7 +130,7 @@ Extract the OAuth token (starts with ya29.) from this cURL command and give me j
|
|||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Prerequisites: Rust toolchain, iptables
|
# Prerequisites: Rust toolchain, iptables, gcc, jq, curl
|
||||||
./scripts/setup-linux.sh
|
./scripts/setup-linux.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
MITM_PORT="${2:-8742}"
|
MITM_PORT="${2:-8742}"
|
||||||
LS_USER="zerogravity-ls"
|
LS_USER="zerogravity-ls"
|
||||||
DATA_DIR="/tmp/antigravity-standalone"
|
DATA_DIR="/tmp/zerogravity-standalone"
|
||||||
LS_BINARY="/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64"
|
LS_BINARY="/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64"
|
||||||
SUDOERS_FILE="/etc/sudoers.d/zerogravity-ls"
|
SUDOERS_FILE="/etc/sudoers.d/zerogravity-ls"
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# ── 0. Dependency check ──
|
||||||
|
MISSING=()
|
||||||
|
for cmd in cargo curl jq gcc sudo iptables; do
|
||||||
|
command -v "$cmd" &>/dev/null || MISSING+=("$cmd")
|
||||||
|
done
|
||||||
|
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||||
|
echo "✗ Missing dependencies: ${MISSING[*]}"
|
||||||
|
echo " Install them first, then re-run this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ── 1. System user for UID isolation ──
|
# ── 1. System user for UID isolation ──
|
||||||
echo "→ Creating zerogravity-ls system user…"
|
echo "→ Creating zerogravity-ls system user…"
|
||||||
if id -u zerogravity-ls &>/dev/null; then
|
if id -u zerogravity-ls &>/dev/null; then
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# Run as: powershell -ExecutionPolicy Bypass -File scripts\setup-windows.ps1
|
# Run as: powershell -ExecutionPolicy Bypass -File scripts\setup-windows.ps1
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$ProjectDir = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
$ProjectDir = Split-Path -Parent $PSScriptRoot
|
||||||
if (-not $ProjectDir) { $ProjectDir = (Get-Location).Path }
|
if (-not $ProjectDir) { $ProjectDir = (Get-Location).Path }
|
||||||
|
|
||||||
# ── 1. Config directory ──
|
# ── 1. Config directory ──
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ impl Backend {
|
|||||||
|
|
||||||
/// Get current OAuth token.
|
/// Get current OAuth token.
|
||||||
///
|
///
|
||||||
/// Priority: token file > env var > cached value.
|
/// Priority: token file > env var > state.vscdb > cached value.
|
||||||
/// Uses async I/O for file reads. Single write-lock acquisition
|
/// Uses async I/O for file reads. Single write-lock acquisition
|
||||||
/// eliminates the TOCTOU race of read-check-then-write.
|
/// eliminates the TOCTOU race of read-check-then-write.
|
||||||
pub async fn oauth_token(&self) -> String {
|
pub async fn oauth_token(&self) -> String {
|
||||||
@@ -168,6 +168,22 @@ impl Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then state.vscdb (blocking I/O — run on spawn_blocking)
|
||||||
|
if let Ok(Some(token)) = tokio::task::spawn_blocking(|| {
|
||||||
|
crate::standalone::read_oauth_from_state_db().map(|(t, _)| t)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if !token.is_empty() && token.starts_with("ya29.") {
|
||||||
|
let mut guard = self.inner.write().await;
|
||||||
|
if guard.oauth_token != token {
|
||||||
|
info!("Token updated from state.vscdb");
|
||||||
|
guard.oauth_token = token.clone();
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.inner.read().await.oauth_token.clone()
|
self.inner.read().await.oauth_token.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,6 +632,12 @@ fn discover() -> Result<BackendInner, String> {
|
|||||||
.map(|s| s.trim().to_string())
|
.map(|s| s.trim().to_string())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
})
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
// Fallback: read from Antigravity's state.vscdb
|
||||||
|
crate::standalone::read_oauth_from_state_db()
|
||||||
|
.map(|(token, _)| token)
|
||||||
|
.filter(|t| !t.is_empty() && t.starts_with("ya29."))
|
||||||
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
Ok(BackendInner {
|
Ok(BackendInner {
|
||||||
|
|||||||
353
src/bin/zg.rs
353
src/bin/zg.rs
@@ -1,12 +1,18 @@
|
|||||||
//! `zg` — ZeroGravity daemon manager.
|
//! `zg` — ZeroGravity daemon manager.
|
||||||
//!
|
//!
|
||||||
//! All commands exit immediately (safe for agent use via fast-bash MCP).
|
//! All commands exit immediately (safe for agent use via fast-bash MCP).
|
||||||
|
//! Platform-aware: uses systemd on Linux, launchctl on macOS, and direct
|
||||||
|
//! process management on Windows.
|
||||||
|
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
const SERVICE: &str = "zerogravity";
|
const SERVICE: &str = "zerogravity";
|
||||||
const PORT: u16 = 8741;
|
const PORT: u16 = 8741;
|
||||||
|
|
||||||
|
// macOS plist identifier
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
const PLIST_LABEL: &str = "com.zerogravity.proxy";
|
||||||
|
|
||||||
// ANSI colors
|
// ANSI colors
|
||||||
const RED: &str = "\x1b[0;31m";
|
const RED: &str = "\x1b[0;31m";
|
||||||
const GREEN: &str = "\x1b[0;32m";
|
const GREEN: &str = "\x1b[0;32m";
|
||||||
@@ -70,6 +76,19 @@ fn project_dir() -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn binary_path() -> String {
|
||||||
|
let dir = project_dir();
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
format!("{dir}\\target\\release\\zerogravity.exe")
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
format!("{dir}/target/release/zerogravity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn base_url() -> String {
|
fn base_url() -> String {
|
||||||
let port = std::env::var("PROXY_PORT")
|
let port = std::env::var("PROXY_PORT")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -78,6 +97,63 @@ fn base_url() -> String {
|
|||||||
format!("http://localhost:{port}")
|
format!("http://localhost:{port}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Platform service management ──
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn svc_start() -> bool {
|
||||||
|
systemctl(&["daemon-reload"]);
|
||||||
|
systemctl(&["start", SERVICE])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn svc_stop() -> bool {
|
||||||
|
systemctl(&["stop", SERVICE])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn svc_status() {
|
||||||
|
let output = Command::new("systemctl")
|
||||||
|
.args(["--user", "status", SERVICE, "--no-pager"])
|
||||||
|
.output();
|
||||||
|
match output {
|
||||||
|
Ok(o) => {
|
||||||
|
let text = String::from_utf8_lossy(&o.stdout);
|
||||||
|
for (i, line) in text.lines().enumerate() {
|
||||||
|
if i >= 6 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
println!("{line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => println!("{RED}Not running{NC}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn svc_logs(n: &str, follow: bool) {
|
||||||
|
let mut args = vec!["--user", "-u", SERVICE, "--no-pager", "-n", n];
|
||||||
|
if follow {
|
||||||
|
args.push("-f");
|
||||||
|
}
|
||||||
|
let _ = Command::new("journalctl")
|
||||||
|
.args(&args)
|
||||||
|
.stdin(Stdio::inherit())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn svc_logs_all() {
|
||||||
|
let _ = Command::new("journalctl")
|
||||||
|
.args(["--user", "-u", SERVICE, "--no-pager"])
|
||||||
|
.stdin(Stdio::inherit())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
fn systemctl(args: &[&str]) -> bool {
|
fn systemctl(args: &[&str]) -> bool {
|
||||||
Command::new("systemctl")
|
Command::new("systemctl")
|
||||||
.arg("--user")
|
.arg("--user")
|
||||||
@@ -87,6 +163,202 @@ fn systemctl(args: &[&str]) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn svc_show_fail_logs() {
|
||||||
|
let _ = Command::new("journalctl")
|
||||||
|
.args(["--user", "-u", SERVICE, "--no-pager", "-n", "20"])
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── macOS: launchctl ──
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn plist_path() -> String {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
|
||||||
|
format!("{home}/Library/LaunchAgents/{PLIST_LABEL}.plist")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn svc_start() -> bool {
|
||||||
|
let plist = plist_path();
|
||||||
|
if !std::path::Path::new(&plist).exists() {
|
||||||
|
eprintln!("{RED}Plist not found at {plist}{NC}");
|
||||||
|
eprintln!(" Run ./scripts/setup-macos.sh first");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// bootout first to ensure clean state (ignore errors)
|
||||||
|
let uid = Command::new("id")
|
||||||
|
.arg("-u")
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||||
|
.unwrap_or_else(|| "501".into());
|
||||||
|
let _ = Command::new("launchctl")
|
||||||
|
.args(["bootout", &format!("gui/{uid}"), &plist])
|
||||||
|
.status();
|
||||||
|
Command::new("launchctl")
|
||||||
|
.args(["load", &plist])
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn svc_stop() -> bool {
|
||||||
|
let plist = plist_path();
|
||||||
|
Command::new("launchctl")
|
||||||
|
.args(["unload", &plist])
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn svc_status() {
|
||||||
|
let output = Command::new("launchctl")
|
||||||
|
.args(["list", PLIST_LABEL])
|
||||||
|
.output();
|
||||||
|
match output {
|
||||||
|
Ok(o) if o.status.success() => {
|
||||||
|
let text = String::from_utf8_lossy(&o.stdout);
|
||||||
|
println!("{text}");
|
||||||
|
}
|
||||||
|
_ => println!("{RED}Not loaded{NC}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn svc_logs(n: &str, follow: bool) {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
|
||||||
|
let log = format!("{home}/Library/Logs/zerogravity.log");
|
||||||
|
if !std::path::Path::new(&log).exists() {
|
||||||
|
println!("{DIM}(no log file yet){NC}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut args = vec!["-n", n];
|
||||||
|
if follow {
|
||||||
|
args.push("-f");
|
||||||
|
}
|
||||||
|
args.push(&log);
|
||||||
|
let _ = Command::new("tail")
|
||||||
|
.args(&args)
|
||||||
|
.stdin(Stdio::inherit())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn svc_logs_all() {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
|
||||||
|
let log = format!("{home}/Library/Logs/zerogravity.log");
|
||||||
|
if std::path::Path::new(&log).exists() {
|
||||||
|
let _ = Command::new("cat")
|
||||||
|
.arg(&log)
|
||||||
|
.stdin(Stdio::inherit())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.status();
|
||||||
|
} else {
|
||||||
|
println!("{DIM}(no log file yet){NC}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn svc_show_fail_logs() {
|
||||||
|
svc_logs("20", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Windows / other: direct process management ──
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
fn svc_start() -> bool {
|
||||||
|
let bin = binary_path();
|
||||||
|
if !std::path::Path::new(&bin).exists() {
|
||||||
|
eprintln!("{RED}Binary not found: {bin}{NC}");
|
||||||
|
eprintln!(" Run `cargo build --release` first");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Try starting via scheduled task on Windows
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let result = Command::new("schtasks")
|
||||||
|
.args(["/run", "/tn", "ZeroGravity Proxy"])
|
||||||
|
.status()
|
||||||
|
.map(|s| s.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if result {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Fallback: start directly (detached)
|
||||||
|
eprintln!("{YELLOW}Scheduled task not found, starting directly...{NC}");
|
||||||
|
}
|
||||||
|
// Fallback for all non-linux/macos: spawn detached
|
||||||
|
match Command::new(&bin)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{RED}Failed to start: {e}{NC}");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
fn svc_stop() -> bool {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = Command::new("schtasks")
|
||||||
|
.args(["/end", "/tn", "ZeroGravity Proxy"])
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
// Also try killing by process name
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = Command::new("taskkill")
|
||||||
|
.args(["/F", "/IM", "zerogravity.exe"])
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
let _ = Command::new("pkill")
|
||||||
|
.args(["-f", "zerogravity"])
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
fn svc_status() {
|
||||||
|
if health_ok() {
|
||||||
|
println!("{GREEN}Running{NC} (responding on port {PORT})");
|
||||||
|
} else {
|
||||||
|
println!("{RED}Not responding on port {PORT}{NC}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
fn svc_logs(_n: &str, _follow: bool) {
|
||||||
|
println!("{DIM}Logs not available via zg on this platform.{NC}");
|
||||||
|
println!(" Check the console output where zerogravity was started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
fn svc_logs_all() {
|
||||||
|
svc_logs("0", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
fn svc_show_fail_logs() {
|
||||||
|
println!("{DIM}Check the console output where zerogravity was started.{NC}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shared helpers ──
|
||||||
|
|
||||||
fn curl_get(path: &str) -> Option<String> {
|
fn curl_get(path: &str) -> Option<String> {
|
||||||
let url = format!("{}{}", base_url(), path);
|
let url = format!("{}{}", base_url(), path);
|
||||||
Command::new("curl")
|
Command::new("curl")
|
||||||
@@ -119,16 +391,29 @@ fn health_ok() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn jq_print(json: &str) {
|
fn jq_print(json: &str) {
|
||||||
let mut child = Command::new("jq")
|
// Try jq first, fall back to raw JSON
|
||||||
|
match Command::new("jq")
|
||||||
.arg(".")
|
.arg(".")
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("jq not found");
|
{
|
||||||
if let Some(stdin) = child.stdin.as_mut() {
|
Ok(mut child) => {
|
||||||
use std::io::Write;
|
// Drop stdin before wait so jq sees EOF and doesn't hang
|
||||||
let _ = stdin.write_all(json.as_bytes());
|
{
|
||||||
|
use std::io::Write;
|
||||||
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
|
let _ = stdin.write_all(json.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = child.wait();
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// jq not installed — print raw
|
||||||
|
println!("{json}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let _ = child.wait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Commands ──
|
// ── Commands ──
|
||||||
@@ -154,8 +439,10 @@ fn do_build() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn do_start() {
|
fn do_start() {
|
||||||
systemctl(&["daemon-reload"]);
|
if !svc_start() {
|
||||||
systemctl(&["start", SERVICE]);
|
eprintln!("{RED}Failed to start service.{NC}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
println!("{GREEN}Started.{NC} Waiting for ready...");
|
println!("{GREEN}Started.{NC} Waiting for ready...");
|
||||||
|
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
@@ -167,14 +454,12 @@ fn do_start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
eprintln!("{RED}Proxy didn't become healthy in 10s. Check logs:{NC}");
|
eprintln!("{RED}Proxy didn't become healthy in 10s. Check logs:{NC}");
|
||||||
let _ = Command::new("journalctl")
|
svc_show_fail_logs();
|
||||||
.args(["--user", "-u", SERVICE, "--no-pager", "-n", "20"])
|
|
||||||
.status();
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_stop() {
|
fn do_stop() {
|
||||||
let _ = systemctl(&["stop", SERVICE]);
|
let _ = svc_stop();
|
||||||
println!("{YELLOW}Stopped.{NC}");
|
println!("{YELLOW}Stopped.{NC}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,22 +472,7 @@ fn do_restart() {
|
|||||||
|
|
||||||
fn do_status() {
|
fn do_status() {
|
||||||
println!("{BOLD}── Service ──{NC}");
|
println!("{BOLD}── Service ──{NC}");
|
||||||
let output = Command::new("systemctl")
|
svc_status();
|
||||||
.args(["--user", "status", SERVICE, "--no-pager"])
|
|
||||||
.output();
|
|
||||||
match output {
|
|
||||||
Ok(o) => {
|
|
||||||
let text = String::from_utf8_lossy(&o.stdout);
|
|
||||||
// Print first 6 lines
|
|
||||||
for (i, line) in text.lines().enumerate() {
|
|
||||||
if i >= 6 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
println!("{line}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => println!("{RED}Not running{NC}"),
|
|
||||||
}
|
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
if !health_ok() {
|
if !health_ok() {
|
||||||
@@ -232,32 +502,23 @@ fn do_status() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn do_logs(n: &str, follow: bool) {
|
fn do_logs(n: &str, follow: bool) {
|
||||||
let mut args = vec!["--user", "-u", SERVICE, "--no-pager", "-n", n];
|
svc_logs(n, follow);
|
||||||
if follow {
|
|
||||||
args.push("-f");
|
|
||||||
}
|
|
||||||
let _ = Command::new("journalctl")
|
|
||||||
.args(&args)
|
|
||||||
.stdin(Stdio::inherit())
|
|
||||||
.stdout(Stdio::inherit())
|
|
||||||
.stderr(Stdio::inherit())
|
|
||||||
.status();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_logs_all() {
|
fn do_logs_all() {
|
||||||
let _ = Command::new("journalctl")
|
svc_logs_all();
|
||||||
.args(["--user", "-u", SERVICE, "--no-pager"])
|
|
||||||
.stdin(Stdio::inherit())
|
|
||||||
.stdout(Stdio::inherit())
|
|
||||||
.stderr(Stdio::inherit())
|
|
||||||
.status();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_test(msg: &str) {
|
fn do_test(msg: &str) {
|
||||||
println!("{CYAN}Testing:{NC} {msg}");
|
println!("{CYAN}Testing:{NC} {msg}");
|
||||||
|
let escaped = msg
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('"', "\\\"")
|
||||||
|
.replace('\n', "\\n")
|
||||||
|
.replace('\r', "\\r")
|
||||||
|
.replace('\t', "\\t");
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"{{"model":"gemini-3-flash","input":"{}","stream":false,"timeout":30}}"#,
|
r#"{{"model":"gemini-3-flash","input":"{escaped}","stream":false,"timeout":30}}"#
|
||||||
msg.replace('"', r#"\""#)
|
|
||||||
);
|
);
|
||||||
match curl_post("/v1/responses", &body) {
|
match curl_post("/v1/responses", &body) {
|
||||||
Some(json) => jq_print(&json),
|
Some(json) => jq_print(&json),
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ fn extract_binary_versions(install_dir: &str) -> (Option<String>, Option<String>
|
|||||||
return (None, None);
|
return (None, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use grep -oP on the binary to avoid loading the whole thing into memory
|
// Use grep -oE on the binary to avoid loading the whole thing into memory
|
||||||
let chrome = Command::new("sh")
|
let chrome = Command::new("sh")
|
||||||
.args([
|
.args([
|
||||||
"-c",
|
"-c",
|
||||||
&format!(
|
&format!(
|
||||||
"strings '{}' | grep -oP 'Chrome/[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+' | head -1",
|
"strings '{}' | grep -oE 'Chrome/[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+' | head -1",
|
||||||
binary
|
binary
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
@@ -124,7 +124,7 @@ fn extract_binary_versions(install_dir: &str) -> (Option<String>, Option<String>
|
|||||||
.args([
|
.args([
|
||||||
"-c",
|
"-c",
|
||||||
&format!(
|
&format!(
|
||||||
"strings '{}' | grep -oP 'Electron/[0-9]+\\.[0-9]+\\.[0-9]+' | head -1",
|
"strings '{}' | grep -oE 'Electron/[0-9]+\\.[0-9]+\\.[0-9]+' | head -1",
|
||||||
binary
|
binary
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
@@ -234,8 +234,9 @@ pub fn token_file_path() -> String {
|
|||||||
|
|
||||||
/// User-Agent string matching the Electron webview — computed once.
|
/// User-Agent string matching the Electron webview — computed once.
|
||||||
pub static USER_AGENT: LazyLock<String> = LazyLock::new(|| {
|
pub static USER_AGENT: LazyLock<String> = LazyLock::new(|| {
|
||||||
|
let os_part = user_agent_os_part();
|
||||||
format!(
|
format!(
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
|
"Mozilla/5.0 ({os_part}) AppleWebKit/537.36 \
|
||||||
(KHTML, like Gecko) Antigravity/{} \
|
(KHTML, like Gecko) Antigravity/{} \
|
||||||
Chrome/{} Electron/{} Safari/537.36",
|
Chrome/{} Electron/{} Safari/537.36",
|
||||||
antigravity_version(),
|
antigravity_version(),
|
||||||
@@ -244,6 +245,22 @@ pub static USER_AGENT: LazyLock<String> = LazyLock::new(|| {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Returns the OS portion of the User-Agent string matching real Electron/Chrome.
|
||||||
|
fn user_agent_os_part() -> &'static str {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
"Macintosh; Intel Mac OS X 10_15_7"
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
"Windows NT 10.0; Win64; x64"
|
||||||
|
}
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
"X11; Linux x86_64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Chrome major version for sec-ch-ua header — computed once.
|
/// Chrome major version for sec-ch-ua header — computed once.
|
||||||
pub static CHROME_MAJOR: LazyLock<String> = LazyLock::new(|| {
|
pub static CHROME_MAJOR: LazyLock<String> = LazyLock::new(|| {
|
||||||
chrome_version()
|
chrome_version()
|
||||||
|
|||||||
35
src/main.rs
35
src/main.rs
@@ -113,16 +113,31 @@ async fn main() {
|
|||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Port in use — try to kill whatever's holding it
|
// Port in use — try to kill whatever's holding it
|
||||||
eprintln!(" Port {} in use, killing stale process...", cli.port);
|
eprintln!(" Port {} in use, killing stale process...", cli.port);
|
||||||
let _ = std::process::Command::new("sh")
|
#[cfg(unix)]
|
||||||
.args([
|
{
|
||||||
"-c",
|
let _ = std::process::Command::new("sh")
|
||||||
&format!("kill $(lsof -ti:{}) 2>/dev/null; sleep 0.3", cli.port),
|
.args([
|
||||||
])
|
"-c",
|
||||||
.status();
|
&format!("kill $(lsof -ti:{}) 2>/dev/null; sleep 0.3", cli.port),
|
||||||
// Also kill any leftover standalone LS processes
|
])
|
||||||
let _ = std::process::Command::new("pkill")
|
.status();
|
||||||
.args(["-f", "language_server.*antigravity-standalone"])
|
let _ = std::process::Command::new("pkill")
|
||||||
.status();
|
.args(["-f", "language_server.*antigravity-standalone"])
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
// Windows: find PID via netstat and kill it
|
||||||
|
let _ = std::process::Command::new("cmd")
|
||||||
|
.args([
|
||||||
|
"/C",
|
||||||
|
&format!(
|
||||||
|
"for /f \"tokens=5\" %a in ('netstat -aon ^| findstr :{} ^| findstr LISTENING') do taskkill /PID %a /F",
|
||||||
|
cli.port
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.status();
|
||||||
|
}
|
||||||
// Retry once
|
// Retry once
|
||||||
match tokio::net::TcpListener::bind(&addr).await {
|
match tokio::net::TcpListener::bind(&addr).await {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ pub(super) fn cleanup_orphaned_ls() {
|
|||||||
/// The DB stores the exact Topic proto bytes under key `antigravityUnifiedStateSync.oauthToken`.
|
/// The DB stores the exact Topic proto bytes under key `antigravityUnifiedStateSync.oauthToken`.
|
||||||
/// This includes access_token + refresh_token + expiry, allowing the LS to auto-refresh.
|
/// This includes access_token + refresh_token + expiry, allowing the LS to auto-refresh.
|
||||||
/// Returns (access_token, topic_proto_bytes) or None if unavailable.
|
/// Returns (access_token, topic_proto_bytes) or None if unavailable.
|
||||||
pub(super) fn read_oauth_from_state_db() -> Option<(String, Vec<u8>)> {
|
pub fn read_oauth_from_state_db() -> Option<(String, Vec<u8>)> {
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
|
||||||
let db_path = paths().state_db_path;
|
let db_path = paths().state_db_path;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use tracing::info;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// Re-export public API
|
// Re-export public API
|
||||||
|
pub use discovery::read_oauth_from_state_db;
|
||||||
pub use spawn::StandaloneLS;
|
pub use spawn::StandaloneLS;
|
||||||
|
|
||||||
/// Source for the DNS redirect preload library (compiled at runtime, Linux only).
|
/// Source for the DNS redirect preload library (compiled at runtime, Linux only).
|
||||||
|
|||||||
Reference in New Issue
Block a user