Adds the serve subcommand: an axum 0.7 + tower-http server on
:7700 backing the React dashboard. Routes:
GET /api/status /api/projects /api/snapshots
/api/sync-queue /api/hot /api/events (SSE 5s tick)
POST /api/activate /api/deactivate /api/sync
/api/gc /api/snapshot
Mutating endpoints shell out to /usr/local/bin/claw-store so
all CLI logic stays single-sourced. claw-store/static/ holds
the prebuilt dashboard Vite bundle for --static-dir.
cargo_init.rs gets a minor wiring tweak so the API can read
back the managed cargo config marker.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
114 lines
3.8 KiB
Rust
114 lines
3.8 KiB
Rust
use anyhow::{Context, Result};
|
|
use std::path::Path;
|
|
|
|
/// Write .cargo/config.toml in the project's warm directory pointing
|
|
/// CARGO_TARGET_DIR at the hot NVMe path. Preserves any existing non-[build]
|
|
/// sections already in the file. Safe to re-run (idempotent).
|
|
pub fn write_cargo_config(warm_path: &Path, hot_target_path: &Path) -> Result<()> {
|
|
let cargo_dir = warm_path.join(".cargo");
|
|
std::fs::create_dir_all(&cargo_dir)
|
|
.with_context(|| format!("creating .cargo dir at {}", cargo_dir.display()))?;
|
|
|
|
let target = hot_target_path.to_str()
|
|
.context("hot_target_path is not valid UTF-8")?;
|
|
|
|
let config_path = cargo_dir.join("config.toml");
|
|
|
|
// Read existing content; strip any prior [build] section so we can replace it.
|
|
let existing = if config_path.exists() {
|
|
std::fs::read_to_string(&config_path)?
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
// Remove old [build] block (from its header to the next section or EOF).
|
|
let stripped = strip_section(&existing, "build");
|
|
|
|
let new_content = format!(
|
|
"# claw-store managed — do not edit manually\n\
|
|
[build]\n\
|
|
target-dir = \"{}\"\n\
|
|
{}",
|
|
target,
|
|
stripped.trim_start()
|
|
);
|
|
|
|
std::fs::write(&config_path, new_content)
|
|
.with_context(|| format!("writing {}", config_path.display()))
|
|
}
|
|
|
|
/// Remove a TOML section `[name]` and all its key=value lines from `src`,
|
|
/// stopping at the next `[section]` header or EOF.
|
|
fn strip_section(src: &str, name: &str) -> String {
|
|
let header = format!("[{}]", name);
|
|
let mut out = String::new();
|
|
let mut in_section = false;
|
|
for line in src.lines() {
|
|
let trimmed = line.trim();
|
|
if trimmed == header {
|
|
in_section = true;
|
|
continue;
|
|
}
|
|
if in_section && trimmed.starts_with('[') {
|
|
in_section = false;
|
|
}
|
|
if !in_section {
|
|
out.push_str(line);
|
|
out.push('\n');
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Returns true if .cargo/config.toml exists and points to the expected hot path.
|
|
pub fn verify_cargo_config(warm_path: &Path, hot_target_path: &Path) -> Result<bool> {
|
|
let config_path = warm_path.join(".cargo/config.toml");
|
|
if !config_path.exists() { return Ok(false); }
|
|
let content = std::fs::read_to_string(&config_path)?;
|
|
let target = hot_target_path.to_str().unwrap_or("");
|
|
Ok(content.contains(target))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn test_write_cargo_config() {
|
|
let dir = TempDir::new().unwrap();
|
|
let warm = dir.path().join("my-project");
|
|
std::fs::create_dir_all(&warm).unwrap();
|
|
let hot_target: std::path::PathBuf = "/hot/targets/my-project".into();
|
|
|
|
write_cargo_config(&warm, &hot_target).unwrap();
|
|
|
|
let cargo_dir = warm.join(".cargo");
|
|
let config_file = cargo_dir.join("config.toml");
|
|
assert!(config_file.exists());
|
|
|
|
let content = std::fs::read_to_string(&config_file).unwrap();
|
|
assert!(content.contains("[build]"));
|
|
assert!(content.contains("/hot/targets/my-project"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verify_cargo_config_detects_correct() {
|
|
let dir = TempDir::new().unwrap();
|
|
let warm = dir.path().join("proj");
|
|
std::fs::create_dir_all(&warm).unwrap();
|
|
let hot: std::path::PathBuf = "/hot/targets/proj".into();
|
|
write_cargo_config(&warm, &hot).unwrap();
|
|
assert!(verify_cargo_config(&warm, &hot).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn test_verify_cargo_config_detects_missing() {
|
|
let dir = TempDir::new().unwrap();
|
|
let warm = dir.path().join("no-config-proj");
|
|
std::fs::create_dir_all(&warm).unwrap();
|
|
let hot: std::path::PathBuf = "/hot/targets/no-config-proj".into();
|
|
assert!(!verify_cargo_config(&warm, &hot).unwrap());
|
|
}
|
|
}
|