use std::path::Path;
use crate::config::Config;
use crate::error::{Error, Result};
fn run_internal(json: bool) -> Result<Option<serde_json::Value>> {
if !Path::new(".spikes").is_dir() {
if json {
let error_json = serde_json::json!({
"error": "No .spikes/ directory found. Run 'spikes init' first."
});
return Ok(Some(error_json));
}
return Err(Error::NoSpikesDir);
}
let config = Config::load()?;
if json {
let output = serde_json::json!({
"project": {
"key": config.effective_project_key()
},
"widget": {
"theme": config.widget.theme,
"position": config.widget.position,
"color": config.widget.color,
"collect_email": config.widget.collect_email
},
"remote": {
"endpoint": config.effective_endpoint(),
"hosted": config.remote.hosted,
"has_token": config.remote.token.is_some()
}
});
return Ok(Some(output));
}
println!();
println!(" / Spikes Configuration");
println!();
println!(" Project: {}", config.effective_project_key());
println!();
println!(" Widget:");
println!(" theme: {}", config.widget.theme);
println!(" position: {}", config.widget.position);
println!(" color: {}", config.widget.color);
println!(" collect_email: {}", config.widget.collect_email);
println!();
println!(" Remote:");
if let Some(endpoint) = config.effective_endpoint() {
println!(" endpoint: {}", endpoint);
println!(" hosted: {}", config.remote.hosted);
println!(" token: {}", if config.remote.token.is_some() { "(set)" } else { "(not set)" });
} else {
println!(" (not configured)");
}
println!();
println!(" Widget tag attributes:");
println!(" {}", config.widget_attributes());
println!();
Ok(None)
}
pub fn run(json: bool) -> Result<()> {
match run_internal(json) {
Ok(Some(json_value)) => {
if json_value.get("error").is_some() {
println!(
"{}",
serde_json::to_string_pretty(&json_value)
.expect("Failed to serialize JSON")
);
std::process::exit(1);
}
println!(
"{}",
serde_json::to_string_pretty(&json_value)
.expect("Failed to serialize JSON")
);
Ok(())
}
Ok(None) => Ok(()),
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
use std::sync::Mutex;
static TEST_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_config_requires_spikes_dir() {
let _lock = TEST_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = run_internal(false);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::NoSpikesDir));
std::env::set_current_dir(original_cwd).unwrap();
}
#[test]
fn test_config_requires_spikes_dir_json() {
let _lock = TEST_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = run_internal(true);
assert!(result.is_ok(), "run_internal should succeed and return JSON error");
let json_value = result.unwrap();
assert!(json_value.is_some(), "should return JSON value");
let json = json_value.unwrap();
assert!(json.get("error").is_some(), "JSON should contain error field");
let error_msg = json.get("error").unwrap().as_str().unwrap();
assert!(error_msg.contains(".spikes/"), "Error should mention .spikes/");
assert!(error_msg.contains("spikes init"), "Error should reference 'spikes init'");
std::env::set_current_dir(original_cwd).unwrap();
}
#[test]
fn test_config_succeeds_with_spikes_dir() {
let _lock = TEST_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let spikes_dir = temp_dir.path().join(".spikes");
fs::create_dir_all(&spikes_dir).unwrap();
let config_content = concat!(
"[project]\n",
"key = \"test-project\"\n",
"\n",
"[widget]\n",
"theme = \"dark\"\n",
"position = \"bottom-right\"\n",
"color = \"#e74c3c\"\n",
"collect_email = false\n",
"\n",
"[remote]\n",
"hosted = true\n",
"endpoint = \"https://spikes.sh\"\n"
);
fs::write(spikes_dir.join("config.toml"), config_content).unwrap();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = run_internal(false);
assert!(result.is_ok(), "config should succeed when .spikes/ exists: {:?}", result);
assert!(result.unwrap().is_none(), "Success in non-JSON mode should return None");
std::env::set_current_dir(original_cwd).unwrap();
}
#[test]
fn test_config_json_succeeds_with_spikes_dir() {
let _lock = TEST_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let spikes_dir = temp_dir.path().join(".spikes");
fs::create_dir_all(&spikes_dir).unwrap();
let config_content = concat!(
"[project]\n",
"key = \"test-project\"\n",
"\n",
"[widget]\n",
"theme = \"dark\"\n",
"position = \"bottom-right\"\n",
"color = \"#e74c3c\"\n",
"collect_email = false\n",
"\n",
"[remote]\n",
"hosted = true\n",
"endpoint = \"https://spikes.sh\"\n"
);
fs::write(spikes_dir.join("config.toml"), config_content).unwrap();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = run_internal(true);
assert!(result.is_ok(), "config --json should succeed when .spikes/ exists: {:?}", result);
let json_value = result.unwrap();
assert!(json_value.is_some(), "JSON mode should return Some(JSON value)");
let json = json_value.unwrap();
assert!(json.get("project").is_some(), "JSON should contain project");
assert!(json.get("widget").is_some(), "JSON should contain widget");
assert!(json.get("remote").is_some(), "JSON should contain remote");
let remote = json.get("remote").unwrap();
assert_eq!(remote.get("hosted").unwrap().as_bool(), Some(true));
std::env::set_current_dir(original_cwd).unwrap();
}
#[test]
fn test_config_requires_spikes_dir_not_file() {
let _lock = TEST_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let spikes_file = temp_dir.path().join(".spikes");
fs::write(&spikes_file, "not a directory").unwrap();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = run_internal(false);
assert!(result.is_err(), "config should fail when .spikes is a file");
assert!(matches!(result.unwrap_err(), Error::NoSpikesDir),
"config should return NoSpikesDir when .spikes is a file, not a directory");
let list_result = crate::storage::load_spikes();
assert!(list_result.is_err(), "list should fail when .spikes is a file");
assert!(matches!(list_result.unwrap_err(), Error::NoSpikesDir),
"list should return NoSpikesDir when .spikes is a file, not a directory");
std::env::set_current_dir(original_cwd).unwrap();
}
#[test]
fn test_config_requires_spikes_dir_not_file_json() {
let _lock = TEST_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let spikes_file = temp_dir.path().join(".spikes");
fs::write(&spikes_file, "not a directory").unwrap();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = run_internal(true);
assert!(result.is_ok(), "config --json (via run_internal) should return JSON error");
let json_value = result.unwrap();
assert!(json_value.is_some(), "should return JSON value");
let json = json_value.unwrap();
assert!(json.get("error").is_some(), "JSON should contain error field");
let error_msg = json.get("error").unwrap().as_str().unwrap();
assert!(error_msg.contains(".spikes/"), "Error should mention .spikes/");
assert!(error_msg.contains("spikes init"), "Error should reference 'spikes init'");
std::env::set_current_dir(original_cwd).unwrap();
}
#[test]
fn test_config_shows_correct_hosted_endpoint() {
let _lock = TEST_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let spikes_dir = temp_dir.path().join(".spikes");
fs::create_dir_all(&spikes_dir).unwrap();
let config_content = concat!(
"[remote]\n",
"hosted = true\n",
"endpoint = \"https://spikes.sh\"\n"
);
fs::write(spikes_dir.join("config.toml"), config_content).unwrap();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let config = Config::load().unwrap();
assert_eq!(config.effective_endpoint(), Some("https://spikes.sh".to_string()));
assert!(config.remote.hosted);
std::env::set_current_dir(original_cwd).unwrap();
}
#[test]
fn test_config_json_error_contains_init_guidance() {
let _lock = TEST_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let original_cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let result = run_internal(false);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, Error::NoSpikesDir), "Should return NoSpikesDir error");
let err_string = err.to_string();
assert!(err_string.contains(".spikes/"), "Error should mention .spikes/ directory: {}", err_string);
assert!(err_string.contains("spikes init"), "Error should reference 'spikes init' command: {}", err_string);
let json_result = run_internal(true);
assert!(json_result.is_ok(), "JSON mode should return Ok with JSON error");
let json_value = json_result.unwrap();
assert!(json_value.is_some(), "JSON mode should return Some(JSON)");
let json = json_value.unwrap();
assert!(json.get("error").is_some(), "JSON should contain error field");
let error_msg = json.get("error").unwrap().as_str().unwrap();
assert!(error_msg.contains(".spikes/"), "JSON error should mention .spikes/");
assert!(error_msg.contains("spikes init"), "JSON error should reference 'spikes init'");
std::env::set_current_dir(original_cwd).unwrap();
}
}