use std::fmt;
use super::*;
use crate::{
commands::{
login,
mcp::install as mcp_install,
skills::{self, coding_tools},
},
consts::{RAILWAY_API_TOKEN_ENV, RAILWAY_TOKEN_ENV},
controllers::user::get_user,
macros::is_stdout_terminal,
telemetry::{self, SetupAgentPhase, SetupAgentTrackEvent},
};
const DOCS_URL: &str = "https://docs.railway.com/ai";
#[derive(Parser)]
pub struct Args {
#[clap(subcommand)]
command: SetupCommand,
}
#[derive(Parser)]
enum SetupCommand {
Agent(AgentArgs),
}
#[derive(Parser)]
pub struct AgentArgs {
#[clap(short = 'y', long)]
yes: bool,
#[clap(long)]
remote: bool,
}
pub async fn command(args: Args) -> Result<()> {
match args.command {
SetupCommand::Agent(a) => agent_setup(a).await,
}
}
#[derive(Clone)]
struct ToolChoice {
slug: &'static str,
name: &'static str,
detected: bool,
}
impl fmt::Display for ToolChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.detected {
write!(f, "{} {}", self.name, "(detected)".dimmed())
} else {
write!(f, "{}", self.name)
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum McpChoice {
Local,
Remote,
Skip,
}
impl fmt::Display for McpChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
McpChoice::Local => write!(
f,
"Local (default) {}",
"— runs `railway mcp` as a stdio server".dimmed()
),
McpChoice::Remote => write!(
f,
"Remote {}",
"— https://mcp.railway.com (HTTP)".dimmed()
),
McpChoice::Skip => write!(f, "Skip {}", "— don't configure MCP".dimmed()),
}
}
}
fn pick_mcp_choice(remote_flag: bool, non_interactive: bool) -> Result<McpChoice> {
if remote_flag {
return Ok(McpChoice::Remote);
}
if non_interactive {
return Ok(McpChoice::Local);
}
let options = vec![McpChoice::Local, McpChoice::Remote, McpChoice::Skip];
inquire::Select::new("Configure MCP server:", options)
.with_render_config(Configs::get_render_config())
.prompt()
.context("Failed to prompt for MCP transport")
}
async fn agent_setup(args: AgentArgs) -> Result<()> {
telemetry::send_setup_agent(SetupAgentTrackEvent {
phase: SetupAgentPhase::Start,
success: None,
error_message: None,
configured_clients: None,
})
.await;
match agent_setup_inner(args).await {
Ok(configured_clients) => {
telemetry::send_setup_agent(SetupAgentTrackEvent {
phase: SetupAgentPhase::Finish,
success: Some(true),
error_message: None,
configured_clients: Some(configured_clients),
})
.await;
Ok(())
}
Err(err) => {
let message = err.to_string();
telemetry::send_setup_agent(SetupAgentTrackEvent {
phase: SetupAgentPhase::Finish,
success: Some(false),
error_message: Some(if message.len() > 256 {
message[..256].to_string()
} else {
message
}),
configured_clients: None,
})
.await;
Err(err)
}
}
}
async fn agent_setup_inner(args: AgentArgs) -> Result<Vec<String>> {
let home = dirs::home_dir().context("could not determine home directory")?;
let non_interactive = args.yes || !is_stdout_terminal();
println!("\n{}\n", "Railway Agent Setup".bold().cyan());
let choices: Vec<ToolChoice> = coding_tools(&home)
.into_iter()
.map(|tool| ToolChoice {
slug: tool.slug,
name: tool.name,
detected: tool.slug == "universal" || tool.global_parent.is_dir(),
})
.collect();
let selected_slugs: Vec<String> = if non_interactive {
let detected: Vec<String> = choices
.iter()
.filter(|c| c.detected)
.map(|c| c.slug.to_string())
.collect();
println!("{} {}\n", "Detected:".bold(), detected.join(", ").cyan());
detected
} else {
let default_indices: Vec<usize> = choices
.iter()
.enumerate()
.filter(|(_, c)| c.detected)
.map(|(i, _)| i)
.collect();
let picked = inquire::MultiSelect::new("Which editors should we set up?", choices.clone())
.with_default(&default_indices)
.with_render_config(Configs::get_render_config())
.prompt()
.context("Failed to prompt for editor selection")?;
if picked.is_empty() {
println!("{}", "No editors selected. Nothing to do.".yellow());
return Ok(Vec::new());
}
picked.iter().map(|c| c.slug.to_string()).collect()
};
if selected_slugs.is_empty() {
println!(
"{}",
"No editors detected. Re-run interactively to pick, or rerun in a TTY.".yellow()
);
return Ok(Vec::new());
}
let configured_clients = selected_slugs.clone();
let missing_skills: Vec<String> = selected_slugs
.iter()
.filter(|slug| !skills::skills_configured_for_slug(&home, slug))
.cloned()
.collect();
if missing_skills.is_empty() {
println!(
"\n{} {}",
"-".dimmed(),
"Railway skills already configured; skipping install.".dimmed()
);
} else {
skills::install_skills(&missing_skills).await?;
}
let mcp_choice = pick_mcp_choice(args.remote, non_interactive)?;
match mcp_choice {
McpChoice::Local => install_missing_mcp(&home, &selected_slugs, false).await?,
McpChoice::Remote => install_missing_mcp(&home, &selected_slugs, true).await?,
McpChoice::Skip => {
println!(
"\n{} {}",
"-".dimmed(),
"Skipping MCP install. Run `railway mcp install` later to configure.".dimmed()
);
}
}
if non_interactive {
warn_if_not_logged_in().await;
} else {
ensure_logged_in_interactive().await?;
}
println!(
"\n{} {} {}\n",
"\u{2713}".green().bold(),
"Setup complete. Learn more:".bold(),
DOCS_URL.purple()
);
if let Err(e) = crate::util::agent_advisory::record_setup_complete() {
eprintln!("{}: {e}", "Warning: failed to record agent setup".yellow());
}
Ok(configured_clients)
}
async fn install_missing_mcp(
home: &std::path::Path,
selected_slugs: &[String],
remote: bool,
) -> Result<()> {
let missing_mcp: Vec<String> = selected_slugs
.iter()
.filter(|slug| slug.as_str() != "universal")
.filter(|slug| !mcp_install::mcp_configured_for_slug(home, slug, remote))
.cloned()
.collect();
if missing_mcp.is_empty() {
println!(
"\n{} {}",
"-".dimmed(),
"Railway MCP already configured; skipping install.".dimmed()
);
return Ok(());
}
mcp_install::install_mcp(&missing_mcp, remote).await
}
async fn warn_if_not_logged_in() {
let configs = match Configs::new() {
Ok(c) => c,
Err(_) => {
print_login_warning();
return;
}
};
let token_name = if Configs::get_railway_token().is_some() {
Some(RAILWAY_TOKEN_ENV)
} else if Configs::get_railway_api_token().is_some() {
Some(RAILWAY_API_TOKEN_ENV)
} else {
None
};
if let Some(name) = token_name {
if let Ok(client) = GQLClient::new_authorized(&configs) {
if get_user(&client, &configs).await.is_ok() {
println!("\n{} {}", "Logged in via".bold(), name.cyan());
return;
}
}
}
if let Ok(client) = GQLClient::new_authorized(&configs) {
if get_user(&client, &configs).await.is_ok() {
println!("\n{}", "Already logged in.".bold());
return;
}
}
print_login_warning();
}
fn print_login_warning() {
println!(
"\n{} {}",
"!".yellow().bold(),
"Not logged in. Run `railway login` to finish setup.".yellow()
);
}
async fn ensure_logged_in_interactive() -> Result<()> {
if let Ok(configs) = Configs::new() {
if let Ok(client) = GQLClient::new_authorized(&configs) {
if get_user(&client, &configs).await.is_ok() {
println!("\n{}", "Already logged in.".bold());
return Ok(());
}
}
}
println!("\n{}", "Logging in to Railway...".bold());
login::command(login::Args { browserless: false }).await
}