feat: Created a new .install command to install bundled assets on-demand

This commit is contained in:
2026-05-18 14:59:02 -06:00
parent 5bd0766a60
commit a3d67bfbf7
8 changed files with 222 additions and 18 deletions
+2 -2
View File
@@ -45,7 +45,7 @@ pub struct Agent {
}
impl Agent {
pub fn install_builtin_agents() -> Result<()> {
pub fn install_builtin_agents(force: bool) -> Result<()> {
info!(
"Installing built-in agents in {}",
paths::agents_data_dir().display()
@@ -65,7 +65,7 @@ impl Agent {
#[cfg_attr(not(unix), expect(unused))]
let is_script = matches!(file_extension.as_deref(), Some("sh") | Some("py"));
if file_path.exists() {
if file_path.exists() && !force {
debug!(
"Agent file already exists, skipping: {}",
file_path.display()
+2 -2
View File
@@ -85,7 +85,7 @@ impl Macro {
Ok(value)
}
pub fn install_macros() -> Result<()> {
pub fn install_macros(force: bool) -> Result<()> {
info!(
"Installing built-in macros in {}",
paths::macros_dir().display()
@@ -98,7 +98,7 @@ impl Macro {
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
let file_path = paths::macros_dir().join(file.as_ref());
if file_path.exists() {
if file_path.exists() && !force {
debug!(
"Macro file already exists, skipping: {}",
file_path.display()
+74 -3
View File
@@ -221,12 +221,83 @@ impl Default for Config {
}
pub fn install_builtins() -> Result<()> {
Functions::install_builtin_global_tools()?;
Agent::install_builtin_agents()?;
Macro::install_macros()?;
Functions::install_builtin_global_tools(false)?;
Agent::install_builtin_agents(false)?;
Macro::install_macros(false)?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum AssetCategory {
Agents,
Macros,
Functions,
#[value(name = "mcp_config")]
McpConfig,
}
impl AssetCategory {
pub const NAMES: [&'static str; 4] = ["agents", "macros", "functions", "mcp_config"];
pub fn parse(name: &str) -> Option<Self> {
match name {
"agents" => Some(Self::Agents),
"macros" => Some(Self::Macros),
"functions" => Some(Self::Functions),
"mcp_config" => Some(Self::McpConfig),
_ => None,
}
}
}
pub fn install_assets(category: AssetCategory) -> Result<()> {
let (label, target) = match category {
AssetCategory::Agents => ("agents", paths::agents_data_dir()),
AssetCategory::Macros => ("macros", paths::macros_dir()),
AssetCategory::Functions => ("functions", paths::functions_dir()),
AssetCategory::McpConfig => ("MCP config", paths::mcp_config_file()),
};
if !confirm_asset_overwrite(category, label, &target)? {
println!("Aborted. No files were changed.");
return Ok(());
}
match category {
AssetCategory::Agents => Agent::install_builtin_agents(true)?,
AssetCategory::Macros => Macro::install_macros(true)?,
AssetCategory::Functions => Functions::install_builtin_global_tools(true)?,
AssetCategory::McpConfig => Functions::install_mcp_config()?,
}
println!("Reinstalled bundled {label} ({})", target.display());
Ok(())
}
fn confirm_asset_overwrite(category: AssetCategory, label: &str, target: &Path) -> Result<bool> {
if !*IS_STDOUT_TERMINAL {
return Ok(true);
}
let body = match category {
AssetCategory::McpConfig => format!(
"This replaces your MCP server configuration at {} with this \
build's bundled template. Your configured MCP servers (and any \
custom secret references they contain) will be lost.",
target.display()
),
_ => format!(
"Reinstalling bundled {label} overwrites every bundled {label} in \
{} with this build's packaged versions. Local changes to bundled \
{label} will be lost; {label} you created yourself are left \
untouched.",
target.display()
),
};
let prompt = format!("{} {body}\nContinue? [y/N] ", warning_text("WARNING:"));
let answer = read_single_key(&['y', 'Y', 'n', 'N'], 'n', &prompt)?;
Ok(matches!(answer, 'y' | 'Y'))
}
pub fn default_sessions_dir() -> PathBuf {
match env::var(get_env_name("sessions_dir")) {
Ok(value) => PathBuf::from(value),
+98 -4
View File
@@ -3,10 +3,10 @@ use super::session::Session;
use super::todo::TodoList;
use super::tool_scope::{McpRuntime, ToolScope};
use super::{
AGENTS_DIR_NAME, Agent, AgentVariables, AppConfig, AppState, CREATE_TITLE_ROLE, Input,
LEFT_PROMPT, LastMessage, MESSAGES_FILE_NAME, RIGHT_PROMPT, Role, RoleLike, SESSIONS_DIR_NAME,
SUMMARIZATION_PROMPT, SUMMARY_CONTEXT_PROMPT, StateFlags, TEMP_ROLE_NAME, TEMP_SESSION_NAME,
WorkingMode, ensure_parent_exists, list_agents, paths,
AGENTS_DIR_NAME, Agent, AgentVariables, AppConfig, AppState, AssetCategory, CREATE_TITLE_ROLE,
Input, LEFT_PROMPT, LastMessage, MESSAGES_FILE_NAME, RIGHT_PROMPT, Role, RoleLike,
SESSIONS_DIR_NAME, SUMMARIZATION_PROMPT, SUMMARY_CONTEXT_PROMPT, StateFlags, TEMP_ROLE_NAME,
TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists, list_agents, paths,
};
use super::{MessageContentToolCalls, prompts};
use crate::client::{Model, ModelType, list_models};
@@ -1784,6 +1784,9 @@ impl RequestContext {
}
".rag" => super::map_completion_values(paths::list_rags()),
".agent" => super::map_completion_values(list_agents()),
".install" => super::map_completion_values(
AssetCategory::NAMES.iter().map(|s| s.to_string()).collect(),
),
".macro" => super::map_completion_values(paths::list_macros()),
".starter" => match &self.agent {
Some(agent) => agent
@@ -3676,4 +3679,95 @@ mod tests {
"Graph agent must not engage a session, not even an inherited default"
);
}
fn first_file(dir: &Path) -> Option<PathBuf> {
for entry in read_dir(dir).ok()?.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(found) = first_file(&path) {
return Some(found);
}
} else {
return Some(path);
}
}
None
}
#[test]
fn asset_category_parse_maps_known_names() {
assert_eq!(AssetCategory::parse("agents"), Some(AssetCategory::Agents));
assert_eq!(AssetCategory::parse("macros"), Some(AssetCategory::Macros));
assert_eq!(
AssetCategory::parse("functions"),
Some(AssetCategory::Functions)
);
assert_eq!(
AssetCategory::parse("mcp_config"),
Some(AssetCategory::McpConfig)
);
assert_eq!(AssetCategory::parse("roles"), None);
assert_eq!(AssetCategory::parse(""), None);
}
#[test]
#[serial]
fn install_builtin_agents_force_overwrites_only_with_force() {
let _guard = TestConfigDirGuard::new();
Agent::install_builtin_agents(false).unwrap();
let file =
first_file(&paths::agents_data_dir()).expect("bundled agents should be installed");
write(&file, "SENTINEL").unwrap();
Agent::install_builtin_agents(false).unwrap();
assert_eq!(
read_to_string(&file).unwrap(),
"SENTINEL",
"non-force install must not overwrite an existing file"
);
Agent::install_builtin_agents(true).unwrap();
assert_ne!(
read_to_string(&file).unwrap(),
"SENTINEL",
"force install must overwrite the existing file"
);
}
#[test]
#[serial]
fn install_functions_force_preserves_user_mcp_json() {
let _guard = TestConfigDirGuard::new();
Functions::install_builtin_global_tools(false).unwrap();
let mcp = paths::mcp_config_file();
assert!(mcp.exists(), "mcp.json should be installed on first run");
write(&mcp, "USER_MCP_CONFIG").unwrap();
Functions::install_builtin_global_tools(true).unwrap();
assert_eq!(
read_to_string(&mcp).unwrap(),
"USER_MCP_CONFIG",
"force install must NOT overwrite the user's mcp.json"
);
}
#[test]
#[serial]
fn install_mcp_config_overwrites_existing() {
let _guard = TestConfigDirGuard::new();
Functions::install_mcp_config().unwrap();
let mcp = paths::mcp_config_file();
assert!(mcp.exists(), "install_mcp_config should create mcp.json");
write(&mcp, "USER_MCP_CONFIG").unwrap();
Functions::install_mcp_config().unwrap();
assert_ne!(
read_to_string(&mcp).unwrap(),
"USER_MCP_CONFIG",
"install_mcp_config must overwrite the existing mcp.json"
);
}
}