fix(onboard,skills): align workspace defaults and open-skills discovery
This commit is contained in:
parent
66ee7e31ac
commit
cd4bb8d10d
@ -3178,7 +3178,7 @@ pub(crate) async fn persist_active_workspace_config_dir(config_dir: &Path) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_config_dir_for_workspace(workspace_dir: &Path) -> (PathBuf, PathBuf) {
|
||||
pub(crate) fn resolve_config_dir_for_workspace(workspace_dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let workspace_config_dir = workspace_dir.to_path_buf();
|
||||
if workspace_config_dir.join("config.toml").exists() {
|
||||
return (
|
||||
@ -3209,6 +3209,17 @@ fn resolve_config_dir_for_workspace(workspace_dir: &Path) -> (PathBuf, PathBuf)
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve the current runtime config/workspace directories for onboarding flows.
|
||||
///
|
||||
/// This mirrors the same precedence used by `Config::load_or_init()`:
|
||||
/// `ZEROCLAW_CONFIG_DIR` > `ZEROCLAW_WORKSPACE` > active workspace marker > defaults.
|
||||
pub(crate) async fn resolve_runtime_dirs_for_onboarding() -> Result<(PathBuf, PathBuf)> {
|
||||
let (default_zeroclaw_dir, default_workspace_dir) = default_config_and_workspace_dirs()?;
|
||||
let (config_dir, workspace_dir, _) =
|
||||
resolve_runtime_config_dirs(&default_zeroclaw_dir, &default_workspace_dir).await?;
|
||||
Ok((config_dir, workspace_dir))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ConfigResolutionSource {
|
||||
EnvConfigDir,
|
||||
|
||||
@ -444,6 +444,28 @@ pub async fn run_quick_setup(
|
||||
.await
|
||||
}
|
||||
|
||||
fn resolve_quick_setup_dirs_with_home(home: &Path) -> (PathBuf, PathBuf) {
|
||||
if let Ok(custom_config_dir) = std::env::var("ZEROCLAW_CONFIG_DIR") {
|
||||
let trimmed = custom_config_dir.trim();
|
||||
if !trimmed.is_empty() {
|
||||
let config_dir = PathBuf::from(trimmed);
|
||||
return (config_dir.clone(), config_dir.join("workspace"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(custom_workspace) = std::env::var("ZEROCLAW_WORKSPACE") {
|
||||
let trimmed = custom_workspace.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return crate::config::schema::resolve_config_dir_for_workspace(&PathBuf::from(
|
||||
trimmed,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let config_dir = home.join(".zeroclaw");
|
||||
(config_dir.clone(), config_dir.join("workspace"))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn run_quick_setup_with_home(
|
||||
credential_override: Option<&str>,
|
||||
@ -462,8 +484,7 @@ async fn run_quick_setup_with_home(
|
||||
);
|
||||
println!();
|
||||
|
||||
let zeroclaw_dir = home.join(".zeroclaw");
|
||||
let workspace_dir = zeroclaw_dir.join("workspace");
|
||||
let (zeroclaw_dir, workspace_dir) = resolve_quick_setup_dirs_with_home(home);
|
||||
let config_path = zeroclaw_dir.join("config.toml");
|
||||
|
||||
ensure_onboard_overwrite_allowed(&config_path, force)?;
|
||||
@ -1856,14 +1877,12 @@ async fn persist_workspace_selection(config_path: &Path) -> Result<()> {
|
||||
// ── Step 1: Workspace ────────────────────────────────────────────
|
||||
|
||||
async fn setup_workspace() -> Result<(PathBuf, PathBuf)> {
|
||||
let home = directories::UserDirs::new()
|
||||
.map(|u| u.home_dir().to_path_buf())
|
||||
.context("Could not find home directory")?;
|
||||
let default_dir = home.join(".zeroclaw");
|
||||
let (default_config_dir, default_workspace_dir) =
|
||||
crate::config::schema::resolve_runtime_dirs_for_onboarding().await?;
|
||||
|
||||
print_bullet(&format!(
|
||||
"Default location: {}",
|
||||
style(default_dir.display()).green()
|
||||
style(default_workspace_dir.display()).green()
|
||||
));
|
||||
|
||||
let use_default = Confirm::new()
|
||||
@ -1871,18 +1890,17 @@ async fn setup_workspace() -> Result<(PathBuf, PathBuf)> {
|
||||
.default(true)
|
||||
.interact()?;
|
||||
|
||||
let zeroclaw_dir = if use_default {
|
||||
default_dir
|
||||
let (config_dir, workspace_dir) = if use_default {
|
||||
(default_config_dir, default_workspace_dir)
|
||||
} else {
|
||||
let custom: String = Input::new()
|
||||
.with_prompt(" Enter workspace path")
|
||||
.interact_text()?;
|
||||
let expanded = shellexpand::tilde(&custom).to_string();
|
||||
PathBuf::from(expanded)
|
||||
crate::config::schema::resolve_config_dir_for_workspace(&PathBuf::from(expanded))
|
||||
};
|
||||
|
||||
let workspace_dir = zeroclaw_dir.join("workspace");
|
||||
let config_path = zeroclaw_dir.join("config.toml");
|
||||
let config_path = config_dir.join("config.toml");
|
||||
|
||||
fs::create_dir_all(&workspace_dir)
|
||||
.await
|
||||
@ -5541,8 +5559,43 @@ fn print_summary(config: &Config) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
struct EnvVarGuard {
|
||||
key: &'static str,
|
||||
previous: Option<String>,
|
||||
}
|
||||
|
||||
impl EnvVarGuard {
|
||||
fn set(key: &'static str, value: &str) -> Self {
|
||||
let previous = std::env::var(key).ok();
|
||||
std::env::set_var(key, value);
|
||||
Self { key, previous }
|
||||
}
|
||||
|
||||
fn unset(key: &'static str) -> Self {
|
||||
let previous = std::env::var(key).ok();
|
||||
std::env::remove_var(key);
|
||||
Self { key, previous }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EnvVarGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(previous) = &self.previous {
|
||||
std::env::set_var(self.key, previous);
|
||||
} else {
|
||||
std::env::remove_var(self.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ProjectContext defaults ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
@ -5710,6 +5763,35 @@ mod tests {
|
||||
assert!(config_raw.contains("default_model = \"custom-model-fresh\""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn quick_setup_respects_zero_claw_workspace_env_layout() {
|
||||
let _env_guard = env_lock().lock().unwrap();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let workspace_root = tmp.path().join("zeroclaw-data");
|
||||
let workspace_dir = workspace_root.join("workspace");
|
||||
let expected_config_path = workspace_root.join(".zeroclaw").join("config.toml");
|
||||
|
||||
let _workspace_env = EnvVarGuard::set(
|
||||
"ZEROCLAW_WORKSPACE",
|
||||
workspace_dir.to_string_lossy().as_ref(),
|
||||
);
|
||||
let _config_env = EnvVarGuard::unset("ZEROCLAW_CONFIG_DIR");
|
||||
|
||||
let config = run_quick_setup_with_home(
|
||||
Some("sk-env"),
|
||||
Some("openrouter"),
|
||||
Some("model-env"),
|
||||
Some("sqlite"),
|
||||
false,
|
||||
tmp.path(),
|
||||
)
|
||||
.await
|
||||
.expect("quick setup should honor ZEROCLAW_WORKSPACE");
|
||||
|
||||
assert_eq!(config.workspace_dir, workspace_dir);
|
||||
assert_eq!(config.config_path, expected_config_path);
|
||||
}
|
||||
|
||||
// ── scaffold_workspace: basic file creation ─────────────────
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@ -141,6 +141,14 @@ fn load_skills_from_directory(skills_dir: &Path) -> Vec<Skill> {
|
||||
}
|
||||
|
||||
fn load_open_skills(repo_dir: &Path) -> Vec<Skill> {
|
||||
// Modern open-skills layout stores skill packages in `skills/<name>/SKILL.md`.
|
||||
// Prefer that structure to avoid treating repository docs (e.g. CONTRIBUTING.md)
|
||||
// as executable skills.
|
||||
let nested_skills_dir = repo_dir.join("skills");
|
||||
if nested_skills_dir.is_dir() {
|
||||
return load_skills_from_directory(&nested_skills_dir);
|
||||
}
|
||||
|
||||
let mut skills = Vec::new();
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(repo_dir) else {
|
||||
@ -1294,10 +1302,15 @@ description = "Bare minimum"
|
||||
fs::create_dir_all(workspace_dir.join("skills")).unwrap();
|
||||
|
||||
let open_skills_dir = dir.path().join("open-skills-local");
|
||||
fs::create_dir_all(&open_skills_dir).unwrap();
|
||||
fs::create_dir_all(open_skills_dir.join("skills/http_request")).unwrap();
|
||||
fs::write(open_skills_dir.join("README.md"), "# open skills\n").unwrap();
|
||||
fs::write(
|
||||
open_skills_dir.join("http_request.md"),
|
||||
open_skills_dir.join("CONTRIBUTING.md"),
|
||||
"# contribution guide\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
open_skills_dir.join("skills/http_request/SKILL.md"),
|
||||
"# HTTP request\nFetch API responses.\n",
|
||||
)
|
||||
.unwrap();
|
||||
@ -1310,6 +1323,7 @@ description = "Bare minimum"
|
||||
let skills = load_skills_with_config(&workspace_dir, &config);
|
||||
assert_eq!(skills.len(), 1);
|
||||
assert_eq!(skills[0].name, "http_request");
|
||||
assert_ne!(skills[0].name, "CONTRIBUTING");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user