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:
273
src/bin/zg.rs
Normal file
273
src/bin/zg.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user