1use std::{collections::HashMap, path::Path};
2
3use anyhow::{anyhow, Result};
4use inquire::{Confirm, Select, Text};
5
6use crate::{
7 AppContext,
8 templates::generator::{to_camel_case, to_kebab_case, to_pascal_case, to_snake_case},
9};
10
11use super::{
12 cache::{get_cached_addon, is_addon_installed},
13 detect::detect_variant,
14 install::{install_addon, read_cached_manifest},
15 lock::{LockEntry, LockFile},
16 manifest::{AddonManifest, InputDef, InputType},
17 steps::{
18 Rollback,
19 append::execute_append,
20 copy::execute_copy,
21 create::execute_create,
22 delete::execute_delete,
23 inject::execute_inject,
24 move_step::execute_move,
25 rename::execute_rename,
26 replace::execute_replace,
27 },
28};
29use crate::addons::manifest::Step;
30
31pub async fn run_addon_command(
32 ctx: &AppContext,
33 addon_id: &str,
34 command_name: &str,
35 project_root: &Path,
36) -> Result<()> {
37 let manifest: AddonManifest = if get_cached_addon(&ctx.paths.addons, addon_id)?.is_some() {
39 read_cached_manifest(&ctx.paths.addons, addon_id)?
40 } else {
41 install_addon(ctx, addon_id).await?
42 };
43
44 let mut lock = LockFile::load(project_root)?;
46
47 for dep_id in &manifest.requires {
52 if !is_addon_installed(&ctx.paths.addons, dep_id)? {
53 return Err(anyhow!(
54 "Addon '{}' requires '{}' to be installed first. Run: oxide addon install {}",
55 addon_id,
56 dep_id,
57 dep_id
58 ));
59 }
60 }
61
62 let mut input_values: HashMap<String, String> = HashMap::new();
64 collect_inputs(&manifest.inputs, &mut input_values)?;
65
66 let mut tera_ctx = tera::Context::new();
68 insert_with_derived(&mut tera_ctx, &input_values);
69
70 let detected_id = detect_variant(&manifest.detect, project_root);
72
73 let variant = manifest
75 .variants
76 .iter()
77 .find(|v| v.when.as_deref() == detected_id.as_deref())
78 .or_else(|| manifest.variants.iter().find(|v| v.when.is_none()))
79 .ok_or_else(|| anyhow!("No matching variant found for addon '{}'", addon_id))?;
80
81 let command = variant
83 .commands
84 .iter()
85 .find(|c| c.name == command_name)
86 .ok_or_else(|| anyhow!("Command '{}' not found in addon '{}'", command_name, addon_id))?;
87
88 if command.once && lock.is_command_executed(addon_id, command_name) {
90 println!("Command '{}' has already been executed, skipping.", command_name);
91 return Ok(());
92 }
93
94 for req_cmd in &command.requires_commands {
96 if !lock.is_command_executed(addon_id, req_cmd) {
97 return Err(anyhow!(
98 "Command '{}' requires '{}' to be run first. Run: oxide addon run {} {} {}",
99 command_name,
100 req_cmd,
101 addon_id,
102 req_cmd,
103 project_root.display()
104 ));
105 }
106 }
107
108 let mut cmd_input_values: HashMap<String, String> = HashMap::new();
110 collect_inputs(&command.inputs, &mut cmd_input_values)?;
111 insert_with_derived(&mut tera_ctx, &cmd_input_values);
112
113 let addon_dir = ctx.paths.addons.join(addon_id);
115 let mut completed_rollbacks: Vec<Rollback> = Vec::new();
116
117 for (idx, step) in command.steps.iter().enumerate() {
118 let result = match step {
119 Step::Copy(s) => execute_copy(s, &addon_dir, project_root),
120 Step::Create(s) => execute_create(s, project_root, &tera_ctx),
121 Step::Inject(s) => execute_inject(s, project_root, &tera_ctx),
122 Step::Replace(s) => execute_replace(s, project_root, &tera_ctx),
123 Step::Append(s) => execute_append(s, project_root, &tera_ctx),
124 Step::Delete(s) => execute_delete(s, project_root),
125 Step::Rename(s) => execute_rename(s, project_root, &tera_ctx),
126 Step::Move(s) => execute_move(s, project_root, &tera_ctx),
127 };
128
129 match result {
130 Ok(rollbacks) => completed_rollbacks.extend(rollbacks),
131 Err(err) => {
132 eprintln!("Step {} failed: {}", idx + 1, err);
133 let choice = Select::new(
134 "How would you like to proceed?",
135 vec!["Keep changes made so far", "Rollback all changes"],
136 )
137 .prompt()?;
138
139 if choice == "Rollback all changes" {
140 for rollback in completed_rollbacks.into_iter().rev() {
141 let _ = apply_rollback(rollback);
142 }
143 }
144
145 return Err(anyhow!("Addon command failed at step {}: {}", idx + 1, err));
146 }
147 }
148 }
149
150 lock.mark_command_executed(addon_id, command_name);
152 let variant_id = detected_id.unwrap_or_else(|| "universal".to_string());
153 lock.upsert_entry(LockEntry {
154 id: addon_id.to_string(),
155 version: manifest.version.clone(),
156 variant: variant_id,
157 commands_executed: lock
158 .addons
159 .iter()
160 .find(|e| e.id == addon_id)
161 .map(|e| e.commands_executed.clone())
162 .unwrap_or_default(),
163 });
164 lock.save(project_root)?;
165
166 println!("✓ Command '{}' completed successfully.", command_name);
167 Ok(())
168}
169
170fn collect_inputs(inputs: &[InputDef], map: &mut HashMap<String, String>) -> Result<()> {
172 for input in inputs {
173 let value = match input.input_type {
174 InputType::Text => {
175 let mut prompt = Text::new(&input.description);
176 if let Some(ref default) = input.default {
177 prompt = prompt.with_default(default);
178 }
179 prompt.prompt()?
180 }
181 InputType::Boolean => {
182 let default = input.default.as_deref().map(|d| d == "true").unwrap_or(false);
183 Confirm::new(&input.description).with_default(default).prompt()?.to_string()
184 }
185 InputType::Select => {
186 Select::new(&input.description, input.options.clone()).prompt()?.to_string()
187 }
188 };
189 map.insert(input.name.clone(), value);
190 }
191 Ok(())
192}
193
194fn insert_with_derived(ctx: &mut tera::Context, map: &HashMap<String, String>) {
197 for (k, v) in map {
198 ctx.insert(k.as_str(), v);
199 ctx.insert(&format!("{k}_pascal"), &to_pascal_case(v));
200 ctx.insert(&format!("{k}_camel"), &to_camel_case(v));
201 ctx.insert(&format!("{k}_kebab"), &to_kebab_case(v));
202 ctx.insert(&format!("{k}_snake"), &to_snake_case(v));
203 }
204}
205
206fn apply_rollback(rollback: Rollback) -> Result<()> {
207 match rollback {
208 Rollback::DeleteCreatedFile { path } => {
209 let _ = std::fs::remove_file(path);
210 }
211 Rollback::RestoreFile { path, original } => {
212 std::fs::write(path, original)?;
213 }
214 Rollback::RenameFile { from, to } => {
215 std::fs::rename(from, to)?;
216 }
217 }
218 Ok(())
219}