use std::borrow::Cow;
use std::path::Path;
use colored::Colorize;
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::{Hinter, HistoryHinter};
use rustyline::history::DefaultHistory;
use rustyline::validate::Validator;
use rustyline::{Config, Context, Editor, Helper};
use crate::sdk::{State, ZinitClient};
pub fn run(socket_path: &Path) -> Result<(), String> {
run_repl(socket_path).map_err(|e| e.to_string())
}
const REPL_COMMANDS: &[(&str, &str)] = &[
("help", "Show this help"),
("list", "List all services"),
("status <name>", "Show service status"),
("start <name>", "Start a service"),
("stop <name>", "Stop a service"),
("restart <name>", "Restart a service"),
("kill <name> [signal]", "Send signal to service"),
("why <name>", "Show why service is blocked"),
("tree", "Show dependency tree"),
("logs <name> [lines]", "Show service logs"),
("reload", "Reload service configs from disk"),
("remove <name>", "Remove a service"),
("ping", "Ping the daemon"),
("xinet list", "List xinet proxies"),
("xinet status [name]", "Show xinet proxy status"),
("clear", "Clear screen"),
("quit", "Exit REPL (or Ctrl+D)"),
];
const CLI_COMMANDS: &[&str] = &[
"help", "list", "status", "start", "stop", "restart", "kill", "why", "tree", "logs", "reload",
"remove", "ping", "xinet", "clear", "quit", "exit",
];
struct ReplHelper {
hinter: HistoryHinter,
service_names: Vec<String>,
}
impl ReplHelper {
fn new() -> Self {
Self {
hinter: HistoryHinter::new(),
service_names: Vec::new(),
}
}
fn update_services(&mut self, names: Vec<String>) {
self.service_names = names;
}
}
impl Helper for ReplHelper {}
impl Completer for ReplHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
let mut completions = Vec::new();
let line_to_cursor = &line[..pos];
let parts: Vec<&str> = line_to_cursor.split_whitespace().collect();
if parts.is_empty() || (parts.len() == 1 && !line_to_cursor.ends_with(' ')) {
let word = parts.first().copied().unwrap_or("");
let word_start = line_to_cursor.rfind(' ').map(|i| i + 1).unwrap_or(0);
for cmd in CLI_COMMANDS {
if cmd.starts_with(word) {
completions.push(Pair {
display: cmd.to_string(),
replacement: cmd.to_string(),
});
}
}
return Ok((word_start, completions));
}
let cmd = parts[0];
let needs_service = matches!(
cmd,
"status" | "start" | "stop" | "restart" | "kill" | "why" | "logs" | "remove"
);
if needs_service
&& (parts.len() == 1 || (parts.len() == 2 && !line_to_cursor.ends_with(' ')))
{
let word = parts.get(1).copied().unwrap_or("");
let word_start = line_to_cursor.rfind(' ').map(|i| i + 1).unwrap_or(0);
for name in &self.service_names {
if name.starts_with(word) {
completions.push(Pair {
display: name.clone(),
replacement: name.clone(),
});
}
}
return Ok((word_start, completions));
}
Ok((pos, completions))
}
}
impl Hinter for ReplHelper {
type Hint = String;
fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
if let Some(hint) = self.hinter.hint(line, pos, ctx) {
return Some(hint);
}
let line_to_cursor = &line[..pos];
let parts: Vec<&str> = line_to_cursor.split_whitespace().collect();
if parts.len() == 1 && !line_to_cursor.ends_with(' ') {
let word = parts[0];
for (cmd, desc) in REPL_COMMANDS {
let cmd_name = cmd.split_whitespace().next().unwrap_or(cmd);
if cmd_name.starts_with(word) && cmd_name != word {
let suffix = &cmd_name[word.len()..];
let args = cmd.strip_prefix(cmd_name).unwrap_or("");
return Some(format!("{}{} - {}", suffix, args.dimmed(), desc.dimmed()));
}
}
}
None
}
}
impl Highlighter for ReplHelper {
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
return Cow::Borrowed(line);
}
let cmd = parts[0];
let rest = line.strip_prefix(cmd).unwrap_or("");
let highlighted_cmd = if CLI_COMMANDS.contains(&cmd) {
format!("{}", cmd.cyan().bold())
} else {
format!("{}", cmd.red())
};
let highlighted_rest = if parts.len() > 1 {
let name = parts[1];
let after_name = rest
.strip_prefix(char::is_whitespace)
.and_then(|s| s.strip_prefix(name))
.unwrap_or("");
let name_color = if self.service_names.contains(&name.to_string()) {
format!("{}", name.green())
} else {
format!("{}", name.yellow())
};
format!(" {}{}", name_color, after_name)
} else {
rest.to_string()
};
Cow::Owned(format!("{}{}", highlighted_cmd, highlighted_rest))
}
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
_default: bool,
) -> Cow<'b, str> {
Cow::Owned(format!("{}", prompt.cyan().bold()))
}
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Cow::Owned(format!("{}", hint.bright_black()))
}
fn highlight_char(
&self,
_line: &str,
_pos: usize,
_kind: rustyline::highlight::CmdKind,
) -> bool {
true
}
}
impl Validator for ReplHelper {}
fn print_banner() {
println!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
println!(
"{}",
"║ zinit Interactive Shell ║".cyan()
);
println!(
"{}",
"╠═══════════════════════════════════════════════════════════╣".cyan()
);
println!(
"{} {}",
"║".cyan(),
format!(
"{:<56} {}",
"Tab: completion | Up/Down: history | Ctrl+D: exit", "║"
)
.cyan()
);
println!(
"{} {}",
"║".cyan(),
format!("{:<56} {}", "Type 'help' for commands", "║").cyan()
);
println!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
println!();
}
fn print_help() {
println!("{}", "Commands:".yellow().bold());
for (cmd, desc) in REPL_COMMANDS {
println!(" {:25} {}", cmd.cyan(), desc);
}
println!();
println!("{}", "Tips:".yellow().bold());
println!(
" - Press {} to autocomplete commands and service names",
"Tab".cyan()
);
println!(" - Use {} arrows for command history", "Up/Down".cyan());
println!(" - Service names are auto-completed from running services");
}
fn execute_command(client: &mut ZinitClient, line: &str) -> Result<bool, String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
return Ok(true);
}
let cmd = parts[0];
let args = &parts[1..];
match cmd {
"help" => {
print_help();
}
"quit" | "exit" => {
return Ok(false);
}
"clear" => {
print!("\x1B[2J\x1B[1;1H");
print_banner();
}
"list" | "ls" => {
let names = client.list().map_err(|e| e.to_string())?;
if names.is_empty() {
println!("No services configured");
} else {
let max_len = names.iter().map(|n| n.len()).max().unwrap_or(10);
for name in names {
if let Ok(status) = client.status(&name) {
let pid_str = if status.pid > 0 {
format!(" (pid: {})", status.pid)
} else {
String::new()
};
let symbol = match status.state {
State::Running => "●".green(),
State::Starting | State::Stopping => "◐".yellow(),
State::Blocked | State::Inactive => "○".normal(),
State::Exited => "◌".normal(),
State::Failed => "✗".red(),
};
println!(
"{} {:<width$} {:?}{}",
symbol,
name,
status.state,
pid_str,
width = max_len
);
} else {
println!("? {:<width$} unknown", name, width = max_len);
}
}
}
}
"status" => {
let name = args.first().ok_or("Usage: status <name>")?;
let status = client.status(name).map_err(|e| e.to_string())?;
let symbol = match status.state {
State::Running => "●".green(),
State::Starting | State::Stopping => "◐".yellow(),
State::Blocked | State::Inactive => "○".normal(),
State::Exited => "◌".normal(),
State::Failed => "✗".red(),
};
println!("Service: {}", status.name);
println!("State: {} {:?}", symbol, status.state);
if status.pid > 0 {
println!("PID: {}", status.pid);
}
if let Some(code) = status.exit_code {
println!("Exit: {}", code);
}
if let Some(ref err) = status.error {
println!("Error: {}", err.red());
}
}
"start" => {
let name = args.first().ok_or("Usage: start <name>")?;
client.start(name).map_err(|e| e.to_string())?;
println!("{} Started: {}", "+".green(), name);
}
"stop" => {
let name = args.first().ok_or("Usage: stop <name>")?;
client.stop(name).map_err(|e| e.to_string())?;
println!("{} Stopped: {}", "-".yellow(), name);
}
"restart" => {
let name = args.first().ok_or("Usage: restart <name>")?;
client.restart(name).map_err(|e| e.to_string())?;
println!("{} Restarted: {}", "*".cyan(), name);
}
"kill" => {
let name = args.first().ok_or("Usage: kill <name> [signal]")?;
let signal = args.get(1).copied();
client.kill(name, signal).map_err(|e| e.to_string())?;
let sig = signal.unwrap_or("SIGTERM");
println!("{} Sent {} to: {}", "!".red(), sig, name);
}
"why" => {
let name = args.first().ok_or("Usage: why <name>")?;
let why = client.why(name).map_err(|e| e.to_string())?;
if !why.blocked {
println!("{} is not blocked", name.green());
} else {
println!("{}", why.ascii);
if !why.waiting_on.is_empty() {
println!("\nWaiting on: {}", why.waiting_on.join(", ").yellow());
}
if !why.conflicts_with.is_empty() {
println!("Conflicts with: {}", why.conflicts_with.join(", ").red());
}
}
}
"tree" => {
let tree = client.tree().map_err(|e| e.to_string())?;
println!("{}", tree);
}
"logs" => {
let name = args.first().ok_or("Usage: logs <name> [lines]")?;
let lines: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(50);
let logs = client.logs(name, Some(lines)).map_err(|e| e.to_string())?;
if logs.is_empty() {
println!("No logs available for {}", name);
} else {
for log in logs {
println!("{}", log);
}
}
}
"reload" => {
let result = client.reload().map_err(|e| e.to_string())?;
if result.added.is_empty() && result.removed.is_empty() && result.changed.is_empty() {
println!("No changes detected");
} else {
if !result.added.is_empty() {
println!("{} {}", "Added:".green(), result.added.join(", "));
}
if !result.removed.is_empty() {
println!("{} {}", "Removed:".red(), result.removed.join(", "));
}
if !result.changed.is_empty() {
println!("{} {}", "Changed:".yellow(), result.changed.join(", "));
}
}
}
"remove" => {
let name = args.first().ok_or("Usage: remove <name>")?;
client.remove(name).map_err(|e| e.to_string())?;
println!("{} Removed: {}", "-".red(), name);
}
"ping" => {
let version = client.ping().map_err(|e| e.to_string())?;
println!("{} zinit daemon v{} is running", "+".green(), version);
}
"xinet" => {
let subcmd = args.first().ok_or("Usage: xinet <list|status [name]>")?;
match *subcmd {
"list" => {
let proxies = client.xinet_list().map_err(|e| e.to_string())?;
if proxies.is_empty() {
println!("No xinet proxies registered");
} else {
println!("{}", "Registered xinet proxies:".cyan());
for name in proxies {
println!(" {}", name);
}
}
}
"status" => {
let name = args.get(1).copied();
match name {
Some(n) => {
let status = client.xinet_status(n).map_err(|e| e.to_string())?;
println!("{}: {}", "Proxy".cyan(), status.name);
println!(" Listen: {}", status.listen);
println!(" Backend: {}", status.backend);
println!(" Service: {}", status.service);
println!(
" Status: {}",
if status.running {
"running".green()
} else {
"stopped".yellow()
}
);
println!(
" Connections: {} active, {} total",
status.active_connections, status.total_connections
);
}
None => {
let statuses = client.xinet_status_all().map_err(|e| e.to_string())?;
if statuses.is_empty() {
println!("No xinet proxies registered");
} else {
for (i, status) in statuses.iter().enumerate() {
if i > 0 {
println!();
}
println!("{}: {}", "Proxy".cyan(), status.name);
println!(" Listen: {}", status.listen);
println!(" Backend: {}", status.backend);
println!(" Service: {}", status.service);
println!(
" Status: {}",
if status.running {
"running".green()
} else {
"stopped".yellow()
}
);
println!(
" Connections: {} active, {} total",
status.active_connections, status.total_connections
);
}
}
}
}
}
_ => {
println!(
"{}: Unknown xinet subcommand '{}'. Use: list, status",
"Error".red(),
subcmd
);
}
}
}
_ => {
println!(
"{}: Unknown command '{}'. Type 'help' for commands.",
"Error".red(),
cmd
);
}
}
Ok(true)
}
fn run_repl(socket_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
print_banner();
let mut client = match ZinitClient::connect(socket_path) {
Ok(c) => c,
Err(e) => {
println!(
"{}: Could not connect to zinit at {}: {}",
"Warning".yellow(),
socket_path.display(),
e
);
println!("Some commands will not work until the server is running.");
println!();
return Err(Box::new(e));
}
};
match client.ping() {
Ok(version) => {
println!("{} Connected to zinit daemon v{}", "+".green(), version);
}
Err(e) => {
println!("{}: Could not ping zinit server: {}", "Warning".yellow(), e);
}
}
println!();
let config = Config::builder()
.history_ignore_space(true)
.completion_type(rustyline::CompletionType::List)
.edit_mode(rustyline::EditMode::Emacs)
.build();
let mut helper = ReplHelper::new();
if let Ok(names) = client.list() {
helper.update_services(names);
}
let mut rl: Editor<ReplHelper, DefaultHistory> = Editor::with_config(config)?;
rl.set_helper(Some(helper));
let history_path = dirs::home_dir()
.map(|h| h.join(".zinit_history"))
.unwrap_or_else(|| std::path::PathBuf::from(".zinit_history"));
let _ = rl.load_history(&history_path);
loop {
match rl.readline("zinit> ") {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
let _ = rl.add_history_entry(line);
match execute_command(&mut client, line) {
Ok(true) => {
if (line.starts_with("start")
|| line.starts_with("stop")
|| line.starts_with("reload")
|| line.starts_with("remove"))
&& let Ok(names) = client.list()
&& let Some(helper) = rl.helper_mut()
{
helper.update_services(names);
}
}
Ok(false) => {
println!("{}", "Goodbye!".cyan());
break;
}
Err(e) => {
println!("{}: {}", "Error".red().bold(), e);
}
}
println!();
}
Err(ReadlineError::Interrupted) => {
println!("{}", "^C (use 'quit' or Ctrl+D to exit)".bright_black());
}
Err(ReadlineError::Eof) => {
println!("{}", "Goodbye!".cyan());
break;
}
Err(err) => {
println!("{}: {:?}", "Error".red(), err);
break;
}
}
}
let _ = rl.save_history(&history_path);
Ok(())
}