horto_os_ui_mcp/
remote_ops.rs1use anyhow::{bail, Context, Result};
4use horto_os_ui_shared::{
5 remote_probe_arch, remote_run_cli, remote_setup_run, EcosystemInstallChoice, RemoteOptions,
6 RemoteOptionsInput, RemoteRunFlags, RemoteRunRequest, SystemProcessRunner,
7};
8
9use crate::config::McpSettings;
10
11fn remote_options(settings: &McpSettings) -> Result<RemoteOptions> {
12 let host = settings
13 .remote_host
14 .clone()
15 .context("HORTO_REMOTE_HOST required for remote tools (OpenSSH Host alias or user@host)")?;
16 Ok(RemoteOptions::from_input(RemoteOptionsInput {
17 host,
18 install_ssh_key: settings.install_ssh_key,
19 bin_dir: settings.bin_dir.clone(),
20 release_tag: settings.release_tag.clone(),
21 force_askpass: true,
22 }))
23}
24
25fn run_cli(settings: &McpSettings, args: &[&str], use_sudo: bool) -> Result<String> {
26 let opts = remote_options(settings)?;
27 let cli_args: Vec<String> = args.iter().map(|s| (*s).to_owned()).collect();
28 remote_run_cli(
29 &SystemProcessRunner,
30 &RemoteRunRequest {
31 options: opts,
32 cli_args,
33 flags: RemoteRunFlags {
34 use_sudo,
35 install_payload_on_success: false,
36 offer_reboot_on_success: false,
37 capture_output: false,
38 },
39 ecosystem: EcosystemInstallChoice::none(),
40 },
41 )
42 .map(|o| o.log)
43 .map_err(|e| anyhow::anyhow!("{e}"))
44}
45
46pub fn remote_probe(settings: &McpSettings) -> Result<String> {
52 let opts = remote_options(settings)?;
53 remote_probe_arch(&SystemProcessRunner, &opts)
54 .map(|a| a.cache_label().to_owned())
55 .map_err(|e| anyhow::anyhow!("{e}"))
56}
57
58pub fn setup_status(settings: &McpSettings, apply: bool, full: bool) -> Result<String> {
64 let mut args = Vec::new();
65 if apply {
66 args.push("--apply");
67 }
68 args.push("setup");
69 args.push("status");
70 if full {
71 args.push("--full");
72 } else {
73 args.push("--minimal");
74 }
75 run_cli(settings, &args, false)
76}
77
78pub fn setup_run(
84 settings: &McpSettings,
85 apply: bool,
86 full: bool,
87 skip_piper: bool,
88) -> Result<String> {
89 let opts = remote_options(settings)?;
90 remote_setup_run(
91 &SystemProcessRunner,
92 opts,
93 apply,
94 full,
95 skip_piper,
96 EcosystemInstallChoice::none(),
97 )
98 .map(|o| o.log)
99 .map_err(|e| anyhow::anyhow!("{e}"))
100}
101
102pub fn setup_step(
108 settings: &McpSettings,
109 step_id: &str,
110 apply: bool,
111 full: bool,
112) -> Result<String> {
113 let kind = if full { "full" } else { "minimal" };
114 let mut args = Vec::new();
115 if apply {
116 args.push("--apply");
117 }
118 args.push("setup");
119 args.push("step");
120 args.push(step_id);
121 args.push(kind);
122 run_cli(settings, &args, apply)
123}
124
125pub fn doctor(settings: &McpSettings) -> Result<String> {
131 run_cli(settings, &["doctor"], false)
132}
133
134pub fn docker_status(settings: &McpSettings) -> Result<String> {
140 run_cli(settings, &["docker", "status"], false)
141}
142
143pub fn docker_rebuild(settings: &McpSettings, confirm: &str) -> Result<String> {
149 if confirm != "docker-rebuild" {
150 bail!("confirm must be exactly `docker-rebuild`");
151 }
152 run_cli(settings, &["docker", "rebuild"], true)
153}
154
155pub fn backup_list(settings: &McpSettings) -> Result<String> {
161 run_cli(settings, &["backup", "list"], false)
162}
163
164pub fn backup_disk_status(settings: &McpSettings) -> Result<String> {
170 run_cli(settings, &["backup", "disk-status"], false)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn docker_rebuild_rejects_bad_confirm() {
179 let settings = McpSettings {
180 mode: crate::config::McpMode::Pc,
181 status_api_url: "http://127.0.0.1:8787".into(),
182 api_token: None,
183 mcp_token: None,
184 remote_host: Some("horto".into()),
185 release_tag: None,
186 bin_dir: None,
187 install_ssh_key: false,
188 };
189 let err = docker_rebuild(&settings, "nope").expect_err("confirm");
190 assert!(err.to_string().contains("docker-rebuild"));
191 }
192
193 #[test]
194 fn remote_options_require_host() {
195 let settings = McpSettings {
196 mode: crate::config::McpMode::Pc,
197 status_api_url: "http://127.0.0.1:8787".into(),
198 api_token: None,
199 mcp_token: None,
200 remote_host: None,
201 release_tag: None,
202 bin_dir: None,
203 install_ssh_key: false,
204 };
205 assert!(remote_options(&settings).is_err());
206 }
207}