feat: added support for loading sbx mixins that are dynamically discovered in the users workspace and config directory
This commit is contained in:
+23
-1
@@ -30,7 +30,7 @@ use std::io::{Read, stdin};
|
|||||||
",
|
",
|
||||||
group(
|
group(
|
||||||
ArgGroup::new("sbx-mode")
|
ArgGroup::new("sbx-mode")
|
||||||
.args(["sandbox", "fresh"])
|
.args(["sandbox", "fresh", "no_mixins"])
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.conflicts_with_all([
|
.conflicts_with_all([
|
||||||
"model", "prompt", "role", "session", "agent", "rag", "rebuild_rag",
|
"model", "prompt", "role", "session", "agent", "rag", "rebuild_rag",
|
||||||
@@ -186,6 +186,9 @@ pub struct Cli {
|
|||||||
/// Create the sandbox without bootstrapping the host config or vault password file
|
/// Create the sandbox without bootstrapping the host config or vault password file
|
||||||
#[arg(long, requires = "sandbox")]
|
#[arg(long, requires = "sandbox")]
|
||||||
pub fresh: bool,
|
pub fresh: bool,
|
||||||
|
/// Skip discovery and application of all sbx mixins (user and built-in)
|
||||||
|
#[arg(long, requires = "sandbox")]
|
||||||
|
pub no_mixins: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
@@ -550,4 +553,23 @@ mod tests {
|
|||||||
assert_eq!(cli.sandbox, Some(Some("foo".to_string())));
|
assert_eq!(cli.sandbox, Some(Some("foo".to_string())));
|
||||||
assert!(cli.fresh);
|
assert!(cli.fresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_no_mixins_requires_sandbox() {
|
||||||
|
assert!(Cli::try_parse_from(["coyote", "--no-mixins"]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_no_mixins_with_sandbox() {
|
||||||
|
let cli = parse(&["--sandbox", "--no-mixins"]);
|
||||||
|
assert!(cli.no_mixins);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_sandbox_with_fresh_and_no_mixins() {
|
||||||
|
let cli = parse(&["--sandbox", "foo", "--fresh", "--no-mixins"]);
|
||||||
|
assert_eq!(cli.sandbox, Some(Some("foo".to_string())));
|
||||||
|
assert!(cli.fresh);
|
||||||
|
assert!(cli.no_mixins);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ const WORKSPACE_MEMORY_FILE_NAME: &str = "COYOTE.md";
|
|||||||
const WORKSPACE_MEMORY_DIR_NAME: &str = ".coyote";
|
const WORKSPACE_MEMORY_DIR_NAME: &str = ".coyote";
|
||||||
const SBX_KIT_DIR_NAME: &str = "sbx-kit";
|
const SBX_KIT_DIR_NAME: &str = "sbx-kit";
|
||||||
const SBX_KIT_HASH_FILE: &str = "kit.sha256";
|
const SBX_KIT_HASH_FILE: &str = "kit.sha256";
|
||||||
|
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
|
||||||
const GIT_DIR_NAME: &str = ".git";
|
const GIT_DIR_NAME: &str = ".git";
|
||||||
const GITIGNORE_FILE_NAME: &str = ".gitignore";
|
const GITIGNORE_FILE_NAME: &str = ".gitignore";
|
||||||
const DEFAULT_VISIBLE_TOOLS: [&str; 18] = [
|
const DEFAULT_VISIBLE_TOOLS: [&str; 18] = [
|
||||||
|
|||||||
+22
-1
@@ -4,7 +4,7 @@ use super::{
|
|||||||
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
||||||
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, MEMORY_DIR_NAME,
|
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, MEMORY_DIR_NAME,
|
||||||
MEMORY_INDEX_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME, SBX_KIT_DIR_NAME,
|
MEMORY_INDEX_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME, SBX_KIT_DIR_NAME,
|
||||||
SBX_KIT_HASH_FILE, SKILLS_DIR_NAME, WORKSPACE_MEMORY_DIR_NAME,
|
SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SKILLS_DIR_NAME, WORKSPACE_MEMORY_DIR_NAME,
|
||||||
};
|
};
|
||||||
use crate::client::ProviderModels;
|
use crate::client::ProviderModels;
|
||||||
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
|
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
|
||||||
@@ -40,6 +40,27 @@ pub fn sandbox_kit_override() -> Option<PathBuf> {
|
|||||||
env::var_os(get_env_name("sandbox_kit")).map(PathBuf::from)
|
env::var_os(get_env_name("sandbox_kit")).map(PathBuf::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sbx_mixin_file() -> PathBuf {
|
||||||
|
config_dir().join(SBX_MIXIN_FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn global_tools_sbx_mixin_file() -> PathBuf {
|
||||||
|
functions_dir().join(SBX_MIXIN_FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_workspace_sbx_mixin(start: &Path) -> Option<PathBuf> {
|
||||||
|
for dir in start.ancestors() {
|
||||||
|
let candidate = dir
|
||||||
|
.join(WORKSPACE_MEMORY_DIR_NAME)
|
||||||
|
.join(SBX_MIXIN_FILE_NAME);
|
||||||
|
if candidate.exists() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn oauth_tokens_path() -> PathBuf {
|
pub fn oauth_tokens_path() -> PathBuf {
|
||||||
cache_path().join("oauth")
|
cache_path().join("oauth")
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = &cli.sandbox {
|
if let Some(name) = &cli.sandbox {
|
||||||
return sandbox::launch(name.clone(), cli.fresh);
|
return sandbox::launch(name.clone(), cli.fresh, cli.no_mixins);
|
||||||
}
|
}
|
||||||
|
|
||||||
install_builtins()?;
|
install_builtins()?;
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fs::{read_dir, read_to_string};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde_yaml::Value;
|
||||||
|
|
||||||
|
use crate::config::paths;
|
||||||
|
|
||||||
|
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DiscoveredMixin {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub label: String,
|
||||||
|
pub install_count: usize,
|
||||||
|
pub domain_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn discover() -> Result<Vec<DiscoveredMixin>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
push_if_exists(&mut out, paths::sbx_mixin_file())?;
|
||||||
|
push_if_exists(&mut out, paths::global_tools_sbx_mixin_file())?;
|
||||||
|
|
||||||
|
for path in collect_subdir_mixins(&paths::functions_dir()) {
|
||||||
|
out.push(read_mixin(path)?);
|
||||||
|
}
|
||||||
|
for path in collect_subdir_mixins(&paths::agents_data_dir()) {
|
||||||
|
out.push(read_mixin(path)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(cwd) = env::current_dir()
|
||||||
|
&& let Some(path) = paths::find_workspace_sbx_mixin(&cwd)
|
||||||
|
{
|
||||||
|
out.push(read_mixin(path)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn summarize(path: &Path) -> Result<(usize, usize)> {
|
||||||
|
let content = read_to_string(path)
|
||||||
|
.with_context(|| format!("Failed to read sbx mixin {}", path.display()))?;
|
||||||
|
let value: Value = serde_yaml::from_str(&content)
|
||||||
|
.with_context(|| format!("Failed to parse sbx mixin {}", path.display()))?;
|
||||||
|
|
||||||
|
let installs = value
|
||||||
|
.get("commands")
|
||||||
|
.and_then(|c| c.get("install"))
|
||||||
|
.and_then(|i| i.as_sequence())
|
||||||
|
.map(|s| s.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let domains = value
|
||||||
|
.get("network")
|
||||||
|
.and_then(|n| n.get("allowedDomains"))
|
||||||
|
.and_then(|d| d.as_sequence())
|
||||||
|
.map(|s| s.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
Ok((installs, domains))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_discovery(mixins: &[DiscoveredMixin], disabled: bool) {
|
||||||
|
if disabled {
|
||||||
|
info!("Mixin discovery disabled via --no-mixins.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mixins.is_empty() {
|
||||||
|
info!("No sbx mixins discovered.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = format!("Applying {} sbx mixin(s):", mixins.len());
|
||||||
|
info!("{header}");
|
||||||
|
println!("{header}");
|
||||||
|
|
||||||
|
for m in mixins {
|
||||||
|
let line = format!(
|
||||||
|
" {} (adds: {} install{}, {} domain{})",
|
||||||
|
m.label,
|
||||||
|
m.install_count,
|
||||||
|
if m.install_count == 1 { "" } else { "s" },
|
||||||
|
m.domain_count,
|
||||||
|
if m.domain_count == 1 { "" } else { "s" },
|
||||||
|
);
|
||||||
|
info!("{line}");
|
||||||
|
println!("{line}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_if_exists(out: &mut Vec<DiscoveredMixin>, path: PathBuf) -> Result<()> {
|
||||||
|
if path.exists() {
|
||||||
|
out.push(read_mixin(path)?);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_mixin(path: PathBuf) -> Result<DiscoveredMixin> {
|
||||||
|
let label = path.display().to_string();
|
||||||
|
let (install_count, domain_count) = summarize(&path)?;
|
||||||
|
|
||||||
|
Ok(DiscoveredMixin {
|
||||||
|
path,
|
||||||
|
label,
|
||||||
|
install_count,
|
||||||
|
domain_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_subdir_mixins(dir: &Path) -> Vec<PathBuf> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let Ok(rd) = read_dir(dir) else { return result };
|
||||||
|
|
||||||
|
let mut entries: Vec<_> = rd
|
||||||
|
.flatten()
|
||||||
|
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
|
||||||
|
.collect();
|
||||||
|
entries.sort_by_key(|e| e.file_name());
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let candidate = entry.path().join(SBX_MIXIN_FILE_NAME);
|
||||||
|
if candidate.exists() {
|
||||||
|
result.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::time;
|
||||||
|
|
||||||
|
fn unique_root(prefix: &str) -> PathBuf {
|
||||||
|
let nanos = time::SystemTime::now()
|
||||||
|
.duration_since(time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let root = env::temp_dir().join(format!("coyote-{prefix}-{nanos}"));
|
||||||
|
fs::create_dir_all(&root).unwrap();
|
||||||
|
root
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarize_counts_installs_and_domains() {
|
||||||
|
let root = unique_root("sbx-mixin-counts");
|
||||||
|
let path = root.join("sbx-mixin.yaml");
|
||||||
|
fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
schemaVersion: "1"
|
||||||
|
kind: mixin
|
||||||
|
commands:
|
||||||
|
install:
|
||||||
|
- command: "echo hi"
|
||||||
|
- command: "echo bye"
|
||||||
|
network:
|
||||||
|
allowedDomains:
|
||||||
|
- "a.example.com:443"
|
||||||
|
- "b.example.com:443"
|
||||||
|
- "c.example.com:443"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(summarize(&path).unwrap(), (2, 3));
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarize_treats_missing_blocks_as_zero() {
|
||||||
|
let root = unique_root("sbx-mixin-empty");
|
||||||
|
let path = root.join("sbx-mixin.yaml");
|
||||||
|
fs::write(&path, "schemaVersion: \"1\"\nkind: mixin\n").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(summarize(&path).unwrap(), (0, 0));
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarize_returns_err_on_malformed_yaml() {
|
||||||
|
let root = unique_root("sbx-mixin-bad");
|
||||||
|
let path = root.join("sbx-mixin.yaml");
|
||||||
|
fs::write(&path, "this: is: not: yaml: ::").unwrap();
|
||||||
|
|
||||||
|
let err = summarize(&path).unwrap_err();
|
||||||
|
let msg = format!("{err:#}");
|
||||||
|
assert!(
|
||||||
|
msg.contains(&path.display().to_string()),
|
||||||
|
"expected error to mention path; got: {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_subdir_mixins_sorts_and_skips_missing() {
|
||||||
|
let root = unique_root("sbx-mixin-subdirs");
|
||||||
|
for name in ["zebra", "apple", "no-mixin", "mango"] {
|
||||||
|
let dir = root.join(name);
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
if name != "no-mixin" {
|
||||||
|
fs::write(dir.join("sbx-mixin.yaml"), "kind: mixin\n").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let found = collect_subdir_mixins(&root);
|
||||||
|
let names: Vec<String> = found
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
p.parent()
|
||||||
|
.unwrap()
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(names, vec!["apple", "mango", "zebra"]);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collect_subdir_mixins_returns_empty_for_missing_dir() {
|
||||||
|
let absent = env::temp_dir().join("coyote-definitely-not-here-xyz");
|
||||||
|
let found = collect_subdir_mixins(&absent);
|
||||||
|
assert!(found.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
-21
@@ -7,7 +7,10 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use which::which;
|
use which::which;
|
||||||
|
|
||||||
|
mod mixins;
|
||||||
|
|
||||||
use crate::config::paths;
|
use crate::config::paths;
|
||||||
|
use crate::sandbox::mixins::DiscoveredMixin;
|
||||||
use crate::utils::run_command_with_output;
|
use crate::utils::run_command_with_output;
|
||||||
use crate::vault::Vault;
|
use crate::vault::Vault;
|
||||||
|
|
||||||
@@ -19,26 +22,39 @@ const SANDBOX_AGENT: &str = "coyote";
|
|||||||
#[folder = "assets/sbx-kit/"]
|
#[folder = "assets/sbx-kit/"]
|
||||||
struct EmbeddedKit;
|
struct EmbeddedKit;
|
||||||
|
|
||||||
pub fn launch(name: Option<String>, fresh: bool) -> Result<()> {
|
pub fn launch(name: Option<String>, fresh: bool, no_mixins: bool) -> Result<()> {
|
||||||
ensure_sbx_installed()?;
|
ensure_sbx_installed()?;
|
||||||
bail_if_nested()?;
|
bail_if_nested()?;
|
||||||
|
|
||||||
let name = resolve_name(name)?;
|
let name = resolve_name(name)?;
|
||||||
let kit_path = resolve_kit_path()?;
|
let kit_path = resolve_kit_path()?;
|
||||||
|
|
||||||
|
let discovered = if no_mixins {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
mixins::discover()?
|
||||||
|
};
|
||||||
|
|
||||||
if sandbox_exists(&name)? {
|
if sandbox_exists(&name)? {
|
||||||
info!("Re-attaching to existing sandbox '{name}'");
|
info!("Re-attaching to existing sandbox '{name}'");
|
||||||
if fresh {
|
if fresh {
|
||||||
debug!("--fresh ignored: re-attaching to existing sandbox '{name}'");
|
debug!("--fresh ignored: re-attaching to existing sandbox '{name}'");
|
||||||
}
|
}
|
||||||
} else if fresh {
|
if no_mixins {
|
||||||
let msg = format!("Creating fresh sandbox '{name}' (no host config will be copied)");
|
debug!("--no-mixins ignored: re-attaching to existing sandbox '{name}'");
|
||||||
info!("{msg}");
|
}
|
||||||
println!("{msg}");
|
|
||||||
create_sandbox(&name, &kit_path)?;
|
|
||||||
} else {
|
} else {
|
||||||
create_sandbox(&name, &kit_path)?;
|
mixins::log_discovery(&discovered, no_mixins);
|
||||||
copy_host_files(&name)?;
|
|
||||||
|
if fresh {
|
||||||
|
let msg = format!("Creating fresh sandbox '{name}' (no host config will be copied)");
|
||||||
|
info!("{msg}");
|
||||||
|
println!("{msg}");
|
||||||
|
create_sandbox(&name, &kit_path, &discovered)?;
|
||||||
|
} else {
|
||||||
|
create_sandbox(&name, &kit_path, &discovered)?;
|
||||||
|
copy_host_files(&name)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exec_run(&name, &kit_path)
|
exec_run(&name, &kit_path)
|
||||||
@@ -192,21 +208,11 @@ fn sandbox_exists(name: &str) -> Result<bool> {
|
|||||||
.any(|line| line.split_whitespace().next() == Some(name)))
|
.any(|line| line.split_whitespace().next() == Some(name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_sandbox(name: &str, kit_path: &Path) -> Result<()> {
|
fn create_sandbox(name: &str, kit_path: &Path, mixins: &[DiscoveredMixin]) -> Result<()> {
|
||||||
info!("Creating sandbox '{name}'");
|
info!("Creating sandbox '{name}'");
|
||||||
let kit_str = kit_path
|
let args = build_create_args(name, kit_path, mixins)?;
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| anyhow!("Kit path is not valid UTF-8: {}", kit_path.display()))?;
|
|
||||||
let status = Command::new(SBX_BINARY)
|
let status = Command::new(SBX_BINARY)
|
||||||
.args([
|
.args(&args)
|
||||||
"create",
|
|
||||||
"--kit",
|
|
||||||
kit_str,
|
|
||||||
SANDBOX_AGENT,
|
|
||||||
"--name",
|
|
||||||
name,
|
|
||||||
".",
|
|
||||||
])
|
|
||||||
.stdin(Stdio::inherit())
|
.stdin(Stdio::inherit())
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
@@ -220,6 +226,38 @@ fn create_sandbox(name: &str, kit_path: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_create_args(
|
||||||
|
name: &str,
|
||||||
|
kit_path: &Path,
|
||||||
|
mixins: &[DiscoveredMixin],
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
let kit_str = kit_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("Kit path is not valid UTF-8: {}", kit_path.display()))?;
|
||||||
|
|
||||||
|
let mut args = vec![
|
||||||
|
"create".to_string(),
|
||||||
|
"--kit".to_string(),
|
||||||
|
kit_str.to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for mixin in mixins {
|
||||||
|
let mixin_str = mixin
|
||||||
|
.path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("Mixin path is not valid UTF-8: {}", mixin.path.display()))?;
|
||||||
|
args.push("--kit".to_string());
|
||||||
|
args.push(mixin_str.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(SANDBOX_AGENT.to_string());
|
||||||
|
args.push("--name".to_string());
|
||||||
|
args.push(name.to_string());
|
||||||
|
args.push(".".to_string());
|
||||||
|
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_host_files(name: &str) -> Result<()> {
|
fn copy_host_files(name: &str) -> Result<()> {
|
||||||
let config_dir = paths::config_dir();
|
let config_dir = paths::config_dir();
|
||||||
let home_dir = dirs::home_dir().context("Could not determine home directory")?;
|
let home_dir = dirs::home_dir().context("Could not determine home directory")?;
|
||||||
@@ -393,4 +431,60 @@ mod tests {
|
|||||||
assert_eq!(h1, h2);
|
assert_eq!(h1, h2);
|
||||||
assert_eq!(h1.len(), 64);
|
assert_eq!(h1.len(), 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_create_args_emits_base_kit_before_mixins() {
|
||||||
|
let kit = PathBuf::from("/cache/sbx-kit");
|
||||||
|
let mixins = vec![
|
||||||
|
DiscoveredMixin {
|
||||||
|
path: PathBuf::from("/cfg/sbx-mixin.yaml"),
|
||||||
|
label: "user".into(),
|
||||||
|
install_count: 0,
|
||||||
|
domain_count: 0,
|
||||||
|
},
|
||||||
|
DiscoveredMixin {
|
||||||
|
path: PathBuf::from("/cfg/agents/sql/sbx-mixin.yaml"),
|
||||||
|
label: "sql".into(),
|
||||||
|
install_count: 0,
|
||||||
|
domain_count: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let args = build_create_args("my-box", &kit, &mixins).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec![
|
||||||
|
"create".to_string(),
|
||||||
|
"--kit".to_string(),
|
||||||
|
"/cache/sbx-kit".to_string(),
|
||||||
|
"--kit".to_string(),
|
||||||
|
"/cfg/sbx-mixin.yaml".to_string(),
|
||||||
|
"--kit".to_string(),
|
||||||
|
"/cfg/agents/sql/sbx-mixin.yaml".to_string(),
|
||||||
|
"coyote".to_string(),
|
||||||
|
"--name".to_string(),
|
||||||
|
"my-box".to_string(),
|
||||||
|
".".to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_create_args_with_no_mixins_omits_mixin_kits() {
|
||||||
|
let kit = PathBuf::from("/cache/sbx-kit");
|
||||||
|
let args = build_create_args("box", &kit, &[]).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec![
|
||||||
|
"create".to_string(),
|
||||||
|
"--kit".to_string(),
|
||||||
|
"/cache/sbx-kit".to_string(),
|
||||||
|
"coyote".to_string(),
|
||||||
|
"--name".to_string(),
|
||||||
|
"box".to_string(),
|
||||||
|
".".to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user