feat: rebrand to ZeroGravity, replace proxyctl with zg Rust binary

Phase 1 - Rename:
- Crate: antigravity-proxy -> zerogravity
- Env: ANTIGRAVITY_OAUTH_TOKEN -> ZEROGRAVITY_TOKEN
- Paths: ~/.config/antigravity-proxy -> ~/.config/zerogravity
- Paths: /tmp/antigravity-* -> /tmp/zerogravity-*
- User: antigravity-ls -> zerogravity-ls
- Service: antigravity-proxy -> zerogravity

Phase 2 - zg daemon manager:
- New Rust binary src/bin/zg.rs replaces scripts/proxyctl bash
- Commands: start, stop, restart, rebuild, status, logs, test, health
- Auto-resolves project dir from binary location
- All commands exit immediately (safe for agent fast-bash)
This commit is contained in:
Nikketryhard
2026-02-18 01:54:54 -06:00
parent 409ee97405
commit 00587fcce8
18 changed files with 403 additions and 122 deletions

273
src/bin/zg.rs Normal file
View File

@@ -0,0 +1,273 @@
//! `zg` — ZeroGravity daemon manager.
//!
//! All commands exit immediately (safe for agent use via fast-bash MCP).
use std::process::{Command, Stdio};
const SERVICE: &str = "zerogravity";
const PORT: u16 = 8741;
// ANSI colors
const RED: &str = "\x1b[0;31m";
const GREEN: &str = "\x1b[0;32m";
const YELLOW: &str = "\x1b[1;33m";
const CYAN: &str = "\x1b[0;36m";
const BOLD: &str = "\x1b[1m";
const DIM: &str = "\x1b[2m";
const NC: &str = "\x1b[0m";
fn main() {
let args: Vec<String> = std::env::args().collect();
let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("");
let arg = args.get(2).map(|s| s.as_str());
match cmd {
"start" => do_start(),
"stop" => do_stop(),
"restart" => do_restart(),
"rebuild" => do_build(),
"status" => do_status(),
"logs" => do_logs(arg.unwrap_or("30"), false),
"logs-follow" => do_logs(arg.unwrap_or("30"), true),
"logs-all" => do_logs_all(),
"test" => do_test(arg.unwrap_or("Say hello in exactly 3 words")),
"health" => do_health(),
_ => usage(),
}
}
fn usage() {
println!("{BOLD}zg{NC} — ZeroGravity daemon manager\n");
println!(" {CYAN}start{NC} Start the proxy daemon");
println!(" {CYAN}stop{NC} Stop the proxy daemon");
println!(" {CYAN}restart{NC} Rebuild + restart");
println!(" {CYAN}rebuild{NC} Build release binary only");
println!(" {CYAN}status{NC} Service status + quota + usage");
println!(" {CYAN}logs{NC} [N] Show last N lines (default 30)");
println!(" {CYAN}logs-follow{NC} [N] Tail last N lines + follow");
println!(" {CYAN}logs-all{NC} Full log dump");
println!(" {CYAN}test{NC} [msg] Quick test request (gemini-3-flash)");
println!(" {CYAN}health{NC} Health check");
}
fn project_dir() -> String {
// Resolve project dir from binary location:
// binary is at <project>/target/release/zg or <project>/target/debug/zg
let exe = std::env::current_exe().expect("cannot resolve exe path");
let exe_dir = exe.parent().expect("no parent dir");
// Walk up from target/release/ or target/debug/
if let Some(target_dir) = exe_dir.parent() {
if let Some(project) = target_dir.parent() {
return project.to_string_lossy().to_string();
}
}
// Fallback: use current dir
std::env::current_dir()
.expect("no cwd")
.to_string_lossy()
.to_string()
}
fn base_url() -> String {
let port = std::env::var("PROXY_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(PORT);
format!("http://localhost:{port}")
}
fn systemctl(args: &[&str]) -> bool {
Command::new("systemctl")
.arg("--user")
.args(args)
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn curl_get(path: &str) -> Option<String> {
let url = format!("{}{}", base_url(), path);
Command::new("curl")
.args(["-sf", &url])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
}
fn curl_post(path: &str, body: &str) -> Option<String> {
let url = format!("{}{}", base_url(), path);
Command::new("curl")
.args(["-sf", &url, "-H", "Content-Type: application/json", "-d", body])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
}
fn health_ok() -> bool {
curl_get("/health").is_some()
}
fn jq_print(json: &str) {
let mut child = Command::new("jq")
.arg(".")
.stdin(Stdio::piped())
.spawn()
.expect("jq not found");
if let Some(stdin) = child.stdin.as_mut() {
use std::io::Write;
let _ = stdin.write_all(json.as_bytes());
}
let _ = child.wait();
}
// ── Commands ──
fn do_build() {
let dir = project_dir();
println!("{YELLOW}Building release binary...{NC}");
let status = Command::new("cargo")
.args(["build", "--release"])
.current_dir(&dir)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.expect("failed to run cargo");
if status.success() {
println!("{GREEN}Build complete.{NC}");
} else {
eprintln!("{RED}Build failed.{NC}");
std::process::exit(1);
}
}
fn do_start() {
systemctl(&["daemon-reload"]);
systemctl(&["start", SERVICE]);
println!("{GREEN}Started.{NC} Waiting for ready...");
for _ in 0..20 {
if health_ok() {
println!("{GREEN}ZeroGravity is up on port {PORT}.{NC}");
return;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
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();
std::process::exit(1);
}
fn do_stop() {
let _ = systemctl(&["stop", SERVICE]);
println!("{YELLOW}Stopped.{NC}");
}
fn do_restart() {
println!("{YELLOW}Stopping...{NC}");
do_stop();
do_build();
do_start();
}
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}"),
}
println!();
if !health_ok() {
println!("{RED}Proxy is not responding on port {PORT}.{NC}");
std::process::exit(1);
}
println!("{BOLD}── Quota ──{NC}");
match curl_get("/v1/quota") {
Some(json) => jq_print(&json),
None => println!("{DIM}(no quota data){NC}"),
}
println!();
println!("{BOLD}── Usage ──{NC}");
match curl_get("/v1/usage") {
Some(json) => jq_print(&json),
None => println!("{DIM}(no usage data){NC}"),
}
println!();
println!("{BOLD}── Sessions ──{NC}");
match curl_get("/v1/sessions") {
Some(json) => jq_print(&json),
None => println!("{DIM}(no sessions){NC}"),
}
}
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();
}
fn do_logs_all() {
let _ = Command::new("journalctl")
.args(["--user", "-u", SERVICE, "--no-pager"])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status();
}
fn do_test(msg: &str) {
println!("{CYAN}Testing:{NC} {msg}");
let body = format!(
r#"{{"model":"gemini-3-flash","input":"{}","stream":false,"timeout":30}}"#,
msg.replace('"', r#"\""#)
);
match curl_post("/v1/responses", &body) {
Some(json) => jq_print(&json),
None => {
eprintln!("{RED}Request failed. Is the proxy running?{NC}");
std::process::exit(1);
}
}
}
fn do_health() {
match curl_get("/health") {
Some(json) => {
jq_print(&json);
println!("{GREEN}Healthy{NC}");
}
None => {
println!("{RED}Not responding{NC}");
std::process::exit(1);
}
}
}