1use crate::download::{download_cask_artifact, ArtifactType};
4use crate::error::{Error, Result};
5use crate::state::{now_timestamp, InstalledArtifact, InstalledCask, InstalledCasks};
6use crate::detect_artifact_type;
7use stout_index::Cask;
8use std::path::{Path, PathBuf};
9use tracing::{debug, info, warn};
10
11#[derive(Debug, Clone, Default)]
13pub struct CaskInstallOptions {
14 pub force: bool,
16 pub no_verify: bool,
18 pub appdir: Option<PathBuf>,
20 pub dry_run: bool,
22}
23
24pub async fn install_cask(
26 cask: &Cask,
27 cache_dir: &Path,
28 state_path: &Path,
29 options: &CaskInstallOptions,
30) -> Result<PathBuf> {
31 let token = &cask.token;
32
33 let mut installed_casks = InstalledCasks::load(state_path)?;
35 if installed_casks.is_installed(token) && !options.force {
36 return Err(Error::InstallFailed(format!(
37 "{} is already installed. Use --force to reinstall.",
38 token
39 )));
40 }
41
42 let url = cask.download_url().ok_or_else(|| {
44 Error::InstallFailed(format!("No download URL for cask {}", token))
45 })?;
46
47 let artifact_type = detect_artifact_type(url);
49
50 let sha256 = if options.no_verify {
52 None
53 } else {
54 cask.sha256.as_str()
55 };
56
57 info!("Downloading {}...", token);
58
59 if options.no_verify {
61 warn!("Checksum verification is disabled - this is a security risk");
62 }
63
64 if options.dry_run {
65 info!("[dry-run] Would download {} from {}", token, url);
66 info!("[dry-run] Would install to /Applications");
67 return Ok(PathBuf::from("/Applications"));
68 }
69
70 let artifact_path = download_cask_artifact(url, cache_dir, token, sha256, artifact_type).await?;
72
73 let install_result = install_artifact(cask, &artifact_path, artifact_type, options).await?;
75
76 let installed = InstalledCask {
78 version: cask.version.clone(),
79 installed_at: now_timestamp(),
80 artifact_path: install_result.clone(),
81 auto_updates: cask.auto_updates,
82 artifacts: vec![], };
84
85 installed_casks.add(token, installed);
86 installed_casks.save(state_path)?;
87
88 info!("Installed {} to {}", token, install_result.display());
89 Ok(install_result)
90}
91
92async fn install_artifact(
94 cask: &Cask,
95 artifact_path: &Path,
96 artifact_type: ArtifactType,
97 options: &CaskInstallOptions,
98) -> Result<PathBuf> {
99 #[cfg(target_os = "macos")]
100 {
101 crate::macos::install_artifact(cask, artifact_path, artifact_type, options).await
102 }
103
104 #[cfg(target_os = "linux")]
105 {
106 crate::linux::install_artifact(cask, artifact_path, artifact_type, options).await
107 }
108
109 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
110 {
111 Err(Error::UnsupportedPlatform(
112 std::env::consts::OS.to_string(),
113 ))
114 }
115}
116
117pub async fn uninstall_cask(
119 token: &str,
120 state_path: &Path,
121 zap: bool,
122) -> Result<()> {
123 let mut installed_casks = InstalledCasks::load(state_path)?;
124
125 let installed = installed_casks.get(token).ok_or_else(|| {
126 Error::UninstallFailed(format!("{} is not installed", token))
127 })?;
128
129 let artifact_path = installed.artifact_path.clone();
130
131 if artifact_path.exists() {
133 if artifact_path.is_dir() {
134 info!("Removing {}", artifact_path.display());
135 std::fs::remove_dir_all(&artifact_path)
136 .map_err(|e| Error::UninstallFailed(format!("Failed to remove {}: {}", artifact_path.display(), e)))?;
137 } else if artifact_path.is_file() {
138 info!("Removing {}", artifact_path.display());
139 std::fs::remove_file(&artifact_path)
140 .map_err(|e| Error::UninstallFailed(format!("Failed to remove {}: {}", artifact_path.display(), e)))?;
141 }
142 } else {
143 warn!("Artifact path {} does not exist", artifact_path.display());
144 }
145
146 installed_casks.remove(token);
148 installed_casks.save(state_path)?;
149
150 if zap {
151 info!("Zap requested - note: full zap (preferences, caches, support files) not yet implemented");
152 }
155
156 info!("Uninstalled {}", token);
157 Ok(())
158}