···11+# Used by "mix format"
22+[
33+ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
44+]
+24
.gitignore
···11+# The directory Mix will write compiled artifacts to.
22+/_build/
33+44+# If you run "mix test --cover", coverage assets end up here.
55+/cover/
66+77+# The directory Mix downloads your dependencies sources to.
88+/deps/
99+1010+# Where third-party dependencies like ExDoc output generated docs.
1111+/doc/
1212+1313+# Temporary files, for example, from tests.
1414+/tmp/
1515+1616+# If the VM crashes, it generates a dump, let's ignore it too.
1717+erl_crash.dump
1818+1919+# Also ignore archive artifacts (built via "mix archive.build").
2020+*.ez
2121+2222+# Ignore package tarball (built via "mix hex.build").
2323+pocketenv-*.tar
2424+
+312
README.md
···11+# pocketenv-elixir
22+33+Elixir SDK for the [Pocketenv](https://pocketenv.io) sandbox platform.
44+55+Pocketenv lets you spin up isolated cloud sandbox environments on demand.
66+This library wraps the Pocketenv XRPC API so you can manage sandboxes
77+directly from your Elixir applications.
88+99+---
1010+1111+## Installation
1212+1313+Add `pocketenv` to your list of dependencies in `mix.exs`:
1414+1515+```elixir
1616+def deps do
1717+ [
1818+ {:pocketenv, "~> 0.1"}
1919+ ]
2020+end
2121+```
2222+2323+```sh
2424+mix deps.get
2525+```
2626+2727+---
2828+2929+## Configuration
3030+3131+### `config/config.exs`
3232+3333+```elixir
3434+import Config
3535+3636+config :pocketenv,
3737+ token: "your-pocketenv-token",
3838+ api_url: "https://api.pocketenv.io" # optional — this is the default
3939+```
4040+4141+### Environment variables
4242+4343+```sh
4444+export POCKETENV_TOKEN="your-pocketenv-token"
4545+export POCKETENV_API_URL="https://api.pocketenv.io" # optional
4646+```
4747+4848+Application config takes precedence over environment variables.
4949+5050+---
5151+5252+## Quick start
5353+5454+`Pocketenv` is the entry point. It returns `%Sandbox{}` structs that you
5555+pipe operations on:
5656+5757+```elixir
5858+{:ok, sandbox} =
5959+ Pocketenv.create_sandbox("my-sandbox")
6060+ |> Sandbox.start()
6161+ |> Sandbox.wait_until_running()
6262+6363+{:ok, result} = sandbox |> Sandbox.exec("echo", ["hello"])
6464+IO.puts(result.stdout) # => "hello"
6565+6666+{:ok, url} = sandbox |> Sandbox.expose(3000)
6767+IO.puts(url) # => "https://3000-my-sandbox.sbx.pocketenv.io"
6868+6969+{:ok, vscode_url} = sandbox |> Sandbox.vscode()
7070+7171+sandbox
7272+|> Sandbox.stop()
7373+|> Sandbox.delete()
7474+```
7575+7676+Every `Sandbox` function accepts either a bare `%Sandbox{}` **or** an
7777+`{:ok, %Sandbox{}}` tuple as its first argument, so you can pipe from any
7878+previous step without manually unwrapping.
7979+8080+---
8181+8282+## API reference
8383+8484+All functions return `{:ok, result}` on success and `{:error, reason}` on
8585+failure. Every function accepts an optional `:token` keyword argument to
8686+override the globally configured token for that single call.
8787+8888+---
8989+9090+### `Pocketenv` — entry point
9191+9292+#### Sandboxes
9393+9494+| Function | Returns | Description |
9595+|---|---|---|
9696+| `Pocketenv.create_sandbox(name, opts)` | `{:ok, %Sandbox{}}` | Create a new sandbox |
9797+| `Pocketenv.get_sandbox(id, opts)` | `{:ok, %Sandbox{} \| nil}` | Fetch a sandbox by id or name |
9898+| `Pocketenv.list_sandboxes(opts)` | `{:ok, {[%Sandbox{}], total}}` | List the public sandbox catalog |
9999+| `Pocketenv.list_sandboxes_by_actor(did, opts)` | `{:ok, {[%Sandbox{}], total}}` | List all sandboxes for a user |
100100+101101+##### `create_sandbox/2` options
102102+103103+| Option | Type | Default | Description |
104104+|---|---|---|---|
105105+| `:base` | `string` | official `openclaw` image | AT-URI of the base sandbox image |
106106+| `:provider` | `string` | `"cloudflare"` | `"cloudflare"`, `"daytona"`, `"deno"`, `"vercel"`, or `"sprites"` |
107107+| `:repo` | `string` | `nil` | GitHub repo URL to clone on start |
108108+| `:keep_alive` | `boolean` | `nil` | Keep the sandbox alive after the session ends |
109109+| `:token` | `string` | global config | Bearer token override |
110110+111111+##### `list_sandboxes/1` and `list_sandboxes_by_actor/2` options
112112+113113+| Option | Type | Default | Description |
114114+|---|---|---|---|
115115+| `:limit` | `integer` | `30` | Max results |
116116+| `:offset` | `integer` | `0` | Pagination offset |
117117+| `:token` | `string` | global config | Bearer token override |
118118+119119+#### Actor / profile
120120+121121+| Function | Returns | Description |
122122+|---|---|---|
123123+| `Pocketenv.me(opts)` | `{:ok, %Profile{}}` | Fetch the authenticated user's profile |
124124+| `Pocketenv.get_profile(did, opts)` | `{:ok, %Profile{}}` | Fetch any user's profile by DID or handle |
125125+126126+```elixir
127127+{:ok, me} = Pocketenv.me()
128128+IO.puts("Logged in as @#{me.handle}")
129129+130130+{:ok, profile} = Pocketenv.get_profile("alice.bsky.social")
131131+```
132132+133133+---
134134+135135+### `Sandbox` — operations on a sandbox
136136+137137+All functions take a `%Sandbox{}` or `{:ok, %Sandbox{}}` as their first
138138+argument.
139139+140140+#### Lifecycle
141141+142142+| Function | Returns | Description |
143143+|---|---|---|
144144+| `Sandbox.start(sandbox, opts)` | `{:ok, %Sandbox{}}` | Start the sandbox, re-fetches state |
145145+| `Sandbox.stop(sandbox, opts)` | `{:ok, %Sandbox{}}` | Stop the sandbox, re-fetches state |
146146+| `Sandbox.delete(sandbox, opts)` | `{:ok, %Sandbox{}}` | Delete the sandbox permanently |
147147+| `Sandbox.wait_until_running(sandbox, opts)` | `{:ok, %Sandbox{}}` | Poll until status is `:running` |
148148+149149+`start/2` and `stop/2` re-fetch the sandbox after the API call so the
150150+returned struct always has the latest status. `delete/2` returns the last
151151+known state.
152152+153153+##### `wait_until_running/2` options
154154+155155+| Option | Type | Default | Description |
156156+|---|---|---|---|
157157+| `:timeout_ms` | `integer` | `60_000` | Total wait time in ms |
158158+| `:interval_ms` | `integer` | `2_000` | Polling interval in ms |
159159+| `:token` | `string` | global config | Bearer token override |
160160+161161+#### Commands
162162+163163+```elixir
164164+{:ok, result} = sandbox |> Sandbox.exec("mix", ["test", "--trace"])
165165+166166+IO.puts(result.stdout)
167167+IO.puts(result.stderr)
168168+IO.inspect(result.exit_code)
169169+```
170170+171171+| Function | Returns | Description |
172172+|---|---|---|
173173+| `Sandbox.exec(sandbox, cmd, args \\ [], opts)` | `{:ok, %ExecResult{}}` | Run a shell command inside the sandbox |
174174+175175+#### Ports
176176+177177+```elixir
178178+{:ok, url} = sandbox |> Sandbox.expose(4000, description: "Phoenix")
179179+{:ok, ports} = sandbox |> Sandbox.list_ports()
180180+{:ok, _} = sandbox |> Sandbox.unexpose(4000)
181181+```
182182+183183+| Function | Returns | Description |
184184+|---|---|---|
185185+| `Sandbox.expose(sandbox, port, opts)` | `{:ok, url \| nil}` | Expose a port publicly |
186186+| `Sandbox.unexpose(sandbox, port, opts)` | `{:ok, %Sandbox{}}` | Remove an exposed port |
187187+| `Sandbox.list_ports(sandbox, opts)` | `{:ok, [%Port{}]}` | List all exposed ports |
188188+189189+#### VS Code
190190+191191+```elixir
192192+{:ok, url} = sandbox |> Sandbox.vscode()
193193+IO.puts("Open VS Code at: #{url}")
194194+```
195195+196196+| Function | Returns | Description |
197197+|---|---|---|
198198+| `Sandbox.vscode(sandbox, opts)` | `{:ok, url \| nil}` | Expose VS Code Server and return its URL |
199199+200200+If VS Code is already exposed the existing URL is returned immediately.
201201+202202+---
203203+204204+## Types
205205+206206+### `%Sandbox{}`
207207+208208+The central type of the SDK. Returned by `Pocketenv.create_sandbox/2`,
209209+`Pocketenv.get_sandbox/2`, and all `Sandbox.*` lifecycle functions.
210210+211211+```
212212+%Sandbox{
213213+ id: String.t() | nil,
214214+ name: String.t() | nil,
215215+ provider: String.t() | nil,
216216+ base_sandbox: String.t() | nil,
217217+ display_name: String.t() | nil,
218218+ uri: String.t() | nil,
219219+ description: String.t() | nil,
220220+ topics: [String.t()] | nil,
221221+ logo: String.t() | nil,
222222+ readme: String.t() | nil,
223223+ repo: String.t() | nil,
224224+ vcpus: integer() | nil,
225225+ memory: integer() | nil,
226226+ disk: integer() | nil,
227227+ installs: integer(),
228228+ status: :running | :stopped | :unknown,
229229+ started_at: String.t() | nil,
230230+ created_at: String.t() | nil,
231231+ owner: %Sandbox.Types.Profile{} | nil
232232+}
233233+```
234234+235235+### `%Sandbox.Types.ExecResult{}`
236236+237237+Returned by `Sandbox.exec/4`.
238238+239239+```
240240+%Sandbox.Types.ExecResult{
241241+ stdout: String.t(),
242242+ stderr: String.t(),
243243+ exit_code: integer()
244244+}
245245+```
246246+247247+### `%Sandbox.Types.Port{}`
248248+249249+Returned in the list by `Sandbox.list_ports/2`.
250250+251251+```
252252+%Sandbox.Types.Port{
253253+ port: integer(),
254254+ description: String.t() | nil,
255255+ preview_url: String.t() | nil
256256+}
257257+```
258258+259259+### `%Sandbox.Types.Profile{}`
260260+261261+Returned by `Pocketenv.me/1` and `Pocketenv.get_profile/2`.
262262+263263+```
264264+%Sandbox.Types.Profile{
265265+ id: String.t() | nil,
266266+ did: String.t(),
267267+ handle: String.t(),
268268+ display_name: String.t() | nil,
269269+ avatar: String.t() | nil,
270270+ created_at: String.t() | nil,
271271+ updated_at: String.t() | nil
272272+}
273273+```
274274+275275+---
276276+277277+## Low-level client
278278+279279+If you need to call an endpoint not yet covered by the high-level API,
280280+use `Pocketenv.Client` directly:
281281+282282+```elixir
283283+{:ok, body} = Pocketenv.Client.get(
284284+ "/xrpc/io.pocketenv.sandbox.getSandbox",
285285+ params: %{"id" => "my-sandbox"},
286286+ token: "override-token"
287287+)
288288+289289+{:ok, body} = Pocketenv.Client.post(
290290+ "/xrpc/io.pocketenv.sandbox.startSandbox",
291291+ %{"keepAlive" => true},
292292+ params: %{"id" => "my-sandbox"}
293293+)
294294+```
295295+296296+---
297297+298298+## Running tests
299299+300300+```sh
301301+mix test
302302+```
303303+304304+The test suite does **not** make real HTTP calls. Integration tests that
305305+exercise the live API require a valid `POCKETENV_TOKEN` and are not
306306+included by default.
307307+308308+---
309309+310310+## License
311311+312312+MIT
+146
lib/pocketenv.ex
···11+defmodule Pocketenv do
22+ @moduledoc """
33+ Elixir SDK for the [Pocketenv](https://pocketenv.io) sandbox platform.
44+55+ `Pocketenv` is the single entry point for the SDK. It returns `%Sandbox{}`
66+ structs that you pipe operations on:
77+88+ {:ok, sandbox} =
99+ Pocketenv.create_sandbox("my-sandbox")
1010+ |> Sandbox.start()
1111+ |> Sandbox.wait_until_running()
1212+1313+ {:ok, result} = sandbox |> Sandbox.exec("mix", ["test"])
1414+ IO.puts(result.stdout)
1515+1616+ {:ok, url} = sandbox |> Sandbox.expose(3000)
1717+1818+ sandbox
1919+ |> Sandbox.stop()
2020+ |> Sandbox.delete()
2121+2222+ ## Configuration
2323+2424+ ### `config/config.exs`
2525+2626+ import Config
2727+2828+ config :pocketenv,
2929+ token: "your-token",
3030+ api_url: "https://api.pocketenv.io" # optional
3131+3232+ ### Environment variables
3333+3434+ export POCKETENV_TOKEN="your-token"
3535+ export POCKETENV_API_URL="https://api.pocketenv.io" # optional
3636+3737+ Application config takes precedence over environment variables.
3838+ """
3939+4040+ alias Pocketenv.API
4141+4242+ # ---------------------------------------------------------------------------
4343+ # Sandboxes
4444+ # ---------------------------------------------------------------------------
4545+4646+ @doc """
4747+ Creates a new sandbox and returns a `%Sandbox{}`.
4848+4949+ ## Options
5050+5151+ - `:base` — AT-URI of the base sandbox image (default: `openclaw`).
5252+ - `:provider` — `"cloudflare"` (default), `"daytona"`, `"deno"`,
5353+ `"vercel"`, or `"sprites"`.
5454+ - `:repo` — GitHub repo URL to clone into the sandbox on start.
5555+ - `:keep_alive` — keep the sandbox alive after the session ends.
5656+ - `:token` — bearer token override.
5757+5858+ ## Example
5959+6060+ {:ok, sandbox} = Pocketenv.create_sandbox("my-sandbox")
6161+ {:ok, sandbox} = Pocketenv.create_sandbox("ml-box", repo: "github.com/me/repo")
6262+ """
6363+ @spec create_sandbox(String.t(), keyword()) :: {:ok, Sandbox.t()} | {:error, term()}
6464+ defdelegate create_sandbox(name, opts \\ []), to: API
6565+6666+ @doc """
6767+ Fetches a single sandbox by id or name.
6868+6969+ ## Example
7070+7171+ {:ok, sandbox} = Pocketenv.get_sandbox("my-sandbox")
7272+ {:ok, nil} = Pocketenv.get_sandbox("nonexistent")
7373+ """
7474+ @spec get_sandbox(String.t(), keyword()) :: {:ok, Sandbox.t() | nil} | {:error, term()}
7575+ defdelegate get_sandbox(id, opts \\ []), to: API
7676+7777+ @doc """
7878+ Lists the official public sandbox catalog.
7979+8080+ Returns `{:ok, {[%Sandbox{}], total}}`.
8181+8282+ ## Options
8383+8484+ - `:limit` — max results (default: `30`).
8585+ - `:offset` — pagination offset (default: `0`).
8686+ - `:token` — bearer token override.
8787+8888+ ## Example
8989+9090+ {:ok, {sandboxes, total}} = Pocketenv.list_sandboxes()
9191+ """
9292+ @spec list_sandboxes(keyword()) ::
9393+ {:ok, {[Sandbox.t()], non_neg_integer()}} | {:error, term()}
9494+ defdelegate list_sandboxes(opts \\ []), to: API
9595+9696+ @doc """
9797+ Lists all sandboxes belonging to a specific actor (user).
9898+9999+ Returns `{:ok, {[%Sandbox{}], total}}`.
100100+101101+ ## Parameters
102102+103103+ - `did` — the actor's DID (`"did:plc:..."`) or handle
104104+ (`"alice.bsky.social"`).
105105+106106+ ## Options
107107+108108+ - `:limit` — max results (default: `30`).
109109+ - `:offset` — pagination offset (default: `0`).
110110+ - `:token` — bearer token override.
111111+112112+ ## Example
113113+114114+ {:ok, {sandboxes, total}} = Pocketenv.list_sandboxes_by_actor("alice.bsky.social")
115115+ """
116116+ @spec list_sandboxes_by_actor(String.t(), keyword()) ::
117117+ {:ok, {[Sandbox.t()], non_neg_integer()}} | {:error, term()}
118118+ defdelegate list_sandboxes_by_actor(did, opts \\ []), to: API
119119+120120+ # ---------------------------------------------------------------------------
121121+ # Actor / profile
122122+ # ---------------------------------------------------------------------------
123123+124124+ @doc """
125125+ Fetches the profile of the currently authenticated user.
126126+127127+ ## Example
128128+129129+ {:ok, me} = Pocketenv.me()
130130+ IO.puts("Logged in as @\#{me.handle}")
131131+ """
132132+ @spec me(keyword()) :: {:ok, Sandbox.Types.Profile.t()} | {:error, term()}
133133+ defdelegate me(opts \\ []), to: API
134134+135135+ @doc """
136136+ Fetches the profile of any actor by DID or handle.
137137+138138+ ## Example
139139+140140+ {:ok, profile} = Pocketenv.get_profile("alice.bsky.social")
141141+ {:ok, profile} = Pocketenv.get_profile("did:plc:abc123")
142142+ """
143143+ @spec get_profile(String.t(), keyword()) ::
144144+ {:ok, Sandbox.Types.Profile.t()} | {:error, term()}
145145+ defdelegate get_profile(did, opts \\ []), to: API
146146+end
+243
lib/pocketenv/api.ex
···11+defmodule Pocketenv.API do
22+ @moduledoc false
33+ # Internal HTTP layer. Consumers should use the `Pocketenv` module and
44+ # pipe on `%Sandbox{}` structs. This module is not part of the public API.
55+66+ alias Pocketenv.Client
77+ alias Sandbox.Types.{ExecResult, Port, Profile}
88+99+ @default_base "at://did:plc:aturpi2ls3yvsmhc6wybomun/io.pocketenv.sandbox/openclaw"
1010+1111+ # ---------------------------------------------------------------------------
1212+ # Sandbox CRUD
1313+ # ---------------------------------------------------------------------------
1414+1515+ def create_sandbox(name, opts \\ []) do
1616+ body =
1717+ %{
1818+ "name" => name,
1919+ "base" => Keyword.get(opts, :base, @default_base),
2020+ "provider" => Keyword.get(opts, :provider, "cloudflare")
2121+ }
2222+ |> maybe_put("repo", Keyword.get(opts, :repo))
2323+ |> maybe_put("keepAlive", Keyword.get(opts, :keep_alive))
2424+2525+ case Client.post("/xrpc/io.pocketenv.sandbox.createSandbox", body, take_token(opts)) do
2626+ {:ok, data} -> {:ok, Sandbox.from_map(data)}
2727+ {:error, _} = err -> err
2828+ end
2929+ end
3030+3131+ def start_sandbox(id, opts \\ []) do
3232+ body =
3333+ %{}
3434+ |> maybe_put("repo", Keyword.get(opts, :repo))
3535+ |> maybe_put("keepAlive", Keyword.get(opts, :keep_alive))
3636+3737+ Client.post(
3838+ "/xrpc/io.pocketenv.sandbox.startSandbox",
3939+ body,
4040+ take_token(opts) ++ [params: %{"id" => id}]
4141+ )
4242+ end
4343+4444+ def stop_sandbox(id, opts \\ []) do
4545+ Client.post(
4646+ "/xrpc/io.pocketenv.sandbox.stopSandbox",
4747+ nil,
4848+ take_token(opts) ++ [params: %{"id" => id}]
4949+ )
5050+ end
5151+5252+ def delete_sandbox(id, opts \\ []) do
5353+ Client.post(
5454+ "/xrpc/io.pocketenv.sandbox.deleteSandbox",
5555+ nil,
5656+ take_token(opts) ++ [params: %{"id" => id}]
5757+ )
5858+ end
5959+6060+ # ---------------------------------------------------------------------------
6161+ # Sandbox queries
6262+ # ---------------------------------------------------------------------------
6363+6464+ def get_sandbox(id, opts \\ []) do
6565+ case Client.get(
6666+ "/xrpc/io.pocketenv.sandbox.getSandbox",
6767+ take_token(opts) ++ [params: %{"id" => id}]
6868+ ) do
6969+ {:ok, %{"sandbox" => nil}} -> {:ok, nil}
7070+ {:ok, %{"sandbox" => data}} -> {:ok, Sandbox.from_map(data)}
7171+ {:ok, data} when is_map(data) -> {:ok, Sandbox.from_map(data)}
7272+ {:error, _} = err -> err
7373+ end
7474+ end
7575+7676+ def list_sandboxes(opts \\ []) do
7777+ params = %{
7878+ "limit" => Keyword.get(opts, :limit, 30),
7979+ "offset" => Keyword.get(opts, :offset, 0)
8080+ }
8181+8282+ case Client.get(
8383+ "/xrpc/io.pocketenv.sandbox.getSandboxes",
8484+ take_token(opts) ++ [params: params]
8585+ ) do
8686+ {:ok, %{"sandboxes" => items, "total" => total}} ->
8787+ {:ok, {Enum.map(items, &Sandbox.from_map/1), total}}
8888+8989+ {:error, _} = err ->
9090+ err
9191+ end
9292+ end
9393+9494+ def list_sandboxes_by_actor(did, opts \\ []) do
9595+ params = %{
9696+ "did" => did,
9797+ "limit" => Keyword.get(opts, :limit, 30),
9898+ "offset" => Keyword.get(opts, :offset, 0)
9999+ }
100100+101101+ case Client.get(
102102+ "/xrpc/io.pocketenv.actor.getActorSandboxes",
103103+ take_token(opts) ++ [params: params]
104104+ ) do
105105+ {:ok, %{"sandboxes" => items, "total" => total}} ->
106106+ {:ok, {Enum.map(items, &Sandbox.from_map/1), total}}
107107+108108+ {:error, _} = err ->
109109+ err
110110+ end
111111+ end
112112+113113+ def wait_until_running(id, opts \\ []) do
114114+ timeout_ms = Keyword.get(opts, :timeout_ms, 60_000)
115115+ interval_ms = Keyword.get(opts, :interval_ms, 2_000)
116116+ deadline = System.monotonic_time(:millisecond) + timeout_ms
117117+ do_wait(id, opts, deadline, interval_ms)
118118+ end
119119+120120+ # ---------------------------------------------------------------------------
121121+ # Exec
122122+ # ---------------------------------------------------------------------------
123123+124124+ def exec(id, cmd, args \\ [], opts \\ []) do
125125+ command = Enum.join([cmd | args], " ")
126126+127127+ case Client.post(
128128+ "/xrpc/io.pocketenv.sandbox.exec",
129129+ %{"command" => command},
130130+ take_token(opts) ++ [params: %{"id" => id}]
131131+ ) do
132132+ {:ok, data} -> {:ok, ExecResult.from_map(data)}
133133+ {:error, _} = err -> err
134134+ end
135135+ end
136136+137137+ # ---------------------------------------------------------------------------
138138+ # Ports
139139+ # ---------------------------------------------------------------------------
140140+141141+ def expose_port(id, port, opts \\ []) do
142142+ body =
143143+ %{"port" => port}
144144+ |> maybe_put("description", Keyword.get(opts, :description))
145145+146146+ case Client.post(
147147+ "/xrpc/io.pocketenv.sandbox.exposePort",
148148+ body,
149149+ take_token(opts) ++ [params: %{"id" => id}]
150150+ ) do
151151+ {:ok, %{"previewUrl" => url}} -> {:ok, url}
152152+ {:ok, _} -> {:ok, nil}
153153+ {:error, _} = err -> err
154154+ end
155155+ end
156156+157157+ def unexpose_port(id, port, opts \\ []) do
158158+ Client.post(
159159+ "/xrpc/io.pocketenv.sandbox.unexposePort",
160160+ %{"port" => port},
161161+ take_token(opts) ++ [params: %{"id" => id}]
162162+ )
163163+ end
164164+165165+ def list_ports(id, opts \\ []) do
166166+ case Client.get(
167167+ "/xrpc/io.pocketenv.sandbox.getExposedPorts",
168168+ take_token(opts) ++ [params: %{"id" => id}]
169169+ ) do
170170+ {:ok, %{"ports" => ports}} -> {:ok, Enum.map(ports, &Port.from_map/1)}
171171+ {:error, _} = err -> err
172172+ end
173173+ end
174174+175175+ # ---------------------------------------------------------------------------
176176+ # VS Code
177177+ # ---------------------------------------------------------------------------
178178+179179+ def expose_vscode(id, opts \\ []) do
180180+ case Client.post(
181181+ "/xrpc/io.pocketenv.sandbox.exposeVscode",
182182+ nil,
183183+ take_token(opts) ++ [params: %{"id" => id}]
184184+ ) do
185185+ {:ok, %{"previewUrl" => url}} -> {:ok, url}
186186+ {:ok, _} -> {:ok, nil}
187187+ {:error, _} = err -> err
188188+ end
189189+ end
190190+191191+ # ---------------------------------------------------------------------------
192192+ # Actor / profile
193193+ # ---------------------------------------------------------------------------
194194+195195+ def me(opts \\ []) do
196196+ case Client.get("/xrpc/io.pocketenv.actor.getProfile", take_token(opts)) do
197197+ {:ok, data} -> {:ok, Profile.from_map(data)}
198198+ {:error, _} = err -> err
199199+ end
200200+ end
201201+202202+ def get_profile(did, opts \\ []) do
203203+ case Client.get(
204204+ "/xrpc/io.pocketenv.actor.getProfile",
205205+ take_token(opts) ++ [params: %{"did" => did}]
206206+ ) do
207207+ {:ok, data} -> {:ok, Profile.from_map(data)}
208208+ {:error, _} = err -> err
209209+ end
210210+ end
211211+212212+ # ---------------------------------------------------------------------------
213213+ # Private helpers
214214+ # ---------------------------------------------------------------------------
215215+216216+ defp do_wait(id, opts, deadline, interval_ms) do
217217+ if System.monotonic_time(:millisecond) >= deadline do
218218+ {:error, :timeout}
219219+ else
220220+ case get_sandbox(id, opts) do
221221+ {:ok, %Sandbox{status: :running} = sandbox} ->
222222+ {:ok, sandbox}
223223+224224+ {:ok, _} ->
225225+ Process.sleep(interval_ms)
226226+ do_wait(id, opts, deadline, interval_ms)
227227+228228+ {:error, _} = err ->
229229+ err
230230+ end
231231+ end
232232+ end
233233+234234+ defp take_token(opts) do
235235+ case Keyword.fetch(opts, :token) do
236236+ {:ok, token} -> [token: token]
237237+ :error -> []
238238+ end
239239+ end
240240+241241+ defp maybe_put(map, _key, nil), do: map
242242+ defp maybe_put(map, key, value), do: Map.put(map, key, value)
243243+end
+165
lib/pocketenv/client.ex
···11+defmodule Pocketenv.Client do
22+ @moduledoc """
33+ Low-level HTTP client for the Pocketenv XRPC API.
44+55+ Wraps `Req` to provide authenticated requests to the
66+ `https://api.pocketenv.io` base URL (configurable via the
77+ `:pocketenv` application environment or the `POCKETENV_API_URL`
88+ environment variable).
99+1010+ ## Configuration
1111+1212+ You can configure the API URL and token in `config/config.exs`:
1313+1414+ config :pocketenv,
1515+ api_url: "https://api.pocketenv.io",
1616+ token: "your-token-here"
1717+1818+ Or set the `POCKETENV_API_URL` / `POCKETENV_TOKEN` environment
1919+ variables at runtime.
2020+2121+ ## Usage
2222+2323+ iex> Pocketenv.Client.get("/xrpc/io.pocketenv.actor.getProfile", token: "...", params: %{did: "did:plc:..."})
2424+ {:ok, %{"did" => "did:plc:...", ...}}
2525+ """
2626+2727+ @default_api_url "https://api.pocketenv.io"
2828+2929+ @doc """
3030+ Returns the base URL for the API, resolved in order from:
3131+3232+ 1. The `:api_url` key in the `:pocketenv` application environment.
3333+ 2. The `POCKETENV_API_URL` environment variable.
3434+ 3. The hardcoded default `https://api.pocketenv.io`.
3535+ """
3636+ @spec base_url() :: String.t()
3737+ def base_url do
3838+ Application.get_env(:pocketenv, :api_url) ||
3939+ System.get_env("POCKETENV_API_URL") ||
4040+ @default_api_url
4141+ end
4242+4343+ @doc """
4444+ Returns the bearer token, resolved in order from:
4545+4646+ 1. The `:token` key in the `:pocketenv` application environment.
4747+ 2. The `POCKETENV_TOKEN` environment variable.
4848+ 3. `nil` (unauthenticated requests are allowed for some endpoints).
4949+ """
5050+ @spec token() :: {:ok, String.t()} | {:error, :not_logged_in}
5151+ def token do
5252+ case Application.get_env(:pocketenv, :token) ||
5353+ System.get_env("POCKETENV_TOKEN") ||
5454+ read_token_file() do
5555+ nil -> {:error, :not_logged_in}
5656+ token -> {:ok, token}
5757+ end
5858+ end
5959+6060+ defp read_token_file do
6161+ path = Path.join([System.user_home!(), ".pocketenv", "token.json"])
6262+6363+ with {:ok, contents} <- File.read(path),
6464+ {:ok, %{"token" => token}} when is_binary(token) <- Jason.decode(contents) do
6565+ token
6666+ else
6767+ _ -> nil
6868+ end
6969+ end
7070+7171+ @doc """
7272+ Perform an HTTP GET request against the Pocketenv API.
7373+7474+ ## Options
7575+7676+ * `:token` – bearer token; defaults to `token/0`.
7777+ * `:params` – query-string parameters as a map.
7878+7979+ Returns `{:ok, body}` on a 2xx response, or `{:error, reason}`.
8080+ """
8181+ @spec get(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
8282+ def get(path, opts \\ []) do
8383+ request(:get, path, nil, opts)
8484+ end
8585+8686+ @doc """
8787+ Perform an HTTP POST request against the Pocketenv API.
8888+8989+ ## Options
9090+9191+ * `:token` – bearer token; defaults to `token/0`.
9292+ * `:params` – query-string parameters as a map.
9393+9494+ Returns `{:ok, body}` on a 2xx response, or `{:error, reason}`.
9595+ """
9696+ @spec post(String.t(), map() | nil, keyword()) :: {:ok, map()} | {:error, term()}
9797+ def post(path, body \\ nil, opts \\ []) do
9898+ request(:post, path, body, opts)
9999+ end
100100+101101+ @doc """
102102+ Perform an HTTP DELETE request against the Pocketenv API.
103103+104104+ ## Options
105105+106106+ * `:token` – bearer token; defaults to `token/0`.
107107+ * `:params` – query-string parameters as a map.
108108+109109+ Returns `{:ok, body}` on a 2xx response, or `{:error, reason}`.
110110+ """
111111+ @spec delete(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
112112+ def delete(path, opts \\ []) do
113113+ request(:delete, path, nil, opts)
114114+ end
115115+116116+ # ---------------------------------------------------------------------------
117117+ # Private helpers
118118+ # ---------------------------------------------------------------------------
119119+120120+ defp request(method, path, body, opts) do
121121+ bearer =
122122+ case Keyword.fetch(opts, :token) do
123123+ {:ok, t} -> {:ok, t}
124124+ :error -> token()
125125+ end
126126+127127+ case bearer do
128128+ {:error, :not_logged_in} ->
129129+ {:error, :not_logged_in}
130130+131131+ {:ok, t} ->
132132+ params = Keyword.get(opts, :params, %{})
133133+134134+ req_opts =
135135+ [
136136+ method: method,
137137+ url: base_url() <> path,
138138+ headers: build_headers(t),
139139+ params: params
140140+ ]
141141+ |> maybe_put_json_body(body)
142142+143143+ case Req.request(req_opts) do
144144+ {:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
145145+ {:ok, body}
146146+147147+ {:ok, %Req.Response{status: status, body: body}} ->
148148+ {:error, %{status: status, body: body}}
149149+150150+ {:error, reason} ->
151151+ {:error, reason}
152152+ end
153153+ end
154154+ end
155155+156156+ defp build_headers(token) do
157157+ [
158158+ {"content-type", "application/json"},
159159+ {"authorization", "Bearer #{token}"}
160160+ ]
161161+ end
162162+163163+ defp maybe_put_json_body(req_opts, nil), do: req_opts
164164+ defp maybe_put_json_body(req_opts, body), do: Keyword.put(req_opts, :json, body)
165165+end
+379
lib/sandbox.ex
···11+defmodule Sandbox do
22+ @moduledoc """
33+ Represents a Pocketenv sandbox environment.
44+55+ `%Sandbox{}` is the central type of the SDK. You get one back from
66+ `Pocketenv.create_sandbox/2` or `Pocketenv.get_sandbox/2`, and then
77+ pipe operations directly on it:
88+99+ {:ok, sandbox} =
1010+ Pocketenv.create_sandbox("my-sandbox")
1111+ |> Sandbox.start()
1212+ |> Sandbox.wait_until_running()
1313+1414+ {:ok, result} = sandbox |> Sandbox.exec("mix", ["test"])
1515+ IO.puts(result.stdout)
1616+1717+ {:ok, url} = sandbox |> Sandbox.expose(3000)
1818+1919+ sandbox
2020+ |> Sandbox.stop()
2121+ |> Sandbox.delete()
2222+2323+ ## Pipe safety
2424+2525+ Every function accepts either a bare `%Sandbox{}` **or** an
2626+ `{:ok, %Sandbox{}}` tuple as its first argument, so you can pipe from
2727+ any previous step without manually unwrapping the result.
2828+2929+ An `{:error, reason}` value is never silently swallowed — passing one
3030+ raises `FunctionClauseError`, keeping error handling explicit.
3131+ """
3232+3333+ alias Pocketenv.API
3434+ alias Sandbox.Types.{ExecResult, Port, Profile}
3535+3636+ @type status :: :running | :stopped | :unknown
3737+3838+ @type t :: %__MODULE__{
3939+ id: String.t() | nil,
4040+ name: String.t() | nil,
4141+ provider: String.t() | nil,
4242+ base_sandbox: String.t() | nil,
4343+ display_name: String.t() | nil,
4444+ uri: String.t() | nil,
4545+ description: String.t() | nil,
4646+ topics: [String.t()] | nil,
4747+ logo: String.t() | nil,
4848+ readme: String.t() | nil,
4949+ repo: String.t() | nil,
5050+ vcpus: integer() | nil,
5151+ memory: integer() | nil,
5252+ disk: integer() | nil,
5353+ installs: integer(),
5454+ status: status(),
5555+ started_at: String.t() | nil,
5656+ created_at: String.t() | nil,
5757+ owner: Profile.t() | nil
5858+ }
5959+6060+ defstruct [
6161+ :id,
6262+ :name,
6363+ :provider,
6464+ :base_sandbox,
6565+ :display_name,
6666+ :uri,
6767+ :description,
6868+ :topics,
6969+ :logo,
7070+ :readme,
7171+ :repo,
7272+ :vcpus,
7373+ :memory,
7474+ :disk,
7575+ :installs,
7676+ :status,
7777+ :started_at,
7878+ :created_at,
7979+ :owner
8080+ ]
8181+8282+ @doc false
8383+ @spec from_map(map()) :: t()
8484+ def from_map(map) when is_map(map) do
8585+ %__MODULE__{
8686+ id: map["id"],
8787+ name: map["name"],
8888+ provider: map["provider"],
8989+ base_sandbox: map["baseSandbox"],
9090+ display_name: map["displayName"],
9191+ uri: map["uri"],
9292+ description: map["description"],
9393+ topics: map["topics"],
9494+ logo: map["logo"],
9595+ readme: map["readme"],
9696+ repo: map["repo"],
9797+ vcpus: map["vcpus"],
9898+ memory: map["memory"],
9999+ disk: map["disk"],
100100+ installs: map["installs"] || 0,
101101+ status: parse_status(map["status"]),
102102+ started_at: map["startedAt"],
103103+ created_at: map["createdAt"],
104104+ owner: map["owner"] && Profile.from_map(map["owner"])
105105+ }
106106+ end
107107+108108+ # ---------------------------------------------------------------------------
109109+ # Lifecycle
110110+ # ---------------------------------------------------------------------------
111111+112112+ @doc """
113113+ Starts the sandbox.
114114+115115+ Re-fetches the sandbox after the API call so the returned struct always
116116+ reflects the latest state.
117117+118118+ ## Options
119119+120120+ - `:repo` — clone a GitHub repo into the sandbox on start.
121121+ - `:keep_alive` — keep the sandbox alive after the session ends.
122122+ - `:token` — bearer token override.
123123+124124+ ## Example
125125+126126+ sandbox |> Sandbox.start()
127127+ sandbox |> Sandbox.start(repo: "github.com/me/app")
128128+ Pocketenv.get_sandbox("my-sandbox") |> Sandbox.start()
129129+ """
130130+ @spec start(t() | {:ok, t()}, keyword()) :: {:ok, t()} | {:error, term()}
131131+ def start(sandbox_or_result, opts \\ [])
132132+ def start({:ok, %__MODULE__{} = sandbox}, opts), do: start(sandbox, opts)
133133+134134+ def start(%__MODULE__{} = sandbox, opts) do
135135+ with {:ok, _} <- API.start_sandbox(sandbox.name, opts) do
136136+ API.get_sandbox(sandbox.name, opts)
137137+ end
138138+ end
139139+140140+ @doc """
141141+ Stops the sandbox.
142142+143143+ Re-fetches the sandbox after the API call so the returned struct always
144144+ reflects the latest state.
145145+146146+ ## Options
147147+148148+ - `:token` — bearer token override.
149149+150150+ ## Example
151151+152152+ sandbox |> Sandbox.stop()
153153+ Pocketenv.get_sandbox("my-sandbox") |> Sandbox.stop()
154154+ """
155155+ @spec stop(t() | {:ok, t()}, keyword()) :: {:ok, t()} | {:error, term()}
156156+ def stop(sandbox_or_result, opts \\ [])
157157+ def stop({:ok, %__MODULE__{} = sandbox}, opts), do: stop(sandbox, opts)
158158+159159+ def stop(%__MODULE__{} = sandbox, opts) do
160160+ with {:ok, _} <- API.stop_sandbox(sandbox.name, opts) do
161161+ API.get_sandbox(sandbox.name, opts)
162162+ end
163163+ end
164164+165165+ @doc """
166166+ Deletes the sandbox permanently.
167167+168168+ Returns `{:ok, %Sandbox{}}` with the last known state — the sandbox
169169+ will no longer be fetchable after this call.
170170+171171+ ## Options
172172+173173+ - `:token` — bearer token override.
174174+175175+ ## Example
176176+177177+ sandbox |> Sandbox.delete()
178178+ sandbox |> Sandbox.stop() |> Sandbox.delete()
179179+ """
180180+ @spec delete(t() | {:ok, t()}, keyword()) :: {:ok, t()} | {:error, term()}
181181+ def delete(sandbox_or_result, opts \\ [])
182182+ def delete({:ok, %__MODULE__{} = sandbox}, opts), do: delete(sandbox, opts)
183183+184184+ def delete(%__MODULE__{} = sandbox, opts) do
185185+ with {:ok, _} <- API.delete_sandbox(sandbox.name, opts) do
186186+ {:ok, sandbox}
187187+ end
188188+ end
189189+190190+ @doc """
191191+ Polls until the sandbox status becomes `:running`, then returns the
192192+ refreshed `%Sandbox{}`.
193193+194194+ Useful after `start/2` when you need the sandbox to be fully ready
195195+ before running commands.
196196+197197+ ## Options
198198+199199+ - `:timeout_ms` — total wait time in ms (default: `60_000`).
200200+ - `:interval_ms` — polling interval in ms (default: `2_000`).
201201+ - `:token` — bearer token override.
202202+203203+ ## Returns
204204+205205+ - `{:ok, %Sandbox{status: :running}}` on success.
206206+ - `{:error, :timeout}` if the deadline is exceeded.
207207+208208+ ## Example
209209+210210+ sandbox
211211+ |> Sandbox.start()
212212+ |> Sandbox.wait_until_running()
213213+ |> Sandbox.exec("mix", ["test"])
214214+ """
215215+ @spec wait_until_running(t() | {:ok, t()}, keyword()) ::
216216+ {:ok, t()} | {:error, :timeout | term()}
217217+ def wait_until_running(sandbox_or_result, opts \\ [])
218218+219219+ def wait_until_running({:ok, %__MODULE__{} = sandbox}, opts),
220220+ do: wait_until_running(sandbox, opts)
221221+222222+ def wait_until_running(%__MODULE__{} = sandbox, opts) do
223223+ API.wait_until_running(sandbox.name, opts)
224224+ end
225225+226226+ # ---------------------------------------------------------------------------
227227+ # Commands
228228+ # ---------------------------------------------------------------------------
229229+230230+ @doc """
231231+ Executes a shell command inside the sandbox.
232232+233233+ ## Parameters
234234+235235+ - `cmd` — the executable (e.g. `"mix"`, `"echo"`).
236236+ - `args` — list of string arguments (default: `[]`).
237237+238238+ ## Options
239239+240240+ - `:token` — bearer token override.
241241+242242+ ## Returns
243243+244244+ `{:ok, %Sandbox.Types.ExecResult{stdout, stderr, exit_code}}`
245245+246246+ ## Example
247247+248248+ sandbox |> Sandbox.exec("echo", ["hello"])
249249+ sandbox |> Sandbox.exec("mix", ["test", "--trace"])
250250+ """
251251+ @spec exec(t() | {:ok, t()}, String.t(), [String.t()], keyword()) ::
252252+ {:ok, ExecResult.t()} | {:error, term()}
253253+ def exec(sandbox_or_result, cmd, args \\ [], opts \\ [])
254254+ def exec({:ok, %__MODULE__{} = sandbox}, cmd, args, opts), do: exec(sandbox, cmd, args, opts)
255255+256256+ def exec(%__MODULE__{} = sandbox, cmd, args, opts) do
257257+ API.exec(sandbox.name, cmd, args, opts)
258258+ end
259259+260260+ # ---------------------------------------------------------------------------
261261+ # Ports
262262+ # ---------------------------------------------------------------------------
263263+264264+ @doc """
265265+ Exposes a port on the sandbox so it is publicly accessible.
266266+267267+ ## Options
268268+269269+ - `:description` — a human-readable label for the port.
270270+ - `:token` — bearer token override.
271271+272272+ ## Returns
273273+274274+ `{:ok, preview_url}` — `preview_url` is `nil` when the provider does
275275+ not return one.
276276+277277+ ## Example
278278+279279+ sandbox |> Sandbox.expose(3000)
280280+ sandbox |> Sandbox.expose(8080, description: "API server")
281281+ """
282282+ @spec expose(t() | {:ok, t()}, pos_integer(), keyword()) ::
283283+ {:ok, String.t() | nil} | {:error, term()}
284284+ def expose(sandbox_or_result, port, opts \\ [])
285285+ def expose({:ok, %__MODULE__{} = sandbox}, port, opts), do: expose(sandbox, port, opts)
286286+287287+ def expose(%__MODULE__{} = sandbox, port, opts) do
288288+ API.expose_port(sandbox.name, port, opts)
289289+ end
290290+291291+ @doc """
292292+ Removes an exposed port from the sandbox.
293293+294294+ Returns `{:ok, %Sandbox{}}` (same struct passed in) so the pipe can
295295+ continue.
296296+297297+ ## Options
298298+299299+ - `:token` — bearer token override.
300300+301301+ ## Example
302302+303303+ sandbox |> Sandbox.unexpose(3000)
304304+ """
305305+ @spec unexpose(t() | {:ok, t()}, pos_integer(), keyword()) ::
306306+ {:ok, t()} | {:error, term()}
307307+ def unexpose(sandbox_or_result, port, opts \\ [])
308308+ def unexpose({:ok, %__MODULE__{} = sandbox}, port, opts), do: unexpose(sandbox, port, opts)
309309+310310+ def unexpose(%__MODULE__{} = sandbox, port, opts) do
311311+ with {:ok, _} <- API.unexpose_port(sandbox.name, port, opts) do
312312+ {:ok, sandbox}
313313+ end
314314+ end
315315+316316+ @doc """
317317+ Lists all currently exposed ports on the sandbox.
318318+319319+ ## Options
320320+321321+ - `:token` — bearer token override.
322322+323323+ ## Returns
324324+325325+ `{:ok, [%Sandbox.Types.Port{port, description, preview_url}]}`
326326+327327+ ## Example
328328+329329+ sandbox |> Sandbox.list_ports()
330330+ """
331331+ @spec list_ports(t() | {:ok, t()}, keyword()) ::
332332+ {:ok, [Port.t()]} | {:error, term()}
333333+ def list_ports(sandbox_or_result, opts \\ [])
334334+ def list_ports({:ok, %__MODULE__{} = sandbox}, opts), do: list_ports(sandbox, opts)
335335+336336+ def list_ports(%__MODULE__{} = sandbox, opts) do
337337+ API.list_ports(sandbox.name, opts)
338338+ end
339339+340340+ # ---------------------------------------------------------------------------
341341+ # VS Code
342342+ # ---------------------------------------------------------------------------
343343+344344+ @doc """
345345+ Exposes a VS Code Server instance for the sandbox and returns its URL.
346346+347347+ If VS Code is already exposed the existing URL is returned immediately
348348+ without re-provisioning.
349349+350350+ ## Options
351351+352352+ - `:token` — bearer token override.
353353+354354+ ## Returns
355355+356356+ `{:ok, preview_url}` — `preview_url` is `nil` when the provider does
357357+ not return one.
358358+359359+ ## Example
360360+361361+ {:ok, url} = sandbox |> Sandbox.vscode()
362362+ IO.puts("Open VS Code at: \#{url}")
363363+ """
364364+ @spec vscode(t() | {:ok, t()}, keyword()) :: {:ok, String.t() | nil} | {:error, term()}
365365+ def vscode(sandbox_or_result, opts \\ [])
366366+ def vscode({:ok, %__MODULE__{} = sandbox}, opts), do: vscode(sandbox, opts)
367367+368368+ def vscode(%__MODULE__{} = sandbox, opts) do
369369+ API.expose_vscode(sandbox.name, opts)
370370+ end
371371+372372+ # ---------------------------------------------------------------------------
373373+ # Private
374374+ # ---------------------------------------------------------------------------
375375+376376+ defp parse_status("RUNNING"), do: :running
377377+ defp parse_status("STOPPED"), do: :stopped
378378+ defp parse_status(_), do: :unknown
379379+end
+76
lib/sandbox/types.ex
···11+defmodule Sandbox.Types do
22+ @moduledoc """
33+ Types and structs used by the Pocketenv Sandbox SDK.
44+ """
55+66+ defmodule Profile do
77+ @moduledoc "Represents a Pocketenv user profile."
88+99+ @type t :: %__MODULE__{
1010+ id: String.t() | nil,
1111+ did: String.t(),
1212+ handle: String.t(),
1313+ display_name: String.t() | nil,
1414+ avatar: String.t() | nil,
1515+ created_at: String.t() | nil,
1616+ updated_at: String.t() | nil
1717+ }
1818+1919+ defstruct [:id, :did, :handle, :display_name, :avatar, :created_at, :updated_at]
2020+2121+ @doc "Build a Profile from the raw API map."
2222+ def from_map(map) when is_map(map) do
2323+ %__MODULE__{
2424+ id: map["id"],
2525+ did: map["did"],
2626+ handle: map["handle"],
2727+ display_name: map["displayName"],
2828+ avatar: map["avatar"],
2929+ created_at: map["createdAt"],
3030+ updated_at: map["updatedAt"]
3131+ }
3232+ end
3333+ end
3434+3535+ defmodule Port do
3636+ @moduledoc "Represents an exposed port on a sandbox."
3737+3838+ @type t :: %__MODULE__{
3939+ port: integer(),
4040+ description: String.t() | nil,
4141+ preview_url: String.t() | nil
4242+ }
4343+4444+ defstruct [:port, :description, :preview_url]
4545+4646+ @doc "Build a Port from the raw API map."
4747+ def from_map(map) when is_map(map) do
4848+ %__MODULE__{
4949+ port: map["port"],
5050+ description: map["description"],
5151+ preview_url: map["previewUrl"]
5252+ }
5353+ end
5454+ end
5555+5656+ defmodule ExecResult do
5757+ @moduledoc "Represents the result of executing a command in a sandbox."
5858+5959+ @type t :: %__MODULE__{
6060+ stdout: String.t(),
6161+ stderr: String.t(),
6262+ exit_code: integer()
6363+ }
6464+6565+ defstruct stdout: "", stderr: "", exit_code: 0
6666+6767+ @doc "Build an ExecResult from the raw API map."
6868+ def from_map(map) when is_map(map) do
6969+ %__MODULE__{
7070+ stdout: map["stdout"] || "",
7171+ stderr: map["stderr"] || "",
7272+ exit_code: map["exitCode"] || 0
7373+ }
7474+ end
7575+ end
7676+end
+36
mix.exs
···11+defmodule Pocketenv.MixProject do
22+ use Mix.Project
33+44+ def project do
55+ [
66+ app: :pocketenv,
77+ version: "0.1.0",
88+ elixir: "~> 1.15",
99+ start_permanent: Mix.env() == :prod,
1010+ deps: deps(),
1111+ description: "Elixir SDK for the Pocketenv sandbox API",
1212+ package: package()
1313+ ]
1414+ end
1515+1616+ def application do
1717+ [
1818+ extra_applications: [:logger]
1919+ ]
2020+ end
2121+2222+ defp deps do
2323+ [
2424+ {:req, "~> 0.5"},
2525+ {:jason, "~> 1.4"},
2626+ {:ex_doc, "~> 0.31", only: :dev, runtime: false}
2727+ ]
2828+ end
2929+3030+ defp package do
3131+ [
3232+ licenses: ["MIT"],
3333+ links: %{"GitHub" => "https://github.com/pocketenv-io/pocketenv-elixir"}
3434+ ]
3535+ end
3636+end