use crate::config::Config;
use anyhow::{Context, Result};
use colored::Colorize;
use std::path::Path;
const VALID_TYPES: &[&str] = &["task", "note", "log", "reminder", "character"];
const VALID_COMMANDS: &[&str] = &[
"open", "list", "append", "update", "edit", "delete", "done", "search", "stats", "tags",
"export", "config", "git", "autogit", "cmd", "notify", "daemon", "meta", "chat", "serve",
"version", "init",
];
pub fn run(
config: &Config,
config_path: &Path,
subcommand: Option<&str>,
no_network: bool,
) -> Result<()> {
match subcommand.unwrap_or("show").to_lowercase().as_str() {
"show" => {
println!("{}", "MPS configuration".white());
println!(" config file : {}", config_path.display());
println!(" mps_dir : {}", config.mps_dir.display());
println!(" storage_dir : {}", config.storage_dir.display());
println!(" log_file : {}", config.log_file.display());
println!(" git_remote : {}", config.git_remote);
println!(" git_branch : {}", config.git_branch);
println!(" default_cmd : {}", config.default_command);
if !config.type_aliases.is_empty() {
let mut pairs: Vec<String> = config
.type_aliases
.iter()
.map(|(k, v)| format!("{}→{}", k, v))
.collect();
pairs.sort();
println!(" type_aliases : {}", pairs.join(", "));
}
if !config.command_aliases.is_empty() {
let mut pairs: Vec<String> = config
.command_aliases
.iter()
.map(|(k, v)| format!("{}→{}", k, v))
.collect();
pairs.sort();
println!(" command_aliases : {}", pairs.join(", "));
}
if !config.custom_tags.is_empty() {
println!(" custom_tags : {}", config.custom_tags.join(", "));
}
let n = &config.notify;
println!(" notify.enabled : {}", n.enabled);
if let Some(ref t) = n.task_notify_at {
println!(" notify.task_notify_at : {}", t);
}
println!(" notify.window_minutes : {}", n.window_minutes);
println!(" notify.overdue_days : {}", n.overdue_days);
let c = &config.chat;
println!();
println!(
"{}",
" ── chat ─────────────────────────────────────".white()
);
println!(
" chat.url : {}",
c.url
.as_deref()
.unwrap_or("(auto-detect :11434 then :8080)")
);
println!(" chat.model : {}", c.model);
println!(" chat.context_days : {}", c.context_days);
println!(" chat.stream : {}", c.stream);
println!(" chat.connect_timeout_secs: {}", c.connect_timeout_secs);
println!(
" chat.api_key : {}",
if c.api_key.is_empty() {
"(not set)".to_string()
} else {
"***".to_string()
}
);
println!(
" chat.sessions_dir : {}",
c.sessions_dir
.as_deref()
.unwrap_or("(default: ~/.mps/sessions/)")
);
}
"edit" => {
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
.unwrap_or_else(|_| "vim".to_string());
println!(
"{}",
format!("Opening {} in editor", config_path.display()).white()
);
std::process::Command::new(&editor)
.arg(config_path)
.status()
.with_context(|| format!("failed to launch editor '{}'", editor))?;
}
"init" => {
let raw = if config_path.exists() {
Config::load(config_path)?
} else {
Config::default_config()?
};
raw.save(config_path)?;
println!("{} {}", "written:".green(), config_path.display());
println!(" All config keys are now explicitly present in the YAML file.");
println!(" chat.api_key and sessions_dir are local-only (never synced).");
println!(
" {} run {} to open in your editor",
"tip:".cyan(),
"mps config edit".bold()
);
}
"check" => {
run_check(config, config_path, no_network)?;
}
other => {
println!(
"{}",
format!(
"Usage: mps config [show|edit|init|check] (got '{}')",
other
)
.yellow()
);
}
}
Ok(())
}
fn run_check(config: &Config, config_path: &Path, no_network: bool) -> Result<()> {
println!("{}", "Config health check".white().bold());
println!(" {}", config_path.display());
println!();
let mut failures = 0usize;
failures += check_exists(
"mps_dir ",
&config.mps_dir.display().to_string(),
config.mps_dir.exists(),
);
failures += check_exists(
"storage_dir",
&config.storage_dir.display().to_string(),
config.storage_dir.exists(),
);
failures += check_exists(
"log_file ",
&config.log_file.display().to_string(),
config.log_file.exists(),
);
let mut type_alias_ok = true;
for (k, v) in &config.type_aliases {
if !VALID_TYPES.contains(&v.to_lowercase().as_str()) {
println!(
" {} type_aliases : {}→{} — '{}' is not a valid element type ({})",
"✗".red().bold(),
k,
v,
v,
VALID_TYPES.join(", ")
);
type_alias_ok = false;
failures += 1;
}
}
if type_alias_ok {
if config.type_aliases.is_empty() {
println!(" {} type_aliases : (none)", "✓".green().bold());
} else {
let pairs: Vec<String> = config
.type_aliases
.iter()
.map(|(k, v)| format!("{}→{}", k, v))
.collect();
println!(
" {} type_aliases : {}",
"✓".green().bold(),
pairs.join(", ")
);
}
}
let mut cmd_alias_ok = true;
for (k, v) in &config.command_aliases {
if !VALID_COMMANDS.contains(&v.to_lowercase().as_str()) {
println!(
" {} command_aliases: {}→{} — '{}' is not a known command",
"✗".red().bold(),
k,
v,
v
);
cmd_alias_ok = false;
failures += 1;
}
}
if cmd_alias_ok {
if config.command_aliases.is_empty() {
println!(" {} command_aliases: (none)", "✓".green().bold());
} else {
let pairs: Vec<String> = config
.command_aliases
.iter()
.map(|(k, v)| format!("{}→{}", k, v))
.collect();
println!(
" {} command_aliases: {}",
"✓".green().bold(),
pairs.join(", ")
);
}
}
if no_network {
println!(" {} chat.url : skipped (--no-network)", "─".white());
} else {
match &config.chat.url {
None => {
println!(
" {} chat.url : (auto-detect — skipped)",
"─".white()
);
}
Some(url) => {
if tcp_reachable(url) {
println!(
" {} chat.url : {} (reachable)",
"✓".green().bold(),
url
);
} else {
println!(
" {} chat.url : {} — cannot connect",
"✗".red().bold(),
url
);
failures += 1;
}
}
}
}
println!();
if failures == 0 {
println!(" {} all checks passed", "✓".green().bold());
} else {
eprintln!(
" {} {} check(s) failed — run {} to fix",
"✗".red().bold(),
failures,
"mps config edit".bold()
);
std::process::exit(1);
}
Ok(())
}
fn check_exists(label: &str, value: &str, exists: bool) -> usize {
if exists {
println!(" {} {} : {}", "✓".green().bold(), label, value);
0
} else {
println!(
" {} {} : {} — does not exist",
"✗".red().bold(),
label,
value
);
1
}
}
fn tcp_reachable(url: &str) -> bool {
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
let is_https = url.starts_with("https://");
let stripped = url
.trim_start_matches("https://")
.trim_start_matches("http://");
let host_port = stripped.split('/').next().unwrap_or(stripped);
let default_port = if is_https { 443u16 } else { 80u16 };
let addr_str = if host_port.contains(':') {
host_port.to_string()
} else {
format!("{}:{}", host_port, default_port)
};
match addr_str.to_socket_addrs() {
Ok(mut addrs) => addrs
.next()
.and_then(|addr| TcpStream::connect_timeout(&addr, Duration::from_secs(3)).ok())
.is_some(),
Err(_) => false,
}
}