Skip to main content

heyo_sdk/
commands.rs

1use reqwest::Method;
2use serde::{Deserialize, Serialize};
3
4use crate::client::{HeyoClient, RequestOptions};
5use crate::errors::HeyoError;
6use crate::types::{CommandResult, CommandRunOptions};
7
8/// Command-execution surface, obtained via [`crate::Sandbox::commands`].
9#[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    /// `POST /sandbox/:id/exec`. Runs `command` with `sh -c`.
42    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    // Minimal path-segment encoding: percent-escape anything that isn't an
73    // unreserved char. Sandbox IDs are `dep-…` so this is mostly a no-op.
74    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}