use std::io::IsTerminal;
use std::path::PathBuf;
use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, MultiSelect};
use toml_edit::DocumentMut;
use super::model::{
SetupAnswers, agent_key_and_command, build_config, detect_installed_agents, home_dir,
plugins_edit_command, plugins_resume_command, preview_paths, read_existing_defaults,
save_config,
};
use crate::agents::CodingAgent;
use crate::error::CliError;
pub(crate) fn prompt_user(
detected_agents: &[CodingAgent],
agent_hint: Option<CodingAgent>,
) -> Result<SetupAnswers, CliError> {
ensure_tty()?;
let defaults = read_existing_defaults().unwrap_or_default();
crate::banner::print_intro();
match agent_hint {
Some(agent) => {
let (name, _) = agent_key_and_command(agent);
println!(" Setting up {name}.");
println!(" Re-run `nemo-relay config` later to configure additional agents.");
}
None => {
println!(" Let's set up your coding agent.");
println!(" This runs once. Re-run later with `nemo-relay config`.");
}
}
if agent_hint.is_none() {
println!();
print_detected_agents(detected_agents);
}
if defaults.has_any() {
println!();
println!(" Existing config detected — current values are pre-selected.");
}
println!();
println!(
" Tip: ↑/↓ to move, SPACE to toggle a checkbox, ENTER to confirm. Defaults are pre-selected."
);
println!();
let theme = ColorfulTheme::default();
let agents = match agent_hint {
Some(agent) => vec![agent],
None => ask_agents(&theme, detected_agents, &defaults.agents)?,
};
if agents.contains(&CodingAgent::Codex) {
print_codex_api_key_guide();
}
Ok(SetupAnswers { agents })
}
pub(super) async fn run(
agent_hint: Option<CodingAgent>,
explicit_plugin_path: Option<PathBuf>,
) -> Result<(), CliError> {
let detected = detect_installed_agents();
let answers = prompt_user(&detected, agent_hint)?;
let home = home_dir().ok_or_else(|| {
CliError::Config("cannot determine home directory (set $HOME or $USERPROFILE)".into())
})?;
let doc = build_config(&answers);
let preview_paths = preview_paths(&home);
if !confirm_summary(&preview_paths, &doc)? {
return Err(CliError::Config("setup cancelled — no config saved".into()));
}
let written = save_config(&doc, &home, agent_hint)?;
println!();
println!(" ✓ Saved:");
for path in &written {
println!(" {}", path.display());
}
println!();
continue_to_plugins(explicit_plugin_path)
}
fn continue_to_plugins(explicit_plugin_path: Option<PathBuf>) -> Result<(), CliError> {
let resume_command = plugins_resume_command(explicit_plugin_path.as_deref());
let proceed = match confirm_plugin_setup() {
Ok(proceed) => proceed,
Err(error) if super::plugin_prompt_was_interrupted(&error) => {
print_plugins_skipped(&resume_command);
return Ok(());
}
Err(error) => {
return Err(CliError::Config(format!(
"plugin setup did not complete; base configuration remains saved. \
Resume with `{}`. Cause: {error}",
resume_command
)));
}
};
if !proceed {
print_plugins_skipped(&resume_command);
return Ok(());
}
let result = crate::plugins::edit(plugins_edit_command(explicit_plugin_path));
result.map_err(|error| {
let cause = match error {
CliError::Config(message) => message,
other => other.to_string(),
};
CliError::Config(format!(
"plugin setup did not complete; base configuration remains saved. \
Resume with `{}`. Cause: {cause}",
resume_command
))
})
}
pub(super) fn confirm_plugin_setup() -> Result<bool, dialoguer::Error> {
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Configure Relay plugins now?")
.default(true)
.interact()
}
pub(super) fn print_plugins_skipped(resume_command: &str) {
println!();
println!(" Base configuration saved. Plugin configuration skipped.");
println!(" Configure plugins later with `{resume_command}`.");
println!();
}
fn print_codex_api_key_guide() {
println!();
println!(" ℹ Codex sends Responses-API requests through the gateway.");
println!(" Authentication (pick one):");
println!(" • ChatGPT-Plus login: codex --login (uses ~/.codex/auth.json)");
println!(" • OpenAI API key: export OPENAI_API_KEY=sk-...");
println!(" When OPENAI_API_KEY is set the gateway uses it; otherwise the");
println!(" ChatGPT-Plus OAuth token is forwarded to the ChatGPT backend.");
println!();
}
fn ensure_tty() -> Result<(), CliError> {
if !std::io::stdin().is_terminal() {
return Err(CliError::Config(
"interactive setup requires a TTY; pass `--config <path>` or set up \
`$XDG_CONFIG_HOME/nemo-relay/config.toml` manually"
.into(),
));
}
Ok(())
}
fn print_detected_agents(detected: &[CodingAgent]) {
println!(" Detected agents on $PATH:");
for agent in detected {
let (name, _) = agent_key_and_command(*agent);
println!(" ✓ {name}");
}
if detected.is_empty() {
println!(" (none — you can still add agents later)");
}
}
fn ask_agents(
theme: &ColorfulTheme,
detected: &[CodingAgent],
configured: &[CodingAgent],
) -> Result<Vec<CodingAgent>, CliError> {
let all_supported = [CodingAgent::ClaudeCode, CodingAgent::Codex];
let labels: Vec<String> = all_supported
.iter()
.map(|a| {
let (name, _) = agent_key_and_command(*a);
name.to_string()
})
.collect();
let defaults: Vec<bool> = if configured.is_empty() {
all_supported.iter().map(|a| detected.contains(a)).collect()
} else {
all_supported
.iter()
.map(|a| configured.contains(a))
.collect()
};
let selected_idx = MultiSelect::with_theme(theme)
.with_prompt("Which agents to observe?")
.items(&labels)
.defaults(&defaults)
.interact()
.map_err(setup_error)?;
Ok(selected_idx.into_iter().map(|i| all_supported[i]).collect())
}
pub(crate) fn confirm_summary(
written_paths: &[PathBuf],
doc: &DocumentMut,
) -> Result<bool, CliError> {
println!();
println!(" ─── Summary ─────────────────────────────────────────────");
println!(" Will write to:");
for path in written_paths {
println!(" {}", path.display());
}
println!();
println!(" Contents:");
for line in doc.to_string().lines() {
println!(" {line}");
}
println!();
Confirm::with_theme(&ColorfulTheme::default())
.with_prompt("Looks good?")
.default(true)
.interact()
.map_err(setup_error)
}
fn setup_error(err: dialoguer::Error) -> CliError {
match err {
dialoguer::Error::IO(io_err)
if matches!(
io_err.kind(),
std::io::ErrorKind::Interrupted | std::io::ErrorKind::UnexpectedEof
) =>
{
CliError::Config("setup cancelled — no config saved".into())
}
other => CliError::Config(format!("setup error: {other}")),
}
}