1mod backend;
4mod railway;
5mod state;
6
7use std::collections::HashMap;
8use std::time::Duration;
9
10use anyhow::{Context, Result};
11use tracing::info;
12
13use crate::cli::{DeployArgs, DestroyArgs, ListArgs};
14use crate::template::load_template;
15
16pub use backend::DeployBackend;
17pub use state::DeploymentState;
18
19pub async fn run(args: DeployArgs) -> Result<()> {
21 let template = load_template(&args.agent, args.template_path.as_deref())?;
22
23 info!(
24 agent = %args.agent,
25 target = %args.target,
26 services = template.services.len(),
27 "deploying agent"
28 );
29
30 let project_name = args
31 .project_name
32 .unwrap_or_else(|| format!("life-{}", args.agent));
33
34 let mut extra_env: HashMap<String, String> = HashMap::new();
36 for kv in &args.env {
37 if let Some((k, v)) = kv.split_once('=') {
38 extra_env.insert(k.to_string(), v.to_string());
39 }
40 }
41
42 if let Some(arcan) = template.services.get("arcan") {
44 let _ = arcan; extra_env
46 .entry("ARCAN_PROVIDER".to_string())
47 .or_insert_with(|| args.provider.clone());
48 }
49
50 let backend = create_backend(&args.target)?;
51
52 println!(
53 "Deploying {agent} to {target}...",
54 agent = args.agent,
55 target = args.target
56 );
57 println!(
58 " Template: {name} — {desc}",
59 name = template.meta.name,
60 desc = template.meta.description
61 );
62 println!(
63 " Services: {svcs}",
64 svcs = template
65 .services
66 .keys()
67 .cloned()
68 .collect::<Vec<_>>()
69 .join(", ")
70 );
71 println!();
72
73 let result = backend.deploy(&project_name, &template, &extra_env).await?;
75
76 let state = DeploymentState {
78 agent_name: args.agent.clone(),
79 project_name: project_name.clone(),
80 target: args.target.clone(),
81 project_id: result.project_id.clone(),
82 environment_id: result.environment_id.clone(),
83 services: result.services.clone(),
84 deployed_at: chrono::Utc::now(),
85 template_name: template.meta.name.clone(),
86 };
87 state.save()?;
88
89 println!("Deployment initiated:");
90 for (name, svc) in &result.services {
91 let url = svc.url.as_deref().unwrap_or("(internal)");
92 println!(" {name}: {url} (service_id: {id})", id = svc.service_id);
93 }
94 println!();
95
96 if !args.no_wait {
98 println!("Waiting for services to become healthy...");
99 let timeout = Duration::from_secs(300);
100 let poll_interval = Duration::from_secs(10);
101 let start = std::time::Instant::now();
102
103 loop {
104 if start.elapsed() > timeout {
105 eprintln!("Timeout: not all services became healthy within 5 minutes.");
106 eprintln!(
107 "Run `life status --agent {agent}` to check progress.",
108 agent = args.agent
109 );
110 break;
111 }
112
113 let status = backend.status(&result.project_id).await;
114 match status {
115 Ok(statuses) => {
116 let all_healthy = statuses.iter().all(|(_, s)| {
117 matches!(
118 s.status.as_str(),
119 "SUCCESS" | "HEALTHY" | "RUNNING" | "ACTIVE"
120 )
121 });
122
123 if all_healthy && !statuses.is_empty() {
124 println!();
125 println!("All services healthy!");
126 for (name, s) in &statuses {
127 println!(" {name}: {} {}", s.status, s.url.as_deref().unwrap_or(""));
128 }
129 break;
130 }
131
132 print!(".");
133 }
134 Err(_) => {
135 print!("?");
136 }
137 }
138
139 tokio::time::sleep(poll_interval).await;
140 }
141 }
142
143 println!();
144 println!("Agent deployed: {project_name}");
145 println!(" life status --agent {agent}", agent = args.agent);
146 println!(" life cost --agent {agent}", agent = args.agent);
147 println!(" life destroy --agent {agent}", agent = args.agent);
148
149 Ok(())
150}
151
152pub async fn destroy(args: DestroyArgs) -> Result<()> {
154 let state = DeploymentState::load(&args.agent)
155 .with_context(|| format!("no deployment found for agent '{}'", args.agent))?;
156
157 if !args.yes {
158 println!(
159 "This will permanently destroy agent '{agent}' (project: {project}).",
160 agent = args.agent,
161 project = state.project_name,
162 );
163 println!(" Target: {}", state.target);
164 println!(
165 " Services: {}",
166 state
167 .services
168 .keys()
169 .cloned()
170 .collect::<Vec<_>>()
171 .join(", ")
172 );
173 println!();
174 println!("Type 'yes' to confirm:");
175
176 let mut input = String::new();
177 std::io::stdin().read_line(&mut input)?;
178 if input.trim() != "yes" {
179 println!("Aborted.");
180 return Ok(());
181 }
182 }
183
184 let backend = create_backend(&state.target)?;
185
186 println!("Destroying agent '{}'...", args.agent);
187 backend.destroy(&state.project_id).await?;
188
189 state.remove()?;
191
192 println!("Agent '{}' destroyed.", args.agent);
193 Ok(())
194}
195
196pub async fn list(args: ListArgs) -> Result<()> {
198 let states = DeploymentState::list_all()?;
199
200 if states.is_empty() {
201 println!("No deployed agents found.");
202 println!("Deploy one with: life deploy --agent coding-agent --target railway");
203 return Ok(());
204 }
205
206 match &args.format[..] {
207 "json" => {
208 let output: Vec<serde_json::Value> = states
209 .iter()
210 .map(|s| {
211 serde_json::json!({
212 "agent": s.agent_name,
213 "project": s.project_name,
214 "target": s.target,
215 "template": s.template_name,
216 "services": s.services.len(),
217 "deployed_at": s.deployed_at.to_rfc3339(),
218 })
219 })
220 .collect();
221 println!("{}", serde_json::to_string_pretty(&output)?);
222 }
223 _ => {
224 println!(
225 "{:<20} {:<25} {:<10} {:<18} {:<6} Deployed",
226 "Agent", "Project", "Target", "Template", "Svcs"
227 );
228 println!("{}", "─".repeat(95));
229
230 for s in &states {
231 println!(
232 "{:<20} {:<25} {:<10} {:<18} {:<6} {}",
233 s.agent_name,
234 s.project_name,
235 s.target,
236 s.template_name,
237 s.services.len(),
238 s.deployed_at.format("%Y-%m-%d %H:%M"),
239 );
240 }
241 }
242 }
243
244 Ok(())
245}
246
247pub fn create_backend(target: &str) -> Result<Box<dyn DeployBackend>> {
249 match target {
250 "railway" => {
251 let token = std::env::var("RAILWAY_API_TOKEN").context(
252 "RAILWAY_API_TOKEN environment variable is required for Railway deploys",
253 )?;
254 Ok(Box::new(railway::RailwayBackend::new(token)))
255 }
256 "flyio" | "fly" => {
257 anyhow::bail!(
258 "Fly.io backend is planned but not yet implemented. Use --target railway."
259 );
260 }
261 "ecs" | "aws" => {
262 anyhow::bail!(
263 "AWS ECS backend is planned but not yet implemented. Use --target railway."
264 );
265 }
266 other => {
267 anyhow::bail!(
268 "Unknown deploy target: '{other}'. Supported: railway, flyio (planned), ecs (planned)."
269 );
270 }
271 }
272}