horto_os_ui_shared/remote/runner/
apply.rs1use super::super::arch::box_arch_from_uname;
4use super::super::bins::{ensure_local_bins, LocalBins};
5use super::super::ecosystem::{
6 remote_enable_ecosystem_script, EcosystemInstallChoice, API_TOKEN_DROP_BASENAME,
7};
8use super::super::process::{ProcessRunner, StdioMode};
9use super::super::ssh::SshSession;
10use super::super::transfer::transfer_files;
11use super::options::{
12 build_remote_command_at, merge_command_log, remote_agent_bin, remote_progress,
13 remote_run_banner_detail, session_from, shell_quote, RemoteOptions, RemoteRunFlags,
14 RemoteRunRequest,
15};
16use super::probe::{probe_remote_cli, RemoteCliProbe};
17use super::reboot::offer_remote_reboot;
18use super::token::parse_api_token_drop;
19use crate::error::Result;
20use std::path::Path;
21
22pub fn upload_remote_cli(
23 runner: &dyn ProcessRunner,
24 session: &SshSession,
25 opts: &RemoteOptions,
26 bins: &LocalBins,
27) -> Result<String> {
28 session.exec(
29 runner,
30 &format!("mkdir -p {}", shell_quote(&opts.remote_agent_dir)),
31 StdioMode::Capture,
32 )?;
33 let remote_bin = remote_agent_bin(opts);
34 remote_progress(&opts.host, "upload CLI agent (SCP; may ask password)");
35 session.scp_to(runner, &bins.cli, &remote_bin)?;
36 session.exec(
37 runner,
38 &format!("chmod +x {}", shell_quote(&remote_bin)),
39 StdioMode::Capture,
40 )?;
41 Ok(remote_bin)
42}
43
44pub fn remote_ensure_ssh_key(runner: &dyn ProcessRunner, opts: &RemoteOptions) -> Result<()> {
50 let session = session_from(opts)?;
51 session.install_ssh_key(runner)
52}
53
54pub fn remote_upload_cli(
60 runner: &dyn ProcessRunner,
61 opts: &RemoteOptions,
62) -> Result<RemoteCliProbe> {
63 let session = session_from(opts)?;
64 remote_progress(&opts.host, "probe arch (SSH; may ask password)");
65 let out = session.exec(runner, "uname -m", StdioMode::Capture)?;
66 let arch = box_arch_from_uname(&out.stdout)?;
67 let bins = ensure_local_bins(
68 runner,
69 &opts.release_tag,
70 &opts.version,
71 &opts.github_repo,
72 arch,
73 opts.bin_dir.as_deref(),
74 &opts.cache_root,
75 )?;
76 upload_remote_cli(runner, &session, opts, &bins)?;
77 maybe_install_key(runner, &session, opts)?;
78 probe_remote_cli(runner, &session, opts)
79}
80
81pub fn ensure_remote_cli(
83 runner: &dyn ProcessRunner,
84 session: &SshSession,
85 opts: &RemoteOptions,
86 bins: &LocalBins,
87) -> Result<String> {
88 let probe = probe_remote_cli(runner, session, opts)?;
89 if probe.current {
90 if let Some(path) = probe.path {
91 remote_progress(&opts.host, "CLI on box already current; skip upload");
92 return Ok(path);
93 }
94 }
95 upload_remote_cli(runner, session, opts, bins)
96}
97
98pub fn maybe_install_key(
99 runner: &dyn ProcessRunner,
100 session: &SshSession,
101 opts: &RemoteOptions,
102) -> Result<()> {
103 if opts.install_ssh_key {
104 session.install_ssh_key(runner)?;
105 }
106 Ok(())
107}
108
109pub fn prepare_remote_agent(
111 runner: &dyn ProcessRunner,
112 opts: &RemoteOptions,
113) -> Result<(SshSession, LocalBins, String)> {
114 let session = session_from(opts)?;
115 remote_progress(&opts.host, "probe arch (SSH; may ask password)");
116 let out = session.exec(runner, "uname -m", StdioMode::Capture)?;
117 let arch = box_arch_from_uname(&out.stdout)?;
118 let bins = ensure_local_bins(
119 runner,
120 &opts.release_tag,
121 &opts.version,
122 &opts.github_repo,
123 arch,
124 opts.bin_dir.as_deref(),
125 &opts.cache_root,
126 )?;
127 let remote_bin = ensure_remote_cli(runner, &session, opts, &bins)?;
128 maybe_install_key(runner, &session, opts)?;
129 Ok((session, bins, remote_bin))
130}
131
132#[derive(Debug, Clone, Default, PartialEq, Eq)]
134pub struct RemoteRunOutcome {
135 pub log: String,
137 pub api_token: Option<String>,
139}
140
141pub fn remote_run_cli(
147 runner: &dyn ProcessRunner,
148 req: &RemoteRunRequest,
149) -> Result<RemoteRunOutcome> {
150 let opts = &req.options;
151 let (session, bins, remote_bin) = prepare_remote_agent(runner, opts)?;
152
153 let remote_cmd = build_remote_command_at(&remote_bin, &req.cli_args, req.use_sudo);
154 remote_progress(
155 &opts.host,
156 &remote_run_banner_detail(&remote_cmd, req.use_sudo),
157 );
158 let stdio = if req.capture_output {
159 StdioMode::Capture
160 } else {
161 StdioMode::Inherit
163 };
164 let out = session.exec(runner, &remote_cmd, stdio)?;
165 let log = merge_command_log(&out, &remote_cmd);
166
167 let api_token = if req.install_payload_on_success && req.ecosystem.any() {
168 remote_install_payload(runner, opts, &bins, req.ecosystem)?
169 } else {
170 None
171 };
172 if req.offer_reboot_on_success {
173 offer_remote_reboot(runner, &session)?;
174 }
175 Ok(RemoteRunOutcome { log, api_token })
176}
177
178pub fn remote_setup_run(
184 runner: &dyn ProcessRunner,
185 opts: RemoteOptions,
186 apply: bool,
187 full: bool,
188 skip_piper: bool,
189 ecosystem: EcosystemInstallChoice,
190) -> Result<RemoteRunOutcome> {
191 let mut cli_args = Vec::new();
192 if apply {
193 cli_args.push("--apply".into());
194 }
195 if skip_piper {
196 cli_args.push("--skip-piper".into());
197 }
198 cli_args.push("setup".into());
199 cli_args.push("run".into());
200 if full {
201 cli_args.push("--full".into());
202 } else {
203 cli_args.push("--minimal".into());
204 }
205 remote_run_cli(
206 runner,
207 &RemoteRunRequest {
208 options: opts,
209 cli_args,
210 ecosystem,
211 flags: RemoteRunFlags {
212 use_sudo: apply,
213 install_payload_on_success: apply && ecosystem.any(),
214 offer_reboot_on_success: apply,
215 capture_output: false,
216 },
217 },
218 )
219}
220
221pub fn remote_install_payload(
230 runner: &dyn ProcessRunner,
231 opts: &RemoteOptions,
232 bins: &LocalBins,
233 choice: EcosystemInstallChoice,
234) -> Result<Option<String>> {
235 let choice = EcosystemInstallChoice {
236 status_api: choice.status_api,
237 mcp: choice.mcp && bins.mcp.is_some(),
238 };
239 if !choice.any() {
240 return Ok(None);
241 }
242 let session = session_from(opts)?;
243 let staging = format!("{}/payload", opts.remote_agent_dir.trim_end_matches('/'));
244 remote_progress(
245 &opts.host,
246 "install box payload (SCP/SSH; may ask password)",
247 );
248 session.exec(
249 runner,
250 &format!("mkdir -p {}", shell_quote(&staging)),
251 StdioMode::Capture,
252 )?;
253
254 let mut locals: Vec<&Path> = vec![&bins.cli, &bins.tui, &bins.status_api];
255 if let Some(ref mcp) = bins.mcp {
256 locals.push(mcp.as_path());
257 }
258 transfer_files(runner, &session, &locals, &staging)?;
259
260 let install = opts.install_dir.trim_end_matches('/');
261 remote_progress(
262 &opts.host,
263 "enable selected ecosystem services on box (SSH + sudo; may ask password)",
264 );
265 let enable = remote_enable_ecosystem_script(&staging, install, choice);
266 session.exec(runner, &enable, StdioMode::Inherit)?;
267
268 let mut api_token = None;
269 if choice.status_api {
270 let drop_path = format!("$HOME/{API_TOKEN_DROP_BASENAME}");
271 let cat_out = session.exec(
272 runner,
273 &format!("cat {drop_path} 2>/dev/null || true"),
274 StdioMode::Capture,
275 )?;
276 api_token = parse_api_token_drop(&cat_out.stdout);
277 let _ = session.exec(runner, &format!("rm -f {drop_path}"), StdioMode::Capture);
278 }
279
280 Ok(api_token)
281}