1use reqwest::Method;
2use serde::{Deserialize, Serialize};
3
4use crate::client::{HeyoClient, RequestOptions};
5use crate::errors::HeyoError;
6use crate::types::{CommandResult, CommandRunOptions};
7
8#[derive(Clone)]
10pub struct Commands {
11 client: HeyoClient,
12 sandbox_id: String,
13}
14
15#[derive(Serialize)]
16struct ExecRequest<'a> {
17 command: &'a str,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 cwd: Option<&'a str>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 env: Option<&'a std::collections::HashMap<String, String>>,
22}
23
24#[derive(Deserialize, Default)]
25struct ExecResponse {
26 #[serde(default)]
27 stdout: Option<String>,
28 #[serde(default)]
29 stderr: Option<String>,
30 #[serde(default)]
31 output: Option<String>,
32 #[serde(default)]
33 exit_code: Option<i32>,
34}
35
36impl Commands {
37 pub(crate) fn new(client: HeyoClient, sandbox_id: String) -> Self {
38 Self { client, sandbox_id }
39 }
40
41 pub async fn run(
43 &self,
44 command: &str,
45 options: CommandRunOptions,
46 ) -> Result<CommandResult, HeyoError> {
47 let body = ExecRequest {
48 command,
49 cwd: options.cwd.as_deref(),
50 env: options.env.as_ref(),
51 };
52 let path = format!(
53 "/sandbox/{}/exec",
54 urlencoding(&self.sandbox_id)
55 );
56 let mut opts = RequestOptions::default();
57 opts.timeout = options.timeout;
58 let raw = self
59 .client
60 .request::<ExecResponse>(Method::POST, &path, Some(&body), opts)
61 .await?;
62 Ok(CommandResult {
63 stdout: raw.stdout.unwrap_or_default(),
64 stderr: raw.stderr.unwrap_or_default(),
65 output: raw.output.unwrap_or_default(),
66 exit_code: raw.exit_code.unwrap_or(0),
67 })
68 }
69}
70
71fn urlencoding(s: &str) -> String {
72 let mut out = String::with_capacity(s.len());
75 for b in s.bytes() {
76 match b {
77 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
78 out.push(b as char);
79 }
80 _ => out.push_str(&format!("%{:02X}", b)),
81 }
82 }
83 out
84}
85
86pub(crate) fn encode_path(s: &str) -> String {
87 urlencoding(s)
88}