mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 10:05:18 +00:00
feat: gigacode (#92)
This commit is contained in:
parent
0a73d1d8e8
commit
a02393436c
40 changed files with 2736 additions and 1327 deletions
17
server/packages/gigacode/Cargo.toml
Normal file
17
server/packages/gigacode/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "gigacode"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
description = "Sandbox Agent CLI with OpenCode attach by default"
|
||||
repository.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "gigacode"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
sandbox-agent.workspace = true
|
||||
tracing.workspace = true
|
||||
80
server/packages/gigacode/README.md
Normal file
80
server/packages/gigacode/README.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# GigaCode
|
||||
|
||||
Use [OpenCode](https://opencode.ai)'s UI with any coding agent.
|
||||
|
||||
Supports Claude Code, Codex, and Amp.
|
||||
|
||||
This is __not__ a fork. It's powered by [Sandbox Agent SDK](https://sandboxagent.dev)'s wizardry.
|
||||
|
||||
> **Experimental**: This project is under active development. Please report bugs on [GitHub Issues](https://github.com/rivet-dev/sandbox-agent/issues) or join our [Discord](https://rivet.dev/discord).
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─ GigaCode ────────────────────────────────────────────────────────┐
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ OpenCode TUI │───▶│ Sandbox Agent │───▶│ Claude Code / │ │
|
||||
│ │ │ │ │ │ Codex / Amp │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- [Sandbox Agent SDK](https://sandboxagent.dev) provides a universal HTTP API for controlling Claude Code, Codex, and Amp
|
||||
- Sandbox Agent SDK exposes an [OpenCode-compatible endpoint](https://sandboxagent.dev/opencode-compatibility) so OpenCode can talk to any agent
|
||||
- OpenCode connects to Sandbox Agent SDK via [`attach`](https://opencode.ai/docs/cli/#attach)
|
||||
|
||||
## Install
|
||||
|
||||
**macOS / Linux / WSL (Recommended)**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://releases.rivet.dev/sandbox-agent/latest/gigacode-install.sh | sh
|
||||
```
|
||||
|
||||
**npm i -g**
|
||||
|
||||
```bash
|
||||
npm install -g gigacode
|
||||
gigacode --help
|
||||
```
|
||||
|
||||
**bun add -g**
|
||||
|
||||
```bash
|
||||
bun add -g gigacode
|
||||
# Allow Bun to run postinstall scripts for native binaries.
|
||||
bun pm -g trust gigacode-linux-x64 gigacode-linux-arm64 gigacode-darwin-arm64 gigacode-darwin-x64 gigacode-win32-x64
|
||||
gigacode --help
|
||||
```
|
||||
|
||||
**npx**
|
||||
|
||||
```bash
|
||||
npx gigacode --help
|
||||
```
|
||||
|
||||
**bunx**
|
||||
|
||||
```bash
|
||||
bunx gigacode --help
|
||||
```
|
||||
|
||||
> **Note:** Windows is unsupported. Please use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
|
||||
## Usage
|
||||
|
||||
**TUI**
|
||||
|
||||
Launch the OpenCode TUI with any coding agent:
|
||||
|
||||
```bash
|
||||
gigacode
|
||||
```
|
||||
|
||||
**Web UI**
|
||||
|
||||
Use the [OpenCode Web UI](https://sandboxagent.dev/opencode-compatibility) to control any coding agent from the browser.
|
||||
|
||||
**OpenCode SDK**
|
||||
|
||||
Use the [`@opencode-ai/sdk`](https://sandboxagent.dev/opencode-compatibility) to programmatically control any coding agent.
|
||||
28
server/packages/gigacode/src/main.rs
Normal file
28
server/packages/gigacode/src/main.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use clap::Parser;
|
||||
use sandbox_agent::cli::{
|
||||
CliConfig, CliError, Command, GigacodeCli, OpencodeArgs, init_logging, run_command,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
tracing::error!(error = %err, "gigacode failed");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), CliError> {
|
||||
let cli = GigacodeCli::parse();
|
||||
let config = CliConfig {
|
||||
token: cli.token,
|
||||
no_token: cli.no_token,
|
||||
gigacode: true,
|
||||
};
|
||||
let command = cli
|
||||
.command
|
||||
.unwrap_or_else(|| Command::Opencode(OpencodeArgs::default()));
|
||||
if let Err(err) = init_logging(&command) {
|
||||
eprintln!("failed to init logging: {err}");
|
||||
return Err(err);
|
||||
}
|
||||
run_command(&command, &config)
|
||||
}
|
||||
|
|
@ -19,9 +19,22 @@ fn main() {
|
|||
println!("cargo:rerun-if-env-changed=SANDBOX_AGENT_VERSION");
|
||||
println!("cargo:rerun-if-changed={}", dist_dir.display());
|
||||
|
||||
// Rebuild when the git HEAD changes so BUILD_ID stays current.
|
||||
let git_head = manifest_dir.join(".git/HEAD");
|
||||
if git_head.exists() {
|
||||
println!("cargo:rerun-if-changed={}", git_head.display());
|
||||
} else {
|
||||
// In a workspace the .git dir lives at the repo root.
|
||||
let root_git_head = root_dir.join(".git/HEAD");
|
||||
if root_git_head.exists() {
|
||||
println!("cargo:rerun-if-changed={}", root_git_head.display());
|
||||
}
|
||||
}
|
||||
|
||||
// Generate version constant from environment variable or fallback to Cargo.toml version
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
|
||||
generate_version(&out_dir);
|
||||
generate_build_id(&out_dir);
|
||||
|
||||
let skip = env::var("SANDBOX_AGENT_SKIP_INSPECTOR").is_ok();
|
||||
let out_file = out_dir.join("inspector_assets.rs");
|
||||
|
|
@ -81,3 +94,33 @@ fn generate_version(out_dir: &Path) {
|
|||
|
||||
fs::write(&out_file, contents).expect("write version.rs");
|
||||
}
|
||||
|
||||
fn generate_build_id(out_dir: &Path) {
|
||||
use std::process::Command;
|
||||
|
||||
let build_id = Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| {
|
||||
// Fallback: use the package version + compile-time timestamp
|
||||
let version = env::var("CARGO_PKG_VERSION").unwrap_or_default();
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs().to_string())
|
||||
.unwrap_or_default();
|
||||
format!("{version}-{timestamp}")
|
||||
});
|
||||
|
||||
let out_file = out_dir.join("build_id.rs");
|
||||
let contents = format!(
|
||||
"/// Unique identifier for this build (git short hash or version-timestamp fallback).\n\
|
||||
pub const BUILD_ID: &str = \"{}\";\n",
|
||||
build_id
|
||||
);
|
||||
|
||||
fs::write(&out_file, contents).expect("write build_id.rs");
|
||||
}
|
||||
|
|
|
|||
1320
server/packages/sandbox-agent/src/cli.rs
Normal file
1320
server/packages/sandbox-agent/src/cli.rs
Normal file
File diff suppressed because it is too large
Load diff
487
server/packages/sandbox-agent/src/daemon.rs
Normal file
487
server/packages/sandbox-agent/src/daemon.rs
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command as ProcessCommand, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use reqwest::blocking::Client as HttpClient;
|
||||
|
||||
use crate::cli::{CliConfig, CliError};
|
||||
|
||||
mod build_id {
|
||||
include!(concat!(env!("OUT_DIR"), "/build_id.rs"));
|
||||
}
|
||||
|
||||
pub use build_id::BUILD_ID;
|
||||
|
||||
const DAEMON_HEALTH_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn daemon_state_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.map(|dir| dir.join("sandbox-agent").join("daemon"))
|
||||
.unwrap_or_else(|| PathBuf::from(".").join(".sandbox-agent").join("daemon"))
|
||||
}
|
||||
|
||||
pub fn sanitize_host(host: &str) -> String {
|
||||
host.chars()
|
||||
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' })
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn daemon_pid_path(host: &str, port: u16) -> PathBuf {
|
||||
let name = format!("daemon-{}-{}.pid", sanitize_host(host), port);
|
||||
daemon_state_dir().join(name)
|
||||
}
|
||||
|
||||
pub fn daemon_log_path(host: &str, port: u16) -> PathBuf {
|
||||
let name = format!("daemon-{}-{}.log", sanitize_host(host), port);
|
||||
daemon_state_dir().join(name)
|
||||
}
|
||||
|
||||
pub fn daemon_version_path(host: &str, port: u16) -> PathBuf {
|
||||
let name = format!("daemon-{}-{}.version", sanitize_host(host), port);
|
||||
daemon_state_dir().join(name)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PID helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn read_pid(path: &Path) -> Option<u32> {
|
||||
let text = fs::read_to_string(path).ok()?;
|
||||
text.trim().parse::<u32>().ok()
|
||||
}
|
||||
|
||||
pub fn write_pid(path: &Path, pid: u32) -> Result<(), CliError> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, pid.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_pid(path: &Path) -> Result<(), CliError> {
|
||||
if path.exists() {
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Version helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn read_daemon_version(host: &str, port: u16) -> Option<String> {
|
||||
let path = daemon_version_path(host, port);
|
||||
let text = fs::read_to_string(path).ok()?;
|
||||
Some(text.trim().to_string())
|
||||
}
|
||||
|
||||
pub fn write_daemon_version(host: &str, port: u16) -> Result<(), CliError> {
|
||||
let path = daemon_version_path(host, port);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(&path, BUILD_ID)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_version_file(host: &str, port: u16) -> Result<(), CliError> {
|
||||
let path = daemon_version_path(host, port);
|
||||
if path.exists() {
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_version_current(host: &str, port: u16) -> bool {
|
||||
match read_daemon_version(host, port) {
|
||||
Some(v) => v == BUILD_ID,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Process helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn is_process_running(pid: u32) -> bool {
|
||||
let result = unsafe { libc::kill(pid as i32, 0) };
|
||||
if result == 0 {
|
||||
return true;
|
||||
}
|
||||
match std::io::Error::last_os_error().raw_os_error() {
|
||||
Some(code) if code == libc::EPERM => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn is_process_running(pid: u32) -> bool {
|
||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
use windows::Win32::System::Threading::{
|
||||
GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
||||
if handle.is_invalid() {
|
||||
return false;
|
||||
}
|
||||
let mut exit_code = 0u32;
|
||||
let ok = GetExitCodeProcess(handle, &mut exit_code).as_bool();
|
||||
let _ = CloseHandle(handle);
|
||||
ok && exit_code == 259
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health checks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn check_health(base_url: &str, token: Option<&str>) -> Result<bool, CliError> {
|
||||
let client = HttpClient::builder().build()?;
|
||||
let url = format!("{base_url}/v1/health");
|
||||
let mut request = client.get(url);
|
||||
if let Some(token) = token {
|
||||
request = request.bearer_auth(token);
|
||||
}
|
||||
match request.send() {
|
||||
Ok(response) if response.status().is_success() => Ok(true),
|
||||
Ok(_) => Ok(false),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wait_for_health(
|
||||
mut server_child: Option<&mut Child>,
|
||||
base_url: &str,
|
||||
token: Option<&str>,
|
||||
timeout: Duration,
|
||||
) -> Result<(), CliError> {
|
||||
let client = HttpClient::builder().build()?;
|
||||
let deadline = Instant::now() + timeout;
|
||||
|
||||
while Instant::now() < deadline {
|
||||
if let Some(child) = server_child.as_mut() {
|
||||
if let Some(status) = child.try_wait()? {
|
||||
return Err(CliError::Server(format!(
|
||||
"sandbox-agent exited before becoming healthy ({status})"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let url = format!("{base_url}/v1/health");
|
||||
let mut request = client.get(&url);
|
||||
if let Some(token) = token {
|
||||
request = request.bearer_auth(token);
|
||||
}
|
||||
match request.send() {
|
||||
Ok(response) if response.status().is_success() => return Ok(()),
|
||||
_ => {
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(CliError::Server(
|
||||
"timed out waiting for sandbox-agent health".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spawn
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn spawn_sandbox_agent_daemon(
|
||||
cli: &CliConfig,
|
||||
host: &str,
|
||||
port: u16,
|
||||
token: Option<&str>,
|
||||
log_path: &Path,
|
||||
) -> Result<Child, CliError> {
|
||||
if let Some(parent) = log_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let log_file = fs::File::create(log_path)?;
|
||||
let log_file_err = log_file.try_clone()?;
|
||||
|
||||
let exe = std::env::current_exe()?;
|
||||
let mut cmd = ProcessCommand::new(exe);
|
||||
cmd.arg("server")
|
||||
.arg("--host")
|
||||
.arg(host)
|
||||
.arg("--port")
|
||||
.arg(port.to_string())
|
||||
.env("SANDBOX_AGENT_LOG_STDOUT", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::from(log_file))
|
||||
.stderr(Stdio::from(log_file_err));
|
||||
|
||||
if cli.no_token {
|
||||
cmd.arg("--no-token");
|
||||
} else if let Some(token) = token {
|
||||
cmd.arg("--token").arg(token);
|
||||
} else {
|
||||
return Err(CliError::MissingToken);
|
||||
}
|
||||
|
||||
cmd.spawn().map_err(CliError::from)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DaemonStatus
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DaemonStatus {
|
||||
Running {
|
||||
pid: u32,
|
||||
version: Option<String>,
|
||||
version_current: bool,
|
||||
log_path: PathBuf,
|
||||
},
|
||||
NotRunning,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DaemonStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DaemonStatus::Running {
|
||||
pid,
|
||||
version,
|
||||
version_current,
|
||||
log_path,
|
||||
} => {
|
||||
let version_str = version.as_deref().unwrap_or("unknown");
|
||||
let outdated = if *version_current {
|
||||
""
|
||||
} else {
|
||||
" [outdated, restart recommended]"
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"Daemon running (PID {pid}, build {version_str}, logs: {}){}",
|
||||
log_path.display(),
|
||||
outdated
|
||||
)
|
||||
}
|
||||
DaemonStatus::NotRunning => write!(f, "Daemon not running"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// High-level commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn status(host: &str, port: u16, token: Option<&str>) -> Result<DaemonStatus, CliError> {
|
||||
let pid_path = daemon_pid_path(host, port);
|
||||
let log_path = daemon_log_path(host, port);
|
||||
|
||||
if let Some(pid) = read_pid(&pid_path) {
|
||||
if is_process_running(pid) {
|
||||
let version = read_daemon_version(host, port);
|
||||
let version_current = is_version_current(host, port);
|
||||
return Ok(DaemonStatus::Running {
|
||||
pid,
|
||||
version,
|
||||
version_current,
|
||||
log_path,
|
||||
});
|
||||
}
|
||||
// Stale PID file
|
||||
let _ = remove_pid(&pid_path);
|
||||
let _ = remove_version_file(host, port);
|
||||
}
|
||||
|
||||
// Also try a health check in case the daemon is running but we lost the PID file
|
||||
let base_url = format!("http://{host}:{port}");
|
||||
if check_health(&base_url, token)? {
|
||||
return Ok(DaemonStatus::Running {
|
||||
pid: 0,
|
||||
version: read_daemon_version(host, port),
|
||||
version_current: is_version_current(host, port),
|
||||
log_path,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(DaemonStatus::NotRunning)
|
||||
}
|
||||
|
||||
pub fn start(
|
||||
cli: &CliConfig,
|
||||
host: &str,
|
||||
port: u16,
|
||||
token: Option<&str>,
|
||||
) -> Result<(), CliError> {
|
||||
let base_url = format!("http://{host}:{port}");
|
||||
let pid_path = daemon_pid_path(host, port);
|
||||
let log_path = daemon_log_path(host, port);
|
||||
|
||||
// Already healthy?
|
||||
if check_health(&base_url, token)? {
|
||||
eprintln!("daemon already running at {base_url}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Stale PID?
|
||||
if let Some(pid) = read_pid(&pid_path) {
|
||||
if is_process_running(pid) {
|
||||
eprintln!("daemon process {pid} exists; waiting for health");
|
||||
return wait_for_health(None, &base_url, token, DAEMON_HEALTH_TIMEOUT);
|
||||
}
|
||||
let _ = remove_pid(&pid_path);
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"starting daemon at {base_url} (logs: {})",
|
||||
log_path.display()
|
||||
);
|
||||
|
||||
let mut child = spawn_sandbox_agent_daemon(cli, host, port, token, &log_path)?;
|
||||
let pid = child.id();
|
||||
write_pid(&pid_path, pid)?;
|
||||
write_daemon_version(host, port)?;
|
||||
|
||||
let result = wait_for_health(Some(&mut child), &base_url, token, DAEMON_HEALTH_TIMEOUT);
|
||||
if result.is_err() {
|
||||
let _ = remove_pid(&pid_path);
|
||||
let _ = remove_version_file(host, port);
|
||||
return result;
|
||||
}
|
||||
|
||||
eprintln!("daemon started (PID {pid}, logs: {})", log_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn stop(host: &str, port: u16) -> Result<(), CliError> {
|
||||
let pid_path = daemon_pid_path(host, port);
|
||||
|
||||
let pid = match read_pid(&pid_path) {
|
||||
Some(pid) => pid,
|
||||
None => {
|
||||
eprintln!("daemon is not running (no PID file)");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if !is_process_running(pid) {
|
||||
eprintln!("daemon is not running (stale PID file)");
|
||||
let _ = remove_pid(&pid_path);
|
||||
let _ = remove_version_file(host, port);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eprintln!("stopping daemon (PID {pid})...");
|
||||
|
||||
// SIGTERM
|
||||
unsafe {
|
||||
libc::kill(pid as i32, libc::SIGTERM);
|
||||
}
|
||||
|
||||
// Wait up to 5 seconds for graceful exit
|
||||
for _ in 0..50 {
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
if !is_process_running(pid) {
|
||||
let _ = remove_pid(&pid_path);
|
||||
let _ = remove_version_file(host, port);
|
||||
eprintln!("daemon stopped");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// SIGKILL
|
||||
eprintln!("daemon did not stop gracefully, sending SIGKILL...");
|
||||
unsafe {
|
||||
libc::kill(pid as i32, libc::SIGKILL);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let _ = remove_pid(&pid_path);
|
||||
let _ = remove_version_file(host, port);
|
||||
eprintln!("daemon killed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn stop(host: &str, port: u16) -> Result<(), CliError> {
|
||||
let pid_path = daemon_pid_path(host, port);
|
||||
|
||||
let pid = match read_pid(&pid_path) {
|
||||
Some(pid) => pid,
|
||||
None => {
|
||||
eprintln!("daemon is not running (no PID file)");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if !is_process_running(pid) {
|
||||
eprintln!("daemon is not running (stale PID file)");
|
||||
let _ = remove_pid(&pid_path);
|
||||
let _ = remove_version_file(host, port);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
eprintln!("stopping daemon (PID {pid})...");
|
||||
|
||||
// Use taskkill on Windows
|
||||
let _ = ProcessCommand::new("taskkill")
|
||||
.args(["/PID", &pid.to_string(), "/F"])
|
||||
.status();
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
let _ = remove_pid(&pid_path);
|
||||
let _ = remove_version_file(host, port);
|
||||
eprintln!("daemon stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_running(
|
||||
cli: &CliConfig,
|
||||
host: &str,
|
||||
port: u16,
|
||||
token: Option<&str>,
|
||||
) -> Result<(), CliError> {
|
||||
let base_url = format!("http://{host}:{port}");
|
||||
let pid_path = daemon_pid_path(host, port);
|
||||
|
||||
// Check if daemon is already healthy
|
||||
if check_health(&base_url, token)? {
|
||||
// Check build version
|
||||
if !is_version_current(host, port) {
|
||||
let old = read_daemon_version(host, port).unwrap_or_else(|| "unknown".to_string());
|
||||
eprintln!(
|
||||
"daemon outdated (build {old} -> {BUILD_ID}), restarting..."
|
||||
);
|
||||
stop(host, port)?;
|
||||
return start(cli, host, port, token);
|
||||
}
|
||||
let log_path = daemon_log_path(host, port);
|
||||
if let Some(pid) = read_pid(&pid_path) {
|
||||
eprintln!(
|
||||
"daemon already running at {base_url} (PID {pid}, logs: {})",
|
||||
log_path.display()
|
||||
);
|
||||
} else {
|
||||
eprintln!("daemon already running at {base_url}");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Not healthy — check for stale PID
|
||||
if let Some(pid) = read_pid(&pid_path) {
|
||||
if is_process_running(pid) {
|
||||
eprintln!("daemon process {pid} running; waiting for health");
|
||||
return wait_for_health(None, &base_url, token, DAEMON_HEALTH_TIMEOUT);
|
||||
}
|
||||
let _ = remove_pid(&pid_path);
|
||||
let _ = remove_version_file(host, port);
|
||||
}
|
||||
|
||||
start(cli, host, port, token)
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
mod agent_server_logs;
|
||||
pub mod credentials;
|
||||
pub mod daemon;
|
||||
pub mod opencode_compat;
|
||||
pub mod router;
|
||||
pub mod server_logs;
|
||||
pub mod telemetry;
|
||||
pub mod ui;
|
||||
pub mod cli;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue