PicoBot/src/cli/init.rs

925 lines
33 KiB
Rust

use std::collections::HashMap;
use std::path::PathBuf;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use crate::config::{
AgentConfig, ChannelConfig, Config, FeishuChannelConfig, GatewayConfig, ModelConfig,
ProviderConfig, SchedulerConfig, TaggedChannelConfig, WechatChannelConfig,
};
/// Interactive configuration wizard for PicoBot
pub struct InitWizard {
read: BufReader<tokio::io::Stdin>,
write: tokio::io::Stdout,
config_path: PathBuf,
}
impl InitWizard {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
Self {
read: BufReader::new(tokio::io::stdin()),
write: tokio::io::stdout(),
config_path: home.join(".picobot").join("config.json"),
}
}
pub async fn run(&mut self, force: bool, skip_channels: bool) -> Result<(), InitError> {
let existing = self.load_existing_config(force)?;
self.show_welcome_message();
// Step 1: Provider
let providers = self.configure_provider(&existing).await?;
// Step 2: Model
let models = self.configure_model(&existing).await?;
// Step 3: Agent
let agents = self.configure_agent(&existing, &providers, &models).await?;
// Step 4: Channels
let channels = if !skip_channels {
self.configure_channels(&existing).await?
} else {
existing.channels.clone()
};
let config = self.build_config(providers, models, agents, channels, &existing);
self.save_config(&config)?;
self.show_completion_message(&config);
Ok(())
}
fn load_existing_config(&self, force: bool) -> Result<Config, InitError> {
if self.config_path.exists() && !force {
Config::load_default().or_else(|_| Ok(Self::empty_config()))
} else {
Ok(Self::empty_config())
}
}
fn empty_config() -> Config {
Config {
providers: HashMap::new(),
models: HashMap::new(),
agents: HashMap::new(),
time: crate::config::TimeConfig::default(),
gateway: GatewayConfig::default(),
scheduler: SchedulerConfig::default(),
client: crate::config::ClientConfig::default(),
channels: HashMap::new(),
skills: crate::config::SkillsConfig::default(),
tools: crate::config::ToolsConfig::default(),
memory_maintenance: crate::config::MemoryMaintenanceConfig::default(),
mcp_servers: HashMap::new(),
image_context: crate::config::ImageContextConfig::default(),
subagents: crate::config::SubagentsConfig::default(),
}
}
fn show_welcome_message(&mut self) {
println!();
println!("╔══════════════════════════════════════════════════════╗");
println!("║ PicoBot Configuration Wizard ║");
println!("╚══════════════════════════════════════════════════════╝");
println!();
println!("This wizard will help you configure PicoBot.");
println!("Press Enter to use default values where shown.");
println!();
}
// ==================== Prompt Helpers ====================
async fn prompt_with_default(
&mut self,
label: &str,
default: &str,
) -> Result<String, InitError> {
if !default.is_empty() {
println!("{} [default: {}]: ", label, default);
} else {
println!("{}: ", label);
}
self.write.flush().await?;
let mut line = String::new();
let bytes_read = self.read.read_line(&mut line).await?;
if bytes_read == 0 {
return Err(InitError::InputError("EOF reached".to_string()));
}
let input = line.trim().to_string();
Ok(if input.is_empty() { default.to_string() } else { input })
}
async fn prompt_required(&mut self, label: &str) -> Result<String, InitError> {
loop {
println!("{}: ", label);
self.write.flush().await?;
let mut line = String::new();
let bytes_read = self.read.read_line(&mut line).await?;
if bytes_read == 0 {
return Err(InitError::InputError("EOF reached".to_string()));
}
let input = line.trim().to_string();
if !input.is_empty() {
return Ok(input);
}
println!("{} is required. Please enter a value.", label);
}
}
async fn prompt_select(
&mut self,
label: &str,
options: &[String],
default: usize,
) -> Result<usize, InitError> {
for (i, opt) in options.iter().enumerate() {
println!(" {}. {}", i + 1, opt);
}
println!();
let default_str = (default + 1).to_string();
let input = self.prompt_with_default(label, &default_str).await?;
let selected: usize = input.parse().map_err(|_| {
InitError::InputError(format!("Invalid selection: {}", input))
})?;
if selected == 0 || selected > options.len() {
return Err(InitError::InputError(format!(
"Selection must be between 1 and {}",
options.len()
)));
}
Ok(selected - 1)
}
// ==================== Step 1: Provider ====================
async fn configure_provider(
&mut self,
existing: &Config,
) -> Result<HashMap<String, ProviderConfig>, InitError> {
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Step 1: Configure Provider (API Endpoint)");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
let provider_names: Vec<String> = existing.providers.keys().cloned().collect();
if !provider_names.is_empty() {
println!("Existing providers:");
for (i, name) in provider_names.iter().enumerate() {
println!(" {}. {}", i + 1, name);
}
println!();
println!("Options:");
println!(" 1. Add new provider");
println!(" 2. Modify existing provider");
println!(" 3. Use existing provider");
println!(" 4. Skip");
println!();
let choice = self
.prompt_with_default("Select option", "1")
.await?;
match choice.as_str() {
"1" => return self.add_provider(existing).await,
"2" => return self.modify_provider(existing).await,
"3" => {
println!("Keeping existing providers.");
return Ok(existing.providers.clone());
}
"4" => {
println!("Skipping provider configuration.");
return Ok(existing.providers.clone());
}
_ => {
println!("Invalid option, adding new provider.");
return self.add_provider(existing).await;
}
}
} else {
println!("No existing providers found.");
println!();
println!("Options:");
println!(" 1. Add new provider");
println!(" 2. Skip");
println!();
let choice = self.prompt_with_default("Select option", "1").await?;
match choice.as_str() {
"1" => self.add_provider(existing).await,
"2" => {
println!("Skipping provider configuration.");
Ok(existing.providers.clone())
}
_ => {
println!("Invalid option, adding new provider.");
self.add_provider(existing).await
}
}
}
}
async fn add_provider(
&mut self,
existing: &Config,
) -> Result<HashMap<String, ProviderConfig>, InitError> {
let provider_name = self
.prompt_with_default("Provider name", "default")
.await?;
println!("Provider type:");
println!(" 1. openai");
println!(" 2. anthropic");
println!();
let type_choice = self.prompt_with_default("Select type", "1").await?;
let provider_type = match type_choice.as_str() {
"1" => "openai",
"2" => "anthropic",
_ => "openai",
};
let base_url = self.prompt_required("API base URL").await?;
println!();
println!("API Key is required and will be stored in config.json.");
let api_key = self.prompt_required("API Key").await?;
let provider = ProviderConfig {
provider_type: provider_type.to_string(),
base_url,
api_key,
extra_headers: HashMap::new(),
llm_timeout_secs: 120,
memory_maintenance_timeout_secs: 600,
};
let mut providers = existing.providers.clone();
providers.insert(provider_name.clone(), provider);
println!();
println!("Provider '{}' configured.", provider_name);
Ok(providers)
}
async fn modify_provider(
&mut self,
existing: &Config,
) -> Result<HashMap<String, ProviderConfig>, InitError> {
// Select which provider to modify
let provider_names: Vec<String> = existing.providers.keys().cloned().collect();
println!("Select provider to modify:");
let provider_idx = self.prompt_select("", &provider_names, 0).await?;
let selected_provider_name = &provider_names[provider_idx];
let current_provider = existing.providers.get(selected_provider_name).unwrap();
println!();
println!(
"Current config: type={}, base_url={}",
current_provider.provider_type, current_provider.base_url
);
println!();
let current_type_idx = if current_provider.provider_type == "anthropic" {
1
} else {
0
};
let type_options = vec!["openai".to_string(), "anthropic".to_string()];
println!("Provider type:");
let type_idx = self.prompt_select("", &type_options, current_type_idx).await?;
let provider_type = &type_options[type_idx];
let base_url = self
.prompt_with_default("API base URL", &current_provider.base_url)
.await?;
println!();
println!("API Key (press Enter to keep current key):");
let api_key = self
.prompt_with_default("API Key", &current_provider.api_key)
.await?;
let provider = ProviderConfig {
provider_type: provider_type.clone(),
base_url,
api_key,
extra_headers: current_provider.extra_headers.clone(),
llm_timeout_secs: current_provider.llm_timeout_secs,
memory_maintenance_timeout_secs: current_provider.memory_maintenance_timeout_secs,
};
let mut providers = existing.providers.clone();
providers.insert(selected_provider_name.clone(), provider);
println!();
println!("Provider '{}' modified.", selected_provider_name);
Ok(providers)
}
// ==================== Step 2: Model ====================
async fn configure_model(
&mut self,
existing: &Config,
) -> Result<HashMap<String, ModelConfig>, InitError> {
println!();
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Step 2: Configure Model");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
let model_names: Vec<String> = existing.models.keys().cloned().collect();
if !model_names.is_empty() {
println!("Existing models:");
for (i, name) in model_names.iter().enumerate() {
let model = existing.models.get(name).unwrap();
println!(" {}. {} (model_id: {})", i + 1, name, model.model_id);
}
println!();
println!("Options:");
println!(" 1. Use existing model");
println!(" 2. Add new model");
println!(" 3. Skip");
println!();
let choice = self.prompt_with_default("Select option", "1").await?;
match choice.as_str() {
"1" => {
println!("Keeping existing models.");
return Ok(existing.models.clone());
}
"2" => return self.add_model(existing).await,
"3" => {
println!("Skipping model configuration.");
return Ok(existing.models.clone());
}
_ => {
println!("Invalid option, keeping existing models.");
return Ok(existing.models.clone());
}
}
} else {
println!("No existing models found.");
println!();
println!("Options:");
println!(" 1. Add new model");
println!(" 2. Skip");
println!();
let choice = self.prompt_with_default("Select option", "1").await?;
match choice.as_str() {
"1" => self.add_model(existing).await,
"2" => {
println!("Skipping model configuration.");
Ok(existing.models.clone())
}
_ => self.add_model(existing).await,
}
}
}
async fn add_model(
&mut self,
existing: &Config,
) -> Result<HashMap<String, ModelConfig>, InitError> {
let model_name = self
.prompt_with_default("Model name (reference key)", "default")
.await?;
let model_id = self.prompt_required("Model ID (actual identifier)").await?;
let temperature_str = self
.prompt_with_default("Temperature (0.0-1.0)", "0.7")
.await?;
let temperature: Option<f32> = temperature_str
.parse()
.ok()
.filter(|v| (0.0..=1.0).contains(v));
let max_tokens_str = self
.prompt_with_default("Max tokens (optional)", "4096")
.await?;
let max_tokens: Option<u32> = max_tokens_str.parse().ok();
let model = ModelConfig {
model_id,
temperature,
max_tokens,
context_window_tokens: Some(128000),
extra: HashMap::new(),
};
let mut models = existing.models.clone();
models.insert(model_name.clone(), model);
println!();
println!("Model '{}' configured.", model_name);
Ok(models)
}
// ==================== Step 3: Agent ====================
async fn configure_agent(
&mut self,
existing: &Config,
providers: &HashMap<String, ProviderConfig>,
models: &HashMap<String, ModelConfig>,
) -> Result<HashMap<String, AgentConfig>, InitError> {
println!();
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Step 3: Configure Agent");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
// Check if we have providers and models to select from
if providers.is_empty() {
println!("Warning: No providers configured. Please configure a provider first.");
return Ok(existing.agents.clone());
}
if models.is_empty() {
println!("Warning: No models configured. Please configure a model first.");
return Ok(existing.agents.clone());
}
let agent_names: Vec<String> = existing.agents.keys().cloned().collect();
if !agent_names.is_empty() {
println!("Existing agents:");
for (i, name) in agent_names.iter().enumerate() {
let agent = existing.agents.get(name).unwrap();
println!(
" {}. {} (provider: {}, model: {})",
i + 1,
name,
agent.provider,
agent.model
);
}
println!();
println!("Options:");
println!(" 1. Add new agent");
println!(" 2. Modify existing agent");
println!(" 3. Use existing agent");
println!(" 4. Skip");
println!();
let choice = self.prompt_with_default("Select option", "1").await?;
match choice.as_str() {
"1" => return self.add_agent(existing, providers, models).await,
"2" => return self.modify_agent(existing, providers, models).await,
"3" => {
println!("Keeping existing agents.");
return Ok(existing.agents.clone());
}
"4" => {
println!("Skipping agent configuration.");
return Ok(existing.agents.clone());
}
_ => {
println!("Invalid option, adding new agent.");
return self.add_agent(existing, providers, models).await;
}
}
} else {
println!("No existing agents found.");
println!();
println!("Options:");
println!(" 1. Add new agent");
println!(" 2. Skip");
println!();
let choice = self.prompt_with_default("Select option", "1").await?;
match choice.as_str() {
"1" => self.add_agent(existing, providers, models).await,
"2" => {
println!("Skipping agent configuration.");
Ok(existing.agents.clone())
}
_ => self.add_agent(existing, providers, models).await,
}
}
}
async fn add_agent(
&mut self,
existing: &Config,
providers: &HashMap<String, ProviderConfig>,
models: &HashMap<String, ModelConfig>,
) -> Result<HashMap<String, AgentConfig>, InitError> {
let agent_name = self
.prompt_with_default("Agent name", "default")
.await?;
// Select provider
let provider_names: Vec<String> = providers.keys().cloned().collect();
println!("Select provider:");
let provider_idx = self.prompt_select("", &provider_names, 0).await?;
let selected_provider = &provider_names[provider_idx];
// Select model (independently)
let model_names: Vec<String> = models.keys().cloned().collect();
println!();
println!("Select model:");
let model_idx = self.prompt_select("", &model_names, 0).await?;
let selected_model = &model_names[model_idx];
let agent = AgentConfig {
provider: selected_provider.clone(),
model: selected_model.clone(),
max_tool_iterations: 100,
tool_result_max_chars: 100_000,
context_tool_result_trim_chars: 2000,
};
let mut agents = existing.agents.clone();
agents.insert(agent_name.clone(), agent);
println!();
println!(
"Agent '{}' configured (provider: {}, model: {}).",
agent_name, selected_provider, selected_model
);
Ok(agents)
}
async fn modify_agent(
&mut self,
existing: &Config,
providers: &HashMap<String, ProviderConfig>,
models: &HashMap<String, ModelConfig>,
) -> Result<HashMap<String, AgentConfig>, InitError> {
// Select which agent to modify
let agent_names: Vec<String> = existing.agents.keys().cloned().collect();
println!("Select agent to modify:");
let agent_idx = self.prompt_select("", &agent_names, 0).await?;
let selected_agent_name = &agent_names[agent_idx];
let current_agent = existing.agents.get(selected_agent_name).unwrap();
println!();
println!(
"Current config: provider={}, model={}",
current_agent.provider, current_agent.model
);
println!();
// Select new provider
let provider_names: Vec<String> = providers.keys().cloned().collect();
let current_provider_idx = provider_names
.iter()
.position(|p| p == &current_agent.provider)
.unwrap_or(0);
println!("Select provider:");
let provider_idx = self.prompt_select("", &provider_names, current_provider_idx).await?;
let selected_provider = &provider_names[provider_idx];
// Select new model
let model_names: Vec<String> = models.keys().cloned().collect();
let current_model_idx = model_names
.iter()
.position(|m| m == &current_agent.model)
.unwrap_or(0);
println!();
println!("Select model:");
let model_idx = self.prompt_select("", &model_names, current_model_idx).await?;
let selected_model = &model_names[model_idx];
let agent = AgentConfig {
provider: selected_provider.clone(),
model: selected_model.clone(),
max_tool_iterations: current_agent.max_tool_iterations,
tool_result_max_chars: current_agent.tool_result_max_chars,
context_tool_result_trim_chars: current_agent.context_tool_result_trim_chars,
};
let mut agents = existing.agents.clone();
agents.insert(selected_agent_name.clone(), agent);
println!();
println!(
"Agent '{}' modified (provider: {}, model: {}).",
selected_agent_name, selected_provider, selected_model
);
Ok(agents)
}
// ==================== Step 4: Channels ====================
async fn configure_channels(
&mut self,
existing: &Config,
) -> Result<HashMap<String, ChannelConfig>, InitError> {
println!();
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Step 4: Configure Channels");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
// Show existing channels
if !existing.channels.is_empty() {
println!("Existing channels:");
for (name, config) in &existing.channels {
let status = if config.enabled() { "enabled" } else { "disabled" };
println!(" - {} ({})", name, status);
}
println!();
}
let mut channels = existing.channels.clone();
// Loop for configuring multiple channels
loop {
println!("Options:");
println!(" 1. Add Feishu channel");
println!(" 2. Add WeChat channel");
println!(" 3. Skip / Done");
println!();
let choice = self.prompt_with_default("Select option", "3").await?;
match choice.as_str() {
"1" => {
let channel_config = self.configure_feishu_channel(&channels).await?;
for (name, config) in channel_config {
channels.insert(name, config);
}
}
"2" => {
let channel_config = self.configure_wechat_channel(&channels).await?;
for (name, config) in channel_config {
channels.insert(name, config);
}
}
"3" => break,
_ => {
println!("Invalid selection.");
continue;
}
}
}
Ok(channels)
}
async fn configure_feishu_channel(
&mut self,
existing: &HashMap<String, ChannelConfig>,
) -> Result<HashMap<String, ChannelConfig>, InitError> {
println!();
println!("Configuring Feishu channel...");
println!();
let channel_name = self
.prompt_with_default("Channel name", "feishu")
.await?;
let _existing_config = existing.get(&channel_name).and_then(|c| c.as_feishu());
let app_id = self.prompt_required("Feishu App ID").await?;
let app_secret = self.prompt_required("Feishu App Secret").await?;
let config = ChannelConfig::Tagged(TaggedChannelConfig::Feishu(FeishuChannelConfig {
enabled: true,
app_id,
app_secret,
allow_from: vec!["*".to_string()],
agent: "default".to_string(),
media_dir: Self::default_feishu_media_dir(),
reaction_emoji: "Typing".to_string(),
max_message_chars: 20000,
reply_context_max_chars: 20000,
}));
let mut result = HashMap::new();
result.insert(channel_name.clone(), config);
println!();
println!("Feishu channel '{}' configured.", channel_name);
Ok(result)
}
async fn configure_wechat_channel(
&mut self,
_existing: &HashMap<String, ChannelConfig>,
) -> Result<HashMap<String, ChannelConfig>, InitError> {
println!();
println!("Configuring WeChat channel...");
println!();
println!("WeChat login requires scanning a QR code.");
println!("The QR code URL will be displayed after configuration.");
println!();
// Use default values directly
let channel_name = "wechat";
let base_url = "https://ilinkai.weixin.qq.com";
let cred_path = Self::default_wechat_cred_path();
let force_login = false;
let config = ChannelConfig::Tagged(TaggedChannelConfig::Wechat(WechatChannelConfig {
enabled: true,
allow_from: vec!["*".to_string()],
agent: "default".to_string(),
base_url: base_url.to_string(),
cred_path,
force_login,
}));
let mut result = HashMap::new();
result.insert(channel_name.to_string(), config);
println!();
println!("WeChat channel '{}' configured.", channel_name);
// Auto login after configuration
println!();
println!("Starting WeChat login...");
self.do_wechat_login(base_url, &Self::default_wechat_cred_path()).await?;
println!();
println!("WeChat login successful! Credentials saved.");
Ok(result)
}
async fn do_wechat_login(&mut self, base_url: &str, cred_path: &str) -> Result<(), InitError> {
use wechatbot::{BotOptions, WeChatBot};
let bot = WeChatBot::new(BotOptions {
base_url: Some(base_url.to_string()),
cred_path: Some(cred_path.to_string()),
on_qr_url: Some(Box::new(|url| {
println!();
println!("┌──────────────────────────────────────────────────────┐");
println!("│ WeChat QR Code │");
println!("└──────────────────────────────────────────────────────┘");
println!();
println!("Scan this URL in WeChat:");
println!("{}", url);
println!();
println!("Waiting for confirmation...");
})),
on_error: Some(Box::new(|error| {
eprintln!("WeChat login error: {}", error);
})),
});
let creds = bot.login(true).await.map_err(|e| {
InitError::WeChatError(format!("WeChat login failed: {}", e))
})?;
println!();
println!(
"Logged in as: {} (account: {})",
creds.user_id, creds.account_id
);
Ok(())
}
// ==================== Build & Save ====================
fn build_config(
&self,
providers: HashMap<String, ProviderConfig>,
models: HashMap<String, ModelConfig>,
agents: HashMap<String, AgentConfig>,
channels: HashMap<String, ChannelConfig>,
existing: &Config,
) -> Config {
Config {
providers,
models,
agents,
channels,
time: existing.time.clone(),
gateway: existing.gateway.clone(),
scheduler: existing.scheduler.clone(),
client: existing.client.clone(),
skills: existing.skills.clone(),
tools: existing.tools.clone(),
memory_maintenance: existing.memory_maintenance.clone(),
mcp_servers: existing.mcp_servers.clone(),
image_context: existing.image_context.clone(),
subagents: existing.subagents.clone(),
}
}
fn save_config(&self, config: &Config) -> Result<(), InitError> {
let dir = self
.config_path
.parent()
.ok_or_else(|| InitError::IoError("Invalid config path".to_string()))?;
std::fs::create_dir_all(dir)
.map_err(|e| InitError::IoError(format!("Failed to create directory: {}", e)))?;
let content = serde_json::to_string_pretty(config)
.map_err(|e| InitError::SerializeError(e.to_string()))?;
std::fs::write(&self.config_path, content)
.map_err(|e| InitError::IoError(format!("Failed to write config: {}", e)))?;
Ok(())
}
fn show_completion_message(&mut self, config: &Config) {
println!();
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!("Configuration complete!");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
println!("Config saved to: {}", self.config_path.display());
println!();
println!("Summary:");
println!(" Providers: {}", config.providers.len());
println!(" Models: {}", config.models.len());
println!(" Agents: {}", config.agents.len());
println!(" Channels: {}", config.channels.len());
println!();
println!("Next steps:");
println!(" 1. Start the gateway: picobot gateway");
println!(" 2. Connect with CLI: picobot agent");
println!();
println!("For more options, run: picobot --help");
println!();
}
fn default_feishu_media_dir() -> String {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
home.join(".picobot/media/feishu")
.to_string_lossy()
.to_string()
}
fn default_wechat_cred_path() -> String {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
home.join(".picobot/wechat/credentials.json")
.to_string_lossy()
.to_string()
}
}
impl Default for InitWizard {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub enum InitError {
IoError(String),
SerializeError(String),
InputError(String),
WeChatError(String),
}
impl std::fmt::Display for InitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InitError::IoError(msg) => write!(f, "IO error: {}", msg),
InitError::SerializeError(msg) => write!(f, "Serialization error: {}", msg),
InitError::InputError(msg) => write!(f, "Input error: {}", msg),
InitError::WeChatError(msg) => write!(f, "WeChat error: {}", msg),
}
}
}
impl std::error::Error for InitError {}
impl From<std::io::Error> for InitError {
fn from(e: std::io::Error) -> Self {
InitError::IoError(e.to_string())
}
}