use crate::schema_version;
use std::ffi::OsStr;
use std::fs;
use std::io::Error;
use std::path::Path;
include!(concat!(env!("OUT_DIR"), "/.generated_templates.rs"));
const MAIN_ARGS_CALL: &str = " let cmd_args = args::build_cli();";
const PROVENANCE_HOOK_SNIPPET: &str = r#" let cmd_args = args::build_cli();
if cmd_args.subcommand_matches("version").is_some() {
print_agent_version_status();
return;
}
if let Some(sub_m) = cmd_args.subcommand_matches("inspect") {
print_agent_inspect(sub_m.get_flag("json"));
return;
}"#;
const GENERATED_AGENT_VERSION_BLOCK_TEMPLATE: &str =
include_str!("templates/agent_version_block.rs.tmpl");
const MANIFEST_VERSION_PLACEHOLDER: &str = "__CARGO_AI_PACKAGE_VERSION__";
const ROOT_EXCLUDED_TEMPLATE_ENTRIES_FOR_CHECK: &[&str] = &["target"];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AgentSyncState {
InSync,
OutOfSync,
Unknown,
}
#[cfg_attr(not(test), allow(dead_code))]
fn determine_agent_sync_state(
generated_by_version: &str,
generated_template_version: &str,
local_cargo_ai_version: Option<&str>,
local_template_version: Option<&str>,
) -> AgentSyncState {
match (local_cargo_ai_version, local_template_version) {
(Some(local_cargo_ai), Some(local_template)) => {
if local_cargo_ai == generated_by_version
&& local_template == generated_template_version
{
AgentSyncState::InSync
} else {
AgentSyncState::OutOfSync
}
}
_ => AgentSyncState::Unknown,
}
}
fn sync_state_label(state: AgentSyncState) -> &'static str {
match state {
AgentSyncState::InSync => "in_sync",
AgentSyncState::OutOfSync => "out_of_sync",
AgentSyncState::Unknown => "unknown",
}
}
fn rust_string_literal(value: &str) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
}
fn provenance_block(generated_by_version: &str, template_schema_version: &str) -> String {
GENERATED_AGENT_VERSION_BLOCK_TEMPLATE
.replace(
"__GENERATED_BY_CARGO_AI_VERSION__",
&rust_string_literal(generated_by_version),
)
.replace(
"__GENERATED_WITH_TEMPLATE_SCHEMA_VERSION__",
&rust_string_literal(template_schema_version),
)
.replace(
"__SYNC_STATUS_IN_SYNC__",
&rust_string_literal(sync_state_label(AgentSyncState::InSync)),
)
.replace(
"__SYNC_STATUS_OUT_OF_SYNC__",
&rust_string_literal(sync_state_label(AgentSyncState::OutOfSync)),
)
.replace(
"__SYNC_STATUS_UNKNOWN__",
&rust_string_literal(sync_state_label(AgentSyncState::Unknown)),
)
}
fn inject_version_command_hook(main_source: &str) -> String {
if !main_source.contains(MAIN_ARGS_CALL) {
return main_source.to_string();
}
main_source.replacen(MAIN_ARGS_CALL, PROVENANCE_HOOK_SNIPPET, 1)
}
fn render_workspace_file_contents(
file_name: &str,
template_contents: &str,
agent_name: &str,
generated_by_version: &str,
template_schema_version: &str,
) -> String {
let mut rendered = if should_replace_agent_identity(file_name) {
template_contents
.replace("cargo-ai", agent_name)
.replace("cargo_ai", agent_name)
} else {
template_contents.to_string()
};
if file_name == "Cargo.toml" {
rendered = rendered.replace(MANIFEST_VERSION_PLACEHOLDER, generated_by_version);
}
if file_name == "src/main.rs" {
rendered = inject_version_command_hook(&rendered);
rendered.push_str(&provenance_block(
generated_by_version,
template_schema_version,
));
}
rendered
}
fn should_replace_agent_identity(file_name: &str) -> bool {
matches!(file_name, "Cargo.toml" | "src/args.rs")
}
pub fn create_new_agent_project(
template_path: &Path,
agent_name: &str,
agentcfg: Result<String, Error>,
) -> Result<(), Error> {
create_agent_workspace(agent_name)?;
seed_agent_workspace(template_path, agent_name)?;
rewrite_agent_generated_files(
&super::agent_workspace_path(agent_name),
agent_name,
agentcfg,
)?;
Ok(())
}
fn create_agent_workspace(agent_name: &str) -> Result<(), Error> {
let agent_workspace_directory = super::agent_workspace_path(agent_name);
if !agent_workspace_directory.exists() {
fs::create_dir_all(agent_workspace_directory)?;
}
Ok(())
}
pub(crate) fn create_template_project(
base_path: &Path,
template_agent_name: &str,
template_agentcfg: &str,
) -> Result<(), Error> {
if !base_path.exists() {
fs::create_dir_all(base_path)?;
}
write_workspace_files(
base_path,
template_agent_name,
Some(template_agentcfg),
WorkspaceWriteMode::WriteAll,
)
}
fn seed_agent_workspace(template_path: &Path, agent_name: &str) -> Result<(), Error> {
let workspace_path = super::agent_workspace_path(agent_name);
copy_directory_recursive_excluding_root_entries(
template_path,
&workspace_path,
ROOT_EXCLUDED_TEMPLATE_ENTRIES_FOR_CHECK,
)
}
fn rewrite_agent_generated_files(
base_path: &Path,
agent_name: &str,
agentcfg: Result<String, Error>,
) -> Result<(), Error> {
let provided_agentcfg = agentcfg.ok();
write_workspace_files(
base_path,
agent_name,
provided_agentcfg.as_deref(),
WorkspaceWriteMode::GeneratedOnly,
)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum WorkspaceWriteMode {
WriteAll,
GeneratedOnly,
}
fn write_workspace_files(
base_path: &Path,
agent_name: &str,
provided_agentcfg: Option<&str>,
mode: WorkspaceWriteMode,
) -> Result<(), Error> {
let generated_by_version = env!("CARGO_PKG_VERSION");
let template_schema_version = provided_agentcfg
.and_then(schema_version::extract_schema_version_from_agentcfg)
.unwrap_or_else(schema_version::current_schema_version);
for (file_name, file_contents) in TEMPLATES {
let file_path = base_path.join(file_name);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
if file_name == ".agentcfg" {
if let Some(contents) = provided_agentcfg {
fs::write(file_path, contents)?;
continue;
}
}
if file_name.ends_with("loader.rs") {
if mode == WorkspaceWriteMode::WriteAll {
fs::write(file_path, file_contents)?;
}
continue;
}
let rendered = render_workspace_file_contents(
file_name,
file_contents,
agent_name,
generated_by_version,
&template_schema_version,
);
let should_write = match mode {
WorkspaceWriteMode::WriteAll => true,
WorkspaceWriteMode::GeneratedOnly => rendered != file_contents,
};
if should_write {
fs::write(file_path, rendered)?;
}
}
Ok(())
}
fn copy_directory_recursive(source: &Path, destination: &Path) -> Result<(), Error> {
fs::create_dir_all(destination)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let source_path = entry.path();
let destination_path = destination.join(entry.file_name());
let file_type = entry.file_type()?;
if file_type.is_dir() {
fs::create_dir_all(&destination_path)?;
copy_directory_recursive(&source_path, &destination_path)?;
} else {
fs::copy(&source_path, &destination_path)?;
}
}
Ok(())
}
fn copy_directory_recursive_excluding_root_entries(
source: &Path,
destination: &Path,
excluded_root_entries: &[&str],
) -> Result<(), Error> {
fs::create_dir_all(destination)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let source_path = entry.path();
let destination_path = destination.join(entry.file_name());
let file_type = entry.file_type()?;
let file_name = entry.file_name();
if excluded_root_entries
.iter()
.any(|excluded| file_name == OsStr::new(excluded))
{
continue;
}
if file_type.is_dir() {
fs::create_dir_all(&destination_path)?;
copy_directory_recursive(&source_path, &destination_path)?;
} else {
fs::copy(&source_path, &destination_path)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
copy_directory_recursive, copy_directory_recursive_excluding_root_entries,
determine_agent_sync_state, render_workspace_file_contents, should_replace_agent_identity,
sync_state_label, AgentSyncState,
};
use crate::schema_version;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
const CURRENT_CARGO_AI_VERSION: &str = env!("CARGO_PKG_VERSION");
const DIFFERENT_CARGO_AI_VERSION: &str = "9.9.9";
fn temp_dir_path(stem: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
std::env::temp_dir().join(format!("cargo-ai-project-{stem}-{nanos}"))
}
#[test]
fn resolves_template_schema_version_from_agentcfg_version() {
let version = schema_version::extract_schema_version_from_agentcfg(
r#"{"version":"2026-03-03.r2","inputs":[{"type":"text","text":"x"}],"agent_schema":{"type":"object","properties":{}},"actions":[]}"#,
);
assert_eq!(version.as_deref(), Some("2026-03-03.r2"));
}
#[test]
fn rejects_legacy_semver_agentcfg_version() {
let version = schema_version::extract_schema_version_from_agentcfg(
r#"{"version":"0.0.10","inputs":[{"type":"text","text":"x"}],"agent_schema":{"type":"object","properties":{}},"actions":[]}"#,
);
assert!(version.is_none());
}
#[test]
fn falls_back_to_current_schema_version_when_agentcfg_version_is_missing() {
let version = schema_version::extract_schema_version_from_agentcfg(
r#"{"inputs":[{"type":"text","text":"x"}],"agent_schema":{"type":"object","properties":{}},"actions":[]}"#,
);
assert!(version.is_none());
let fallback = schema_version::current_schema_version();
assert!(schema_version::is_valid_schema_version(&fallback));
}
#[test]
fn stamps_main_template_with_provenance_metadata() {
let rendered = render_workspace_file_contents(
"src/main.rs",
"fn main() {\n let cmd_args = args::build_cli();\n}\n",
"adder_agent",
CURRENT_CARGO_AI_VERSION,
"2026-03-03.r1",
);
assert!(rendered.contains("print_agent_version_status();"));
assert!(rendered.contains(r#"subcommand_matches("version")"#));
assert!(rendered.contains(r#"subcommand_matches("inspect")"#));
assert!(rendered.contains("print_agent_inspect(sub_m.get_flag(\"json\"));"));
assert!(rendered.contains("Agent version status"));
assert!(rendered.contains("generated_agent_provenance"));
assert!(rendered.contains("generated_agent_inspect_value"));
assert!(rendered.contains("generated_by_cargo_ai_version"));
assert!(rendered.contains("generated_with_template_schema_version"));
assert!(rendered.contains("agent_build_id"));
assert!(rendered.contains("target_triple"));
assert!(rendered.contains("definition_sha256"));
assert!(rendered.contains("embedded_definition_json"));
assert!(rendered.contains("build_timestamp_utc"));
assert!(rendered.contains("cargo_ai_metadata"));
assert!(rendered.contains(CURRENT_CARGO_AI_VERSION));
assert!(rendered.contains("2026-03-03.r1"));
}
#[test]
fn non_main_templates_do_not_get_provenance_block() {
let rendered = render_workspace_file_contents(
"src/args.rs",
"const BIN: &str = \"cargo-ai\";\n",
"adder_agent",
CURRENT_CARGO_AI_VERSION,
"2026-03-03.r1",
);
assert!(rendered.contains("adder_agent"));
assert!(!rendered.contains("generated_agent_provenance"));
}
#[test]
fn replaces_agent_identity_only_in_manifest_and_cli_files() {
assert!(should_replace_agent_identity("Cargo.toml"));
assert!(should_replace_agent_identity("src/args.rs"));
assert!(!should_replace_agent_identity("src/main.rs"));
assert!(!should_replace_agent_identity("src/credentials/store.rs"));
}
#[test]
fn preserves_shared_cargo_ai_paths_and_urls_in_runtime_files() {
let rendered = render_workspace_file_contents(
"src/main.rs",
r#"const INFRA_BASE_URL: &str = "https://api.cargo-ai.org";
const KEYCHAIN_SERVICE: &str = "cargo-ai";
const PATH: &str = ".cargo/.cargo-ai/credentials.toml";
fn main() {
let cmd_args = args::build_cli();
}
"#,
"number_2_email",
CURRENT_CARGO_AI_VERSION,
"2026-03-03.r1",
);
assert!(rendered.contains("https://api.cargo-ai.org"));
assert!(rendered.contains(r#"const KEYCHAIN_SERVICE: &str = "cargo-ai";"#));
assert!(rendered.contains(r#".cargo/.cargo-ai/credentials.toml"#));
assert!(!rendered.contains("https://api.number_2_email.org"));
assert!(!rendered.contains(".cargo/.number_2_email/credentials.toml"));
}
#[test]
fn still_rewrites_manifest_package_name() {
let rendered = render_workspace_file_contents(
"Cargo.toml",
"[package]\nname = \"cargo-ai\"\nversion = \"__CARGO_AI_PACKAGE_VERSION__\"\n",
"number_2_email",
CURRENT_CARGO_AI_VERSION,
"2026-03-03.r1",
);
assert!(rendered.contains("name = \"number_2_email\""));
assert!(rendered.contains(&format!("version = \"{CURRENT_CARGO_AI_VERSION}\"")));
}
#[test]
fn sync_state_is_in_sync_when_versions_match() {
let state = determine_agent_sync_state(
CURRENT_CARGO_AI_VERSION,
"2026-03-03.r1",
Some(CURRENT_CARGO_AI_VERSION),
Some("2026-03-03.r1"),
);
assert_eq!(state, AgentSyncState::InSync);
assert_eq!(sync_state_label(state), "in_sync");
}
#[test]
fn sync_state_is_out_of_sync_for_exact_mismatch() {
let state = determine_agent_sync_state(
CURRENT_CARGO_AI_VERSION,
"2026-03-03.r1",
Some(DIFFERENT_CARGO_AI_VERSION),
Some("2026-03-03.r1"),
);
assert_eq!(state, AgentSyncState::OutOfSync);
assert_eq!(sync_state_label(state), "out_of_sync");
}
#[test]
fn sync_state_is_unknown_when_local_baseline_missing() {
let state = determine_agent_sync_state(
CURRENT_CARGO_AI_VERSION,
"2026-03-03.r1",
None,
Some("2026-03-03.r1"),
);
assert_eq!(state, AgentSyncState::Unknown);
assert_eq!(sync_state_label(state), "unknown");
}
#[test]
fn recursive_copy_copies_nested_files() {
let source = temp_dir_path("copy-src");
let destination = temp_dir_path("copy-dst");
fs::create_dir_all(source.join("src")).expect("source tree should be creatable");
fs::write(source.join("Cargo.toml"), "name = 'seed'").expect("cargo file should write");
fs::write(source.join("src").join("main.rs"), "fn main() {}")
.expect("main file should write");
copy_directory_recursive(&source, &destination).expect("copy should succeed");
assert_eq!(
fs::read_to_string(destination.join("Cargo.toml"))
.ok()
.as_deref(),
Some("name = 'seed'")
);
assert_eq!(
fs::read_to_string(destination.join("src").join("main.rs"))
.ok()
.as_deref(),
Some("fn main() {}")
);
let _ = fs::remove_dir_all(source);
let _ = fs::remove_dir_all(destination);
}
#[test]
fn root_exclusion_copy_skips_target_directory_only_at_root() {
let source = temp_dir_path("copy-skip-src");
let destination = temp_dir_path("copy-skip-dst");
fs::create_dir_all(source.join("src")).expect("source tree should be creatable");
fs::create_dir_all(source.join("target").join("debug"))
.expect("root target tree should be creatable");
fs::write(source.join("Cargo.toml"), "name = 'seed'").expect("cargo file should write");
fs::write(source.join("src").join("main.rs"), "fn main() {}")
.expect("main file should write");
fs::write(source.join("target").join("debug").join("seed"), "binary")
.expect("binary file should write");
copy_directory_recursive_excluding_root_entries(&source, &destination, &["target"])
.expect("copy with root exclusions should succeed");
assert!(destination.join("Cargo.toml").exists());
assert!(destination.join("src").join("main.rs").exists());
assert!(!destination.join("target").exists());
let _ = fs::remove_dir_all(source);
let _ = fs::remove_dir_all(destination);
}
}