use anyhow::{Context, Result, bail};
use chrono::Utc;
use crate::app::sources::{SourceInfo, is_wrap_source, parse_source, source_kind};
use crate::config::WarrenConfig;
use crate::install::{InstallerExecutor, InstallerRewriter};
use crate::instance::metadata::{InstallInfo, InstanceInfo, PathsInfo, SourceType};
use crate::instance::{self, InstanceLayout, InstanceMetadata, Launcher};
use crate::ui::progress;
use crate::ui::theme::Theme;
pub async fn execute(
config: &WarrenConfig,
theme: &Theme,
source: &str,
alias: &str,
yes: bool,
gui_override: Option<bool>,
) -> Result<()> {
instance::validate_alias(alias)?;
let layout = InstanceLayout::new(&config.paths.instances_dir, alias);
if layout.exists() {
if !yes {
use dialoguer::Confirm;
let overwrite = Confirm::new()
.with_prompt(format!("Instance '{}' already exists. Overwrite?", alias))
.default(false)
.interact()?;
if !overwrite {
theme.warn("Aborted.");
return Ok(());
}
}
Launcher::uninstall(&config.paths.bin_dir, alias)?;
crate::app::desktop::uninstall(alias).ok();
layout.destroy()?;
}
theme.header(&format!("digging {}", alias));
layout.create()?;
let source_info = parse_source(source);
if is_wrap_source(&source_info) {
return execute_wrap(
config,
theme,
&layout,
source,
&source_info,
alias,
gui_override,
)
.await;
}
let (installer_hash, source_type) = match &source_info {
SourceInfo::RemoteScript { url, .. } => {
let spinner = progress::spinner(&format!("Downloading installer from {}", url));
let original_path = layout.installers_dir().join("original.sh");
let hash = crate::install::download_installer(url, &original_path).await?;
spinner.finish_with_message("Downloaded installer");
(Some(hash), SourceType::RemoteScript)
}
SourceInfo::LocalScript { path } => {
let src_path = std::path::Path::new(path);
if !src_path.exists() {
layout.destroy()?;
bail!("local installer not found: {}", path);
}
let original_path = layout.installers_dir().join("original.sh");
let hash = crate::install::download::read_local_installer(src_path, &original_path)?;
(Some(hash), SourceType::LocalScript)
}
SourceInfo::Package { name } => {
theme.step("๐ฆ", &format!("Package source: {}", name));
if let Ok(found) = which::which(name) {
let dest = layout.bin_dir().join(name);
std::fs::copy(&found, &dest)
.with_context(|| format!("failed to copy {} to instance", name))?;
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(0o755))?;
(None, SourceType::Package)
} else {
layout.destroy()?;
bail!("package '{}' not found on PATH", name);
}
}
_ => {
layout.destroy()?;
bail!("unsupported source '{}'", source);
}
};
if matches!(
source_info,
SourceInfo::RemoteScript { .. } | SourceInfo::LocalScript { .. }
) {
let original_path = layout.installers_dir().join("original.sh");
let original_content =
std::fs::read_to_string(&original_path).context("failed to read original installer")?;
let rewriter = InstallerRewriter::new(&layout.root);
let result = rewriter.rewrite(&original_content);
theme.step(
"โ",
&format!(
"Rewriting {} path reference{}",
result.total_changes(),
if result.total_changes() == 1 { "" } else { "s" }
),
);
InstallerRewriter::validate(&result.content, &layout.root)?;
let rewritten_path = layout.installers_dir().join("rewritten.sh");
std::fs::write(&rewritten_path, &result.content)
.context("failed to write rewritten installer")?;
if !yes && (config.defaults.show_diff || result.has_changes()) {
use dialoguer::Confirm;
let show = Confirm::new()
.with_prompt("Show diff?")
.default(false)
.interact()?;
if show {
let diff =
crate::install::rewriter::generate_diff(&original_content, &result.content);
eprintln!("\n{}\n", diff);
}
}
let spinner = progress::spinner("Running installer...");
InstallerExecutor::execute(&rewritten_path, &layout).await?;
spinner.finish_with_message("Installer complete");
}
let binary_name = crate::install::detect::detect_binary(&layout.bin_dir())?
.unwrap_or_else(|| {
theme.warn("No executable found in instance bin/ โ the installer may have failed to place one.");
theme.warn(&format!(
"Launcher will point to '{}', which may not exist yet. Inspect with: warren inspect {}",
alias, alias
));
alias.to_string()
});
let version = crate::install::detect::detect_version(&layout, &binary_name);
let gui = gui_override.unwrap_or(false);
let launcher_content = Launcher::generate_file(alias, &layout, &binary_name, gui);
Launcher::write(&layout, &launcher_content)?;
let launcher_dest = Launcher::install(&layout, &config.paths.bin_dir, alias)?;
let desktop_file = if gui {
let path = crate::app::desktop::install(alias, &binary_name, &launcher_dest, None, false)?;
theme.success(&format!(
"Desktop entry: {}",
InstanceMetadata::display_path(&path.to_string_lossy())
));
Some(path.to_string_lossy().to_string())
} else {
None
};
let now = Utc::now();
let shell = crate::shell::detect_shell();
let metadata = InstanceMetadata {
instance: InstanceInfo {
alias: alias.to_string(),
app_name: binary_name.clone(),
version,
shell: shell.to_string(),
created_at: now,
updated_at: now,
gui,
icon: None,
},
install: InstallInfo {
source: source.to_string(),
source_type,
installer_hash,
launch_command: Vec::new(),
},
paths: PathsInfo {
root: layout.root.to_string_lossy().to_string(),
bin: layout.bin_dir().to_string_lossy().to_string(),
launcher: launcher_dest.to_string_lossy().to_string(),
desktop_file,
},
};
metadata.save(&layout.metadata_path())?;
theme.blank();
theme.success(&format!("Installed: {}", alias));
theme.success(&format!(
"Launcher: {}",
InstanceMetadata::display_path(&launcher_dest.to_string_lossy())
));
theme.success(&format!(
"Binary: {}",
InstanceMetadata::display_path(&layout.bin_dir().join(&binary_name).to_string_lossy())
));
theme.blank();
theme.dim(&format!("Run it: {}", alias));
theme.dim(&format!(" warren run {}", alias));
theme.blank();
Ok(())
}
async fn execute_wrap(
config: &WarrenConfig,
theme: &Theme,
layout: &InstanceLayout,
source: &str,
source_info: &SourceInfo,
alias: &str,
gui_override: Option<bool>,
) -> Result<()> {
theme.step(
"๐",
&format!("Wrapping {} source: {}", source_kind(source_info), source),
);
let spec = crate::app::resolve::resolve(source_info, gui_override).inspect_err(|_| {
layout.destroy().ok();
})?;
theme.step(
"๐งท",
&format!("Found: {} โ {}", spec.display_name, spec.command.join(" ")),
);
let source_type = match source_info {
SourceInfo::Flatpak { .. } => SourceType::Flatpak,
SourceInfo::Snap { .. } => SourceType::Snap,
SourceInfo::System { .. } => SourceType::System,
SourceInfo::App { .. } => SourceType::App,
SourceInfo::Desktop { .. } => SourceType::Desktop,
_ => SourceType::Package,
};
let launcher_content = Launcher::generate_wrapped(alias, layout, &spec);
Launcher::write(layout, &launcher_content)?;
let launcher_dest = Launcher::install(layout, &config.paths.bin_dir, alias)?;
let desktop_file = if spec.gui {
let path = crate::app::desktop::install(
alias,
&spec.display_name,
&launcher_dest,
spec.icon.as_deref(),
false,
)?;
theme.success(&format!(
"Desktop entry: {}",
InstanceMetadata::display_path(&path.to_string_lossy())
));
Some(path.to_string_lossy().to_string())
} else {
None
};
let now = Utc::now();
let shell = crate::shell::detect_shell();
let metadata = InstanceMetadata {
instance: InstanceInfo {
alias: alias.to_string(),
app_name: spec.display_name.clone(),
version: None,
shell: shell.to_string(),
created_at: now,
updated_at: now,
gui: spec.gui,
icon: spec.icon.clone(),
},
install: InstallInfo {
source: source.to_string(),
source_type,
installer_hash: None,
launch_command: spec.command.clone(),
},
paths: PathsInfo {
root: layout.root.to_string_lossy().to_string(),
bin: layout.bin_dir().to_string_lossy().to_string(),
launcher: launcher_dest.to_string_lossy().to_string(),
desktop_file,
},
};
metadata.save(&layout.metadata_path())?;
theme.blank();
theme.success(&format!(
"Wrapped: {} ({} account #1 of many)",
alias, spec.display_name
));
theme.success(&format!(
"Launcher: {}",
InstanceMetadata::display_path(&launcher_dest.to_string_lossy())
));
theme.blank();
if spec.gui {
theme.dim(&format!(
"Run it from your app grid, or with: warren run {}",
alias
));
theme.dim("Tip: each alias keeps its own login โ dig again with a");
theme.dim(&format!(
"different --as name for account #2: warren dig {} --as <other>",
source
));
} else {
theme.dim(&format!("Run it: {}", alias));
theme.dim(&format!(" warren run {}", alias));
}
theme.blank();
Ok(())
}