use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntegrationMode {
Binary,
WorkspaceMember,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ManifestType {
None,
Package,
Workspace,
}
pub struct CargoManager {
manifest_dir: PathBuf,
venus_path: PathBuf,
}
impl CargoManager {
pub fn new(manifest_dir: impl AsRef<Path>) -> Result<Self> {
let manifest_dir = manifest_dir.as_ref().to_path_buf();
let venus_path = Self::find_venus_crate()?;
Ok(Self {
manifest_dir,
venus_path,
})
}
fn find_venus_crate() -> Result<PathBuf> {
if let Ok(path) = std::env::var("VENUS_PATH") {
let path = PathBuf::from(path);
if path.exists() {
return Ok(path);
}
}
if let Ok(exe_path) = std::env::current_exe()
&& exe_path.starts_with(dirs::home_dir().unwrap_or_default().join(".cargo/bin"))
{
return Ok(PathBuf::from("venus")); }
if let Ok(exe_path) = std::env::current_exe() {
if let Some(target_dir) = exe_path.parent().and_then(|p| p.parent()) {
let venus_crate = target_dir
.parent()
.map(|repo_root| repo_root.join("crates/venus"));
if let Some(path) = venus_crate
&& path.exists()
{
return Ok(path);
}
}
}
bail!("Could not find venus crate. Set VENUS_PATH environment variable.")
}
fn detect_manifest_type(&self) -> Result<ManifestType> {
let manifest_path = self.manifest_dir.join("Cargo.toml");
if !manifest_path.exists() {
return Ok(ManifestType::None);
}
let content = fs::read_to_string(&manifest_path).context("Failed to read Cargo.toml")?;
if content.contains("[workspace]") {
Ok(ManifestType::Workspace)
} else if content.contains("[package]") {
Ok(ManifestType::Package)
} else {
bail!("Invalid Cargo.toml: missing [package] or [workspace]")
}
}
pub fn add_notebook(
&self,
notebook_name: &str,
notebook_path: &Path,
mode: IntegrationMode,
) -> Result<()> {
Self::validate_crate_name(notebook_name)?;
let manifest_type = self.detect_manifest_type()?;
match (manifest_type, mode) {
(ManifestType::None, IntegrationMode::Binary) => {
self.create_bin_manifest(notebook_name, notebook_path)?;
}
(ManifestType::None, IntegrationMode::WorkspaceMember) => {
self.create_workspace_manifest(notebook_name)?;
}
(ManifestType::Package, IntegrationMode::Binary) => {
self.add_bin_to_manifest(notebook_name, notebook_path)?;
}
(ManifestType::Package, IntegrationMode::WorkspaceMember) => {
bail!(
"Cannot create workspace member: Cargo.toml is a package, not a workspace. \
Convert it to a workspace first or use binary mode (remove --workspace flag)."
);
}
(ManifestType::Workspace, IntegrationMode::Binary) => {
bail!(
"Cannot add binary: Cargo.toml is a workspace root. \
Use --workspace flag to add as workspace member."
);
}
(ManifestType::Workspace, IntegrationMode::WorkspaceMember) => {
self.add_workspace_member(notebook_name)?;
}
}
Ok(())
}
fn validate_crate_name(name: &str) -> Result<()> {
if name.is_empty() {
bail!("Notebook name cannot be empty");
}
if !name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
bail!(
"Notebook name '{}' contains invalid characters. Use only alphanumeric, '-', or '_'.",
name
);
}
if name.starts_with(|c: char| c.is_numeric()) {
bail!("Notebook name '{}' cannot start with a number", name);
}
Ok(())
}
fn create_bin_manifest(&self, notebook_name: &str, notebook_path: &Path) -> Result<()> {
let manifest_path = self.manifest_dir.join("Cargo.toml");
let venus_dep = self.format_venus_dependency();
let bin_path = notebook_path.display();
let content = format!(
r#"[package]
name = "venus-notebooks"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "{notebook_name}"
path = "{bin_path}"
[dependencies]
{venus_dep}
serde = {{ version = "1", features = ["derive"] }}
"#
);
fs::write(&manifest_path, content).context("Failed to write Cargo.toml")?;
println!(
"✓ Created Cargo.toml with notebook '{}' as binary",
notebook_name
);
Ok(())
}
fn add_bin_to_manifest(&self, notebook_name: &str, notebook_path: &Path) -> Result<()> {
let manifest_path = self.manifest_dir.join("Cargo.toml");
let content = fs::read_to_string(&manifest_path)?;
if self.bin_exists(&content, notebook_name)? {
bail!("Binary '{}' already exists in Cargo.toml", notebook_name);
}
let bin_path = notebook_path.display();
let bin_entry = format!(
"\n[[bin]]\nname = \"{}\"\npath = \"{}\"\n",
notebook_name, bin_path
);
let new_content = if let Some(pos) = content.find("[dependencies]") {
let (before, after) = content.split_at(pos);
format!("{}{}{}", before, bin_entry, after)
} else {
format!("{}{}", content, bin_entry)
};
fs::write(&manifest_path, new_content).context("Failed to update Cargo.toml")?;
println!(
"✓ Added notebook '{}' as binary to Cargo.toml",
notebook_name
);
Ok(())
}
fn bin_exists(&self, content: &str, name: &str) -> Result<bool> {
let bin_pattern = format!("name = \"{}\"", name);
let mut in_bin_section = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[[bin]]" {
in_bin_section = true;
} else if trimmed.starts_with('[') && in_bin_section {
in_bin_section = false;
}
if in_bin_section && trimmed.contains(&bin_pattern) {
return Ok(true);
}
}
Ok(false)
}
fn create_workspace_manifest(&self, notebook_name: &str) -> Result<()> {
let manifest_path = self.manifest_dir.join("Cargo.toml");
let content = format!(
r#"[workspace]
members = ["{notebook_name}"]
resolver = "2"
"#
);
fs::write(&manifest_path, content).context("Failed to write Cargo.toml")?;
self.create_workspace_member(notebook_name)?;
println!("✓ Created workspace with member '{}'", notebook_name);
Ok(())
}
fn add_workspace_member(&self, notebook_name: &str) -> Result<()> {
let manifest_path = self.manifest_dir.join("Cargo.toml");
let content = fs::read_to_string(&manifest_path)?;
if content.contains(&format!("\"{}\"", notebook_name)) {
bail!(
"Workspace member '{}' already exists in Cargo.toml",
notebook_name
);
}
let new_content = if let Some(start) = content.find("members = [") {
let after_bracket = start + "members = [".len();
let before = &content[..after_bracket];
let after = &content[after_bracket..];
if after.trim_start().starts_with(']') {
format!("{}\"{}\"]{}", before, notebook_name, &after[1..])
} else {
format!("{}\"{}\", {}", before, notebook_name, after)
}
} else {
bail!("Could not find 'members' array in workspace Cargo.toml");
};
fs::write(&manifest_path, new_content).context("Failed to update Cargo.toml")?;
self.create_workspace_member(notebook_name)?;
println!("✓ Added workspace member '{}'", notebook_name);
Ok(())
}
fn create_workspace_member(&self, notebook_name: &str) -> Result<()> {
let member_dir = self.manifest_dir.join(notebook_name);
fs::create_dir_all(&member_dir).context("Failed to create workspace member directory")?;
let member_manifest = member_dir.join("Cargo.toml");
let venus_dep = self.format_venus_dependency();
let content = format!(
r#"[package]
name = "{notebook_name}"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "{notebook_name}"
path = "{notebook_name}.rs"
[dependencies]
{venus_dep}
serde = {{ version = "1", features = ["derive"] }}
"#
);
fs::write(&member_manifest, content).context("Failed to write member Cargo.toml")?;
Ok(())
}
fn format_venus_dependency(&self) -> String {
let path_str = self.venus_path.display().to_string();
if path_str == "venus" {
r#"venus = "0.1""#.to_string()
} else {
format!(r#"venus = {{ path = "{}" }}"#, path_str)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_crate_name() {
assert!(CargoManager::validate_crate_name("my_notebook").is_ok());
assert!(CargoManager::validate_crate_name("notebook-1").is_ok());
assert!(CargoManager::validate_crate_name("notebook_name_123").is_ok());
assert!(CargoManager::validate_crate_name("").is_err());
assert!(CargoManager::validate_crate_name("123start").is_err());
assert!(CargoManager::validate_crate_name("my notebook").is_err());
assert!(CargoManager::validate_crate_name("my/notebook").is_err());
}
#[test]
fn test_bin_exists() {
let manager = CargoManager {
manifest_dir: PathBuf::from("/tmp"),
venus_path: PathBuf::from("venus"),
};
let content = r#"
[package]
name = "test"
[[bin]]
name = "notebook1"
path = "notebook1.rs"
[[bin]]
name = "notebook2"
path = "notebook2.rs"
"#;
assert!(manager.bin_exists(content, "notebook1").unwrap());
assert!(manager.bin_exists(content, "notebook2").unwrap());
assert!(!manager.bin_exists(content, "notebook3").unwrap());
}
}