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:
353
src/bin/zg.rs
353
src/bin/zg.rs
@@ -1,12 +1,18 @@
|
||||
//! `zg` — ZeroGravity daemon manager.
|
||||
//!
|
||||
//! 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};
|
||||
|
||||
const SERVICE: &str = "zerogravity";
|
||||
const PORT: u16 = 8741;
|
||||
|
||||
// macOS plist identifier
|
||||
#[cfg(target_os = "macos")]
|
||||
const PLIST_LABEL: &str = "com.zerogravity.proxy";
|
||||
|
||||
// ANSI colors
|
||||
const RED: &str = "\x1b[0;31m";
|
||||
const GREEN: &str = "\x1b[0;32m";
|
||||
@@ -70,6 +76,19 @@ fn project_dir() -> 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 {
|
||||
let port = std::env::var("PROXY_PORT")
|
||||
.ok()
|
||||
@@ -78,6 +97,63 @@ fn base_url() -> String {
|
||||
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 {
|
||||
Command::new("systemctl")
|
||||
.arg("--user")
|
||||
@@ -87,6 +163,202 @@ fn systemctl(args: &[&str]) -> bool {
|
||||
.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> {
|
||||
let url = format!("{}{}", base_url(), path);
|
||||
Command::new("curl")
|
||||
@@ -119,16 +391,29 @@ fn health_ok() -> bool {
|
||||
}
|
||||
|
||||
fn jq_print(json: &str) {
|
||||
let mut child = Command::new("jq")
|
||||
// Try jq first, fall back to raw JSON
|
||||
match Command::new("jq")
|
||||
.arg(".")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.expect("jq not found");
|
||||
if let Some(stdin) = child.stdin.as_mut() {
|
||||
use std::io::Write;
|
||||
let _ = stdin.write_all(json.as_bytes());
|
||||
{
|
||||
Ok(mut child) => {
|
||||
// Drop stdin before wait so jq sees EOF and doesn't hang
|
||||
{
|
||||
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 ──
|
||||
@@ -154,8 +439,10 @@ fn do_build() {
|
||||
}
|
||||
|
||||
fn do_start() {
|
||||
systemctl(&["daemon-reload"]);
|
||||
systemctl(&["start", SERVICE]);
|
||||
if !svc_start() {
|
||||
eprintln!("{RED}Failed to start service.{NC}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("{GREEN}Started.{NC} Waiting for ready...");
|
||||
|
||||
for _ in 0..20 {
|
||||
@@ -167,14 +454,12 @@ fn do_start() {
|
||||
}
|
||||
|
||||
eprintln!("{RED}Proxy didn't become healthy in 10s. Check logs:{NC}");
|
||||
let _ = Command::new("journalctl")
|
||||
.args(["--user", "-u", SERVICE, "--no-pager", "-n", "20"])
|
||||
.status();
|
||||
svc_show_fail_logs();
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn do_stop() {
|
||||
let _ = systemctl(&["stop", SERVICE]);
|
||||
let _ = svc_stop();
|
||||
println!("{YELLOW}Stopped.{NC}");
|
||||
}
|
||||
|
||||
@@ -187,22 +472,7 @@ fn do_restart() {
|
||||
|
||||
fn do_status() {
|
||||
println!("{BOLD}── Service ──{NC}");
|
||||
let output = Command::new("systemctl")
|
||||
.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}"),
|
||||
}
|
||||
svc_status();
|
||||
println!();
|
||||
|
||||
if !health_ok() {
|
||||
@@ -232,32 +502,23 @@ fn do_status() {
|
||||
}
|
||||
|
||||
fn do_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();
|
||||
svc_logs(n, follow);
|
||||
}
|
||||
|
||||
fn do_logs_all() {
|
||||
let _ = Command::new("journalctl")
|
||||
.args(["--user", "-u", SERVICE, "--no-pager"])
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status();
|
||||
svc_logs_all();
|
||||
}
|
||||
|
||||
fn do_test(msg: &str) {
|
||||
println!("{CYAN}Testing:{NC} {msg}");
|
||||
let escaped = msg
|
||||
.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace('\n', "\\n")
|
||||
.replace('\r', "\\r")
|
||||
.replace('\t', "\\t");
|
||||
let body = format!(
|
||||
r#"{{"model":"gemini-3-flash","input":"{}","stream":false,"timeout":30}}"#,
|
||||
msg.replace('"', r#"\""#)
|
||||
r#"{{"model":"gemini-3-flash","input":"{escaped}","stream":false,"timeout":30}}"#
|
||||
);
|
||||
match curl_post("/v1/responses", &body) {
|
||||
Some(json) => jq_print(&json),
|
||||
|
||||
Reference in New Issue
Block a user