···11+MIT License
22+33+Copyright (c) 2026 tsiry.sndr@pocketenv.io
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
+269
README.md
···11+# pocketenv
22+33+[](https://hex.pm/packages/pocketenv)
44+[](https://hexdocs.pm/pocketenv/)
55+66+A Gleam client library for the [Pocketenv](https://pocketenv.io) API, providing
77+access to sandboxes, environment variables, secrets, files, volumes, services,
88+ports, and networking.
99+1010+```sh
1111+gleam add pocketenv@1
1212+```
1313+1414+## Usage
1515+1616+### Creating a client
1717+1818+```gleam
1919+import pocketenv
2020+2121+pub fn main() {
2222+ let client = pocketenv.new_client("your-api-token")
2323+ // use client with any of the sub-modules below
2424+}
2525+```
2626+2727+### Sandboxes
2828+2929+```gleam
3030+import pocketenv
3131+import pocketenv/sandbox
3232+import gleam/option.{None, Some}
3333+import gleam/io
3434+3535+pub fn main() {
3636+ let client = pocketenv.new_client("https://pocketenv.io", "your-token")
3737+3838+ let assert Ok(sb) =
3939+ client
4040+ |> sandbox.new("my-sandbox", "openclaw", "cloudflare")
4141+ |> sandbox.with_description("My app sandbox")
4242+ |> sandbox.create()
4343+ io.println("Created: " <> sb.data.id)
4444+4545+ // List all sandboxes
4646+ let assert Ok(#(sandboxes, _total)) = sandbox.list(client, None, None)
4747+ io.debug(sandboxes)
4848+4949+ // Start, exec, then stop the sandbox
5050+ let assert Ok(Nil) = sb |> sandbox.start(None, None)
5151+ let assert Ok(result) = sb |> sandbox.exec("echo hello")
5252+ io.println(result.stdout)
5353+ let assert Ok(Nil) = sb |> sandbox.stop()
5454+5555+ // Delete when done
5656+ let assert Ok(Nil) = sb |> sandbox.delete()
5757+}
5858+```
5959+6060+### Environment variables
6161+6262+```gleam
6363+import pocketenv
6464+import pocketenv/env
6565+import gleam/option.{None}
6666+6767+pub fn main() {
6868+ let client = pocketenv.new_client("https://pocketenv.io", "your-token")
6969+ let sandbox_id = "sandbox-abc123"
7070+7171+ // Set a variable
7272+ let assert Ok(Nil) = env.put(client, sandbox_id, "DATABASE_URL", "postgres://localhost/mydb")
7373+7474+ // List variables
7575+ let assert Ok(vars) = env.list(client, sandbox_id, None, None)
7676+7777+ // Delete a variable by its id
7878+ case vars {
7979+ [first, ..] -> {
8080+ let assert Ok(Nil) = env.delete(client, first.id)
8181+ }
8282+ [] -> Nil
8383+ }
8484+}
8585+```
8686+8787+### Secrets
8888+8989+```gleam
9090+import pocketenv
9191+import pocketenv/secrets
9292+import gleam/option.{None}
9393+9494+pub fn main() {
9595+ let client = pocketenv.new_client("https://pocketenv.io", "your-token")
9696+ let sandbox_id = "sandbox-abc123"
9797+9898+ // Store a secret
9999+ let assert Ok(Nil) = secrets.put(client, sandbox_id, "API_KEY", "super-secret-value")
100100+101101+ // List secret names (values are never returned)
102102+ let assert Ok(all) = secrets.list(client, sandbox_id, None, None)
103103+104104+ // Delete a secret
105105+ case all {
106106+ [first, ..] -> {
107107+ let assert Ok(Nil) = secrets.delete(client, first.id)
108108+ }
109109+ [] -> Nil
110110+ }
111111+}
112112+```
113113+114114+### Files
115115+116116+```gleam
117117+import pocketenv
118118+import pocketenv/files
119119+120120+pub fn main() {
121121+ let client = pocketenv.new_client("https://pocketenv.io", "your-token")
122122+ let sandbox_id = "sandbox-abc123"
123123+124124+ // Write a file into the sandbox
125125+ let assert Ok(Nil) =
126126+ files.write(client, sandbox_id, "/app/config.json", "{\"debug\": true}")
127127+128128+ // List files
129129+ let assert Ok(all) = files.list(client, sandbox_id)
130130+131131+ // Delete a file
132132+ case all {
133133+ [first, ..] -> {
134134+ let assert Ok(Nil) = files.delete(client, first.id)
135135+ }
136136+ [] -> Nil
137137+ }
138138+}
139139+```
140140+141141+### Volumes
142142+143143+```gleam
144144+import pocketenv
145145+import pocketenv/volume
146146+147147+pub fn main() {
148148+ let client = pocketenv.new_client("https://pocketenv.io", "your-token")
149149+ let sandbox_id = "sandbox-abc123"
150150+151151+ // Mount a persistent volume
152152+ let assert Ok(Nil) = volume.create(client, sandbox_id, "data-vol", "/mnt/data")
153153+154154+ // List volumes
155155+ let assert Ok(vols) = volume.list(client, sandbox_id)
156156+157157+ // Delete a volume
158158+ case vols {
159159+ [first, ..] -> {
160160+ let assert Ok(Nil) = volume.delete(client, first.id)
161161+ }
162162+ [] -> Nil
163163+ }
164164+}
165165+```
166166+167167+### Services
168168+169169+```gleam
170170+import pocketenv
171171+import pocketenv/services
172172+import gleam/option.{None, Some}
173173+174174+pub fn main() {
175175+ let client = pocketenv.new_client("https://pocketenv.io", "your-token")
176176+ let sandbox_id = "sandbox-abc123"
177177+178178+ // Register and start a web server service
179179+ let assert Ok(Nil) =
180180+ services.create(
181181+ client,
182182+ sandbox_id,
183183+ "web",
184184+ "python -m http.server 8080",
185185+ Some([8080]),
186186+ Some("Simple HTTP server"),
187187+ )
188188+189189+ let assert Ok(svcs) = services.list(client, sandbox_id)
190190+ case svcs {
191191+ [svc, ..] -> {
192192+ let assert Ok(Nil) = services.start(client, svc.id)
193193+ let assert Ok(Nil) = services.restart(client, svc.id)
194194+ let assert Ok(Nil) = services.stop(client, svc.id)
195195+ let assert Ok(Nil) = services.delete(client, svc.id)
196196+ }
197197+ [] -> Nil
198198+ }
199199+}
200200+```
201201+202202+### Ports & Networking
203203+204204+```gleam
205205+import pocketenv
206206+import pocketenv/network
207207+import pocketenv/ports
208208+import gleam/io
209209+import gleam/option.{None, Some}
210210+211211+pub fn main() {
212212+ let client = pocketenv.new_client("https://pocketenv.io", "your-token")
213213+ let sandbox_id = "sandbox-abc123"
214214+215215+ // Expose a port and get a preview URL
216216+ let assert Ok(preview_url) =
217217+ network.expose(client, sandbox_id, 3000, Some("Dev server"))
218218+ io.debug(preview_url)
219219+220220+ // List currently exposed ports
221221+ let assert Ok(exposed) = ports.list(client, sandbox_id)
222222+ io.debug(exposed)
223223+224224+ // Unexpose the port
225225+ let assert Ok(Nil) = network.unexpose(client, sandbox_id, 3000)
226226+227227+ // Configure Tailscale networking
228228+ let assert Ok(Nil) =
229229+ network.setup_tailscale(client, sandbox_id, "tskey-auth-xxxx")
230230+}
231231+```
232232+233233+### Profile
234234+235235+```gleam
236236+import pocketenv
237237+import gleam/io
238238+239239+pub fn main() {
240240+ let client = pocketenv.new_client("https://pocketenv.io", "your-token")
241241+ let assert Ok(profile) = pocketenv.get_profile(client)
242242+ io.println("Logged in as: " <> profile.handle)
243243+}
244244+```
245245+246246+## Error handling
247247+248248+All API functions return `Result(_, PocketenvError)`. The error variants are:
249249+250250+```gleam
251251+import pocketenv.{ApiError, HttpError, JsonDecodeError, RequestBuildError}
252252+253253+case pocketenv.get_profile(client) {
254254+ Ok(profile) -> io.println(profile.handle)
255255+ Error(ApiError(status)) -> io.println("API error: " <> int.to_string(status))
256256+ Error(HttpError(msg)) -> io.println("HTTP error: " <> msg)
257257+ Error(JsonDecodeError(_)) -> io.println("Failed to decode response")
258258+ Error(RequestBuildError) -> io.println("Could not build request URL")
259259+}
260260+```
261261+262262+Further documentation can be found at <https://hexdocs.pm/pocketenv>.
263263+264264+## Development
265265+266266+```sh
267267+gleam run # Run the project
268268+gleam test # Run the tests
269269+```
+22
gleam.toml
···11+name = "pocketenv"
22+version = "1.0.0"
33+44+# Fill out these fields if you intend to generate HTML documentation or publish
55+# your project to the Hex package manager.
66+#
77+# description = ""
88+# licences = ["Apache-2.0"]
99+# repository = { type = "github", user = "", repo = "" }
1010+# links = [{ title = "Website", href = "" }]
1111+#
1212+# For a full reference of all the available options, you can have a look at
1313+# https://gleam.run/writing-gleam/gleam-toml/.
1414+1515+[dependencies]
1616+gleam_stdlib = ">= 0.44.0 and < 2.0.0"
1717+gleam_http = ">= 4.0.0 and < 5.0.0"
1818+gleam_httpc = ">= 5.0.0 and < 6.0.0"
1919+gleam_json = ">= 3.0.0 and < 4.0.0"
2020+2121+[dev-dependencies]
2222+gleeunit = ">= 1.0.0 and < 2.0.0"
+18
manifest.toml
···11+# This file was generated by Gleam
22+# You typically do not need to edit this file
33+44+packages = [
55+ { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
66+ { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
77+ { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" },
88+ { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
99+ { name = "gleam_stdlib", version = "0.70.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86949BF5D1F0E4AC0AB5B06F235D8A5CC11A2DFC33BF22F752156ED61CA7D0FF" },
1010+ { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
1111+]
1212+1313+[requirements]
1414+gleam_http = { version = ">= 4.0.0 and < 5.0.0" }
1515+gleam_httpc = { version = ">= 5.0.0 and < 6.0.0" }
1616+gleam_json = { version = ">= 3.0.0 and < 4.0.0" }
1717+gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
1818+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+183
src/pocketenv.gleam
···11+//// Core types and HTTP helpers for the Pocketenv API client.
22+////
33+//// Start by creating a `Client` with `new_client/2`, then pass it to any
44+//// of the sub-module functions (`sandbox`, `env`, `secrets`, `files`, etc.).
55+66+import gleam/dynamic/decode
77+import gleam/http
88+import gleam/http/request
99+import gleam/httpc
1010+import gleam/json
1111+import gleam/option.{type Option, None}
1212+import gleam/result
1313+import gleam/string
1414+1515+/// Holds the base URL and bearer token used for every API request.
1616+pub type Client {
1717+ Client(base_url: String, token: String)
1818+}
1919+2020+/// Errors that can be returned by any API call.
2121+pub type PocketenvError {
2222+ /// An HTTP-level error (connection failure, timeout, etc.).
2323+ HttpError(String)
2424+ /// The response body could not be decoded as expected JSON.
2525+ JsonDecodeError(json.DecodeError)
2626+ /// The server returned a non-2xx status code.
2727+ ApiError(Int)
2828+ /// The request URL could not be constructed (malformed base URL).
2929+ RequestBuildError
3030+}
3131+3232+/// Information about the authenticated actor (user/bot).
3333+pub type Profile {
3434+ Profile(
3535+ id: Option(String),
3636+ did: String,
3737+ handle: String,
3838+ display_name: Option(String),
3939+ avatar: Option(String),
4040+ created_at: Option(String),
4141+ updated_at: Option(String),
4242+ )
4343+}
4444+4545+/// The default base URL for the Pocketenv API.
4646+pub const default_base_url = "https://api.pocketenv.io"
4747+4848+/// Creates a new API client using the default base URL (`https://api.pocketenv.io`).
4949+///
5050+/// ## Example
5151+///
5252+/// ```gleam
5353+/// let client = pocketenv.new_client("your-token")
5454+/// ```
5555+pub fn new_client(token: String) -> Client {
5656+ Client(base_url: default_base_url, token: token)
5757+}
5858+5959+/// Creates a new API client with a custom base URL.
6060+///
6161+/// ## Example
6262+///
6363+/// ```gleam
6464+/// let client = pocketenv.new_client_with_base_url("https://self-hosted.example.com", "your-token")
6565+/// ```
6666+pub fn new_client_with_base_url(base_url: String, token: String) -> Client {
6767+ Client(base_url: base_url, token: token)
6868+}
6969+7070+/// Fetches the profile of the authenticated actor.
7171+///
7272+/// ## Example
7373+///
7474+/// ```gleam
7575+/// let assert Ok(profile) = pocketenv.get_profile(client)
7676+/// io.println(profile.handle)
7777+/// ```
7878+pub fn get_profile(client: Client) -> Result(Profile, PocketenvError) {
7979+ use body <- result.try(do_get(
8080+ client,
8181+ "/xrpc/io.pocketenv.actor.getProfile",
8282+ [],
8383+ ))
8484+ json.parse(body, profile_decoder())
8585+ |> result.map_error(JsonDecodeError)
8686+}
8787+8888+/// JSON decoder for `Profile`. Useful when embedding profile data in custom decoders.
8989+pub fn profile_decoder() -> decode.Decoder(Profile) {
9090+ use id <- decode.optional_field("id", None, decode.optional(decode.string))
9191+ use did <- decode.field("did", decode.string)
9292+ use handle <- decode.field("handle", decode.string)
9393+ use display_name <- decode.optional_field(
9494+ "displayName",
9595+ None,
9696+ decode.optional(decode.string),
9797+ )
9898+ use avatar <- decode.optional_field(
9999+ "avatar",
100100+ None,
101101+ decode.optional(decode.string),
102102+ )
103103+ use created_at <- decode.optional_field(
104104+ "createdAt",
105105+ None,
106106+ decode.optional(decode.string),
107107+ )
108108+ use updated_at <- decode.optional_field(
109109+ "updatedAt",
110110+ None,
111111+ decode.optional(decode.string),
112112+ )
113113+ decode.success(Profile(
114114+ id: id,
115115+ did: did,
116116+ handle: handle,
117117+ display_name: display_name,
118118+ avatar: avatar,
119119+ created_at: created_at,
120120+ updated_at: updated_at,
121121+ ))
122122+}
123123+124124+/// Sends an authenticated GET request to `path` with optional query params.
125125+/// Returns the raw response body on success.
126126+pub fn do_get(
127127+ client: Client,
128128+ path: String,
129129+ query: List(#(String, String)),
130130+) -> Result(String, PocketenvError) {
131131+ let url = client.base_url <> path
132132+ use req <- result.try(
133133+ request.to(url) |> result.replace_error(RequestBuildError),
134134+ )
135135+ let req =
136136+ req
137137+ |> request.set_method(http.Get)
138138+ |> request.prepend_header("authorization", "Bearer " <> client.token)
139139+ let req = case query {
140140+ [] -> req
141141+ q -> request.set_query(req, q)
142142+ }
143143+ use resp <- result.try(
144144+ httpc.send(req)
145145+ |> result.map_error(fn(e) { HttpError(string.inspect(e)) }),
146146+ )
147147+ case resp.status {
148148+ s if s >= 200 && s < 300 -> Ok(resp.body)
149149+ s -> Error(ApiError(s))
150150+ }
151151+}
152152+153153+/// Sends an authenticated POST request to `path` with an optional query and a
154154+/// JSON `body`. Returns the raw response body on success.
155155+pub fn do_post(
156156+ client: Client,
157157+ path: String,
158158+ query: List(#(String, String)),
159159+ body: String,
160160+) -> Result(String, PocketenvError) {
161161+ let url = client.base_url <> path
162162+ use req <- result.try(
163163+ request.to(url) |> result.replace_error(RequestBuildError),
164164+ )
165165+ let req =
166166+ req
167167+ |> request.set_method(http.Post)
168168+ |> request.prepend_header("authorization", "Bearer " <> client.token)
169169+ |> request.prepend_header("content-type", "application/json")
170170+ |> request.set_body(body)
171171+ let req = case query {
172172+ [] -> req
173173+ q -> request.set_query(req, q)
174174+ }
175175+ use resp <- result.try(
176176+ httpc.send(req)
177177+ |> result.map_error(fn(e) { HttpError(string.inspect(e)) }),
178178+ )
179179+ case resp.status {
180180+ s if s >= 200 && s < 300 -> Ok(resp.body)
181181+ s -> Error(ApiError(s))
182182+ }
183183+}
+107
src/pocketenv/env.gleam
···11+//// Manage environment variables attached to a sandbox.
22+////
33+//// Variables are plain-text key/value pairs injected into the sandbox at
44+//// runtime. For sensitive data prefer `pocketenv/secrets`.
55+66+import gleam/dynamic/decode
77+import gleam/int
88+import gleam/json
99+import gleam/list
1010+import gleam/option.{type Option, None, Some}
1111+import gleam/result
1212+import pocketenv.{
1313+ type Client, type PocketenvError, JsonDecodeError, do_get, do_post,
1414+}
1515+1616+/// A single environment variable stored in a sandbox.
1717+pub type Variable {
1818+ Variable(id: String, name: String, value: String, created_at: String)
1919+}
2020+2121+/// Lists environment variables for `sandbox_id`.
2222+/// Optionally paginate with `limit` and `offset`.
2323+///
2424+/// ## Example
2525+///
2626+/// ```gleam
2727+/// let assert Ok(vars) = env.list(client, sandbox_id, None, None)
2828+/// ```
2929+pub fn list(
3030+ client: Client,
3131+ sandbox_id: String,
3232+ limit: Option(Int),
3333+ offset: Option(Int),
3434+) -> Result(List(Variable), PocketenvError) {
3535+ let query = [#("sandboxId", sandbox_id)]
3636+ let query = case limit {
3737+ Some(l) -> list.append(query, [#("limit", int.to_string(l))])
3838+ None -> query
3939+ }
4040+ let query = case offset {
4141+ Some(o) -> list.append(query, [#("offset", int.to_string(o))])
4242+ None -> query
4343+ }
4444+ use body <- result.try(do_get(
4545+ client,
4646+ "/xrpc/io.pocketenv.variable.getVariables",
4747+ query,
4848+ ))
4949+ json.parse(body, {
5050+ use variables <- decode.field("variables", decode.list(variable_decoder()))
5151+ decode.success(variables)
5252+ })
5353+ |> result.map_error(JsonDecodeError)
5454+}
5555+5656+/// Creates or updates an environment variable named `name` with `value` in `sandbox_id`.
5757+///
5858+/// ## Example
5959+///
6060+/// ```gleam
6161+/// let assert Ok(Nil) = env.put(client, sandbox_id, "PORT", "8080")
6262+/// ```
6363+pub fn put(
6464+ client: Client,
6565+ sandbox_id: String,
6666+ name: String,
6767+ value: String,
6868+) -> Result(Nil, PocketenvError) {
6969+ let body =
7070+ json.to_string(json.object([
7171+ #(
7272+ "variable",
7373+ json.object([
7474+ #("sandboxId", json.string(sandbox_id)),
7575+ #("name", json.string(name)),
7676+ #("value", json.string(value)),
7777+ ]),
7878+ ),
7979+ ]))
8080+ use _ <- result.try(do_post(
8181+ client,
8282+ "/xrpc/io.pocketenv.variable.addVariable",
8383+ [],
8484+ body,
8585+ ))
8686+ Ok(Nil)
8787+}
8888+8989+/// Deletes the environment variable identified by `id`.
9090+pub fn delete(client: Client, id: String) -> Result(Nil, PocketenvError) {
9191+ use _ <- result.try(do_post(
9292+ client,
9393+ "/xrpc/io.pocketenv.variable.deleteVariable",
9494+ [#("id", id)],
9595+ "{}",
9696+ ))
9797+ Ok(Nil)
9898+}
9999+100100+/// JSON decoder for `Variable`.
101101+pub fn variable_decoder() -> decode.Decoder(Variable) {
102102+ use id <- decode.field("id", decode.string)
103103+ use name <- decode.field("name", decode.string)
104104+ use value <- decode.field("value", decode.string)
105105+ use created_at <- decode.field("createdAt", decode.string)
106106+ decode.success(Variable(id: id, name: name, value: value, created_at: created_at))
107107+}
+90
src/pocketenv/files.gleam
···11+//// Manage files stored inside a sandbox.
22+////
33+//// Files are written into the sandbox filesystem and can be listed or deleted.
44+55+import gleam/dynamic/decode
66+import gleam/json
77+import gleam/result
88+import pocketenv.{
99+ type Client, type PocketenvError, JsonDecodeError, do_get, do_post,
1010+}
1111+1212+/// Metadata for a file stored in a sandbox.
1313+pub type File {
1414+ File(id: String, path: String, created_at: String)
1515+}
1616+1717+/// Lists all files in `sandbox_id`.
1818+///
1919+/// ## Example
2020+///
2121+/// ```gleam
2222+/// let assert Ok(files) = files.list(client, sandbox_id)
2323+/// ```
2424+pub fn list(
2525+ client: Client,
2626+ sandbox_id: String,
2727+) -> Result(List(File), PocketenvError) {
2828+ use body <- result.try(do_get(
2929+ client,
3030+ "/xrpc/io.pocketenv.file.getFiles",
3131+ [#("sandboxId", sandbox_id)],
3232+ ))
3333+ json.parse(body, {
3434+ use files <- decode.field("files", decode.list(file_decoder()))
3535+ decode.success(files)
3636+ })
3737+ |> result.map_error(JsonDecodeError)
3838+}
3939+4040+/// Writes (or overwrites) a file at `path` with `content` in `sandbox_id`.
4141+///
4242+/// ## Example
4343+///
4444+/// ```gleam
4545+/// let assert Ok(Nil) = files.write(client, sandbox_id, "/app/.env", "PORT=8080\n")
4646+/// ```
4747+pub fn write(
4848+ client: Client,
4949+ sandbox_id: String,
5050+ path: String,
5151+ content: String,
5252+) -> Result(Nil, PocketenvError) {
5353+ let body =
5454+ json.to_string(json.object([
5555+ #(
5656+ "file",
5757+ json.object([
5858+ #("sandboxId", json.string(sandbox_id)),
5959+ #("path", json.string(path)),
6060+ #("content", json.string(content)),
6161+ ]),
6262+ ),
6363+ ]))
6464+ use _ <- result.try(do_post(
6565+ client,
6666+ "/xrpc/io.pocketenv.file.addFile",
6767+ [],
6868+ body,
6969+ ))
7070+ Ok(Nil)
7171+}
7272+7373+/// Deletes the file identified by `id`.
7474+pub fn delete(client: Client, id: String) -> Result(Nil, PocketenvError) {
7575+ use _ <- result.try(do_post(
7676+ client,
7777+ "/xrpc/io.pocketenv.file.deleteFile",
7878+ [#("id", id)],
7979+ "{}",
8080+ ))
8181+ Ok(Nil)
8282+}
8383+8484+/// JSON decoder for `File`.
8585+pub fn file_decoder() -> decode.Decoder(File) {
8686+ use id <- decode.field("id", decode.string)
8787+ use path <- decode.field("path", decode.string)
8888+ use created_at <- decode.field("createdAt", decode.string)
8989+ decode.success(File(id: id, path: path, created_at: created_at))
9090+}
+116
src/pocketenv/network.gleam
···11+//// Manage network access for a sandbox.
22+////
33+//// Expose or unexpose ports to the internet and configure Tailscale for
44+//// private network connectivity.
55+66+import gleam/dynamic/decode
77+import gleam/json
88+import gleam/option.{type Option, None, Some}
99+import gleam/result
1010+import pocketenv.{
1111+ type Client, type PocketenvError, JsonDecodeError, do_get, do_post,
1212+}
1313+1414+/// Exposes `port` on `sandbox_id` to the internet.
1515+/// Returns a preview URL if one is generated by the platform.
1616+///
1717+/// ## Example
1818+///
1919+/// ```gleam
2020+/// let assert Ok(url) = network.expose(client, sandbox_id, 3000, Some("Dev server"))
2121+/// io.debug(url) // Some("https://...")
2222+/// ```
2323+pub fn expose(
2424+ client: Client,
2525+ sandbox_id: String,
2626+ port: Int,
2727+ description: Option(String),
2828+) -> Result(Option(String), PocketenvError) {
2929+ let fields = [#("port", json.int(port))]
3030+ let fields = case description {
3131+ Some(d) -> [#("description", json.string(d)), ..fields]
3232+ None -> fields
3333+ }
3434+ let body = json.to_string(json.object(fields))
3535+ use resp_body <- result.try(do_post(
3636+ client,
3737+ "/xrpc/io.pocketenv.sandbox.exposePort",
3838+ [#("id", sandbox_id)],
3939+ body,
4040+ ))
4141+ json.parse(resp_body, {
4242+ use preview_url <- decode.optional_field(
4343+ "previewUrl",
4444+ None,
4545+ decode.optional(decode.string),
4646+ )
4747+ decode.success(preview_url)
4848+ })
4949+ |> result.map_error(JsonDecodeError)
5050+}
5151+5252+/// Removes the public exposure of `port` on `sandbox_id`.
5353+///
5454+/// ## Example
5555+///
5656+/// ```gleam
5757+/// let assert Ok(Nil) = network.unexpose(client, sandbox_id, 3000)
5858+/// ```
5959+pub fn unexpose(
6060+ client: Client,
6161+ sandbox_id: String,
6262+ port: Int,
6363+) -> Result(Nil, PocketenvError) {
6464+ let body = json.to_string(json.object([#("port", json.int(port))]))
6565+ use _ <- result.try(do_post(
6666+ client,
6767+ "/xrpc/io.pocketenv.sandbox.unexposePort",
6868+ [#("id", sandbox_id)],
6969+ body,
7070+ ))
7171+ Ok(Nil)
7272+}
7373+7474+/// Stores a Tailscale auth key for `sandbox_id`, enabling private network access.
7575+///
7676+/// ## Example
7777+///
7878+/// ```gleam
7979+/// let assert Ok(Nil) = network.setup_tailscale(client, sandbox_id, "tskey-auth-xxxx")
8080+/// ```
8181+pub fn setup_tailscale(
8282+ client: Client,
8383+ sandbox_id: String,
8484+ auth_key: String,
8585+) -> Result(Nil, PocketenvError) {
8686+ let body =
8787+ json.to_string(json.object([#("tailscaleAuthKey", json.string(auth_key))]))
8888+ use _ <- result.try(do_post(
8989+ client,
9090+ "/xrpc/io.pocketenv.sandbox.putTailscaleAuthKey",
9191+ [#("id", sandbox_id)],
9292+ body,
9393+ ))
9494+ Ok(Nil)
9595+}
9696+9797+/// Retrieves the stored Tailscale auth key for `sandbox_id`, if any.
9898+pub fn get_tailscale_auth_key(
9999+ client: Client,
100100+ sandbox_id: String,
101101+) -> Result(Option(String), PocketenvError) {
102102+ use body <- result.try(do_get(
103103+ client,
104104+ "/xrpc/io.pocketenv.sandbox.getTailscaleAuthKey",
105105+ [#("id", sandbox_id)],
106106+ ))
107107+ json.parse(body, {
108108+ use key <- decode.optional_field(
109109+ "tailscaleAuthKey",
110110+ None,
111111+ decode.optional(decode.string),
112112+ )
113113+ decode.success(key)
114114+ })
115115+ |> result.map_error(JsonDecodeError)
116116+}
+53
src/pocketenv/ports.gleam
···11+//// Query exposed ports of a sandbox.
22+////
33+//// To expose or unexpose ports use `pocketenv/network`.
44+55+import gleam/dynamic/decode
66+import gleam/json
77+import gleam/option.{type Option, None}
88+import gleam/result
99+import pocketenv.{type Client, type PocketenvError, JsonDecodeError, do_get}
1010+1111+/// An exposed port on a sandbox.
1212+pub type Port {
1313+ Port(port: Int, description: Option(String), preview_url: Option(String))
1414+}
1515+1616+/// Lists all currently exposed ports for `sandbox_id`.
1717+///
1818+/// ## Example
1919+///
2020+/// ```gleam
2121+/// let assert Ok(ports) = ports.list(client, sandbox_id)
2222+/// ```
2323+pub fn list(
2424+ client: Client,
2525+ sandbox_id: String,
2626+) -> Result(List(Port), PocketenvError) {
2727+ use body <- result.try(do_get(
2828+ client,
2929+ "/xrpc/io.pocketenv.sandbox.getExposedPorts",
3030+ [#("id", sandbox_id)],
3131+ ))
3232+ json.parse(body, {
3333+ use ports <- decode.field("ports", decode.list(port_decoder()))
3434+ decode.success(ports)
3535+ })
3636+ |> result.map_error(JsonDecodeError)
3737+}
3838+3939+/// JSON decoder for `Port`.
4040+pub fn port_decoder() -> decode.Decoder(Port) {
4141+ use port <- decode.field("port", decode.int)
4242+ use description <- decode.optional_field(
4343+ "description",
4444+ None,
4545+ decode.optional(decode.string),
4646+ )
4747+ use preview_url <- decode.optional_field(
4848+ "previewUrl",
4949+ None,
5050+ decode.optional(decode.string),
5151+ )
5252+ decode.success(Port(port: port, description: description, preview_url: preview_url))
5353+}
+463
src/pocketenv/sandbox.gleam
···11+//// Create, manage, and interact with Pocketenv sandboxes.
22+////
33+//// A sandbox is an isolated cloud environment that can run commands, expose
44+//// ports, and host services. All other resources (env vars, secrets, files,
55+//// volumes, services) are attached to a sandbox.
66+77+import gleam/dynamic/decode
88+import gleam/int
99+import gleam/json
1010+import gleam/list
1111+import gleam/option.{type Option, None, Some}
1212+import gleam/result
1313+import pocketenv.{
1414+ type Client, type PocketenvError, JsonDecodeError, do_get, do_post,
1515+}
1616+1717+/// Full details of a Pocketenv sandbox.
1818+pub type Sandbox {
1919+ Sandbox(
2020+ id: String,
2121+ name: String,
2222+ provider: Option(String),
2323+ base_sandbox: Option(String),
2424+ display_name: Option(String),
2525+ uri: Option(String),
2626+ description: Option(String),
2727+ topics: Option(List(String)),
2828+ logo: Option(String),
2929+ readme: Option(String),
3030+ repo: Option(String),
3131+ vcpus: Option(Int),
3232+ memory: Option(Int),
3333+ disk: Option(Int),
3434+ installs: Option(Int),
3535+ status: Option(String),
3636+ started_at: Option(String),
3737+ created_at: String,
3838+ )
3939+}
4040+4141+/// A sandbox bundled with its client — returned by `create` and `connect`.
4242+/// All operations (`start`, `stop`, `exec`, …) work directly on this type.
4343+pub type ConnectedSandbox {
4444+ ConnectedSandbox(data: Sandbox, client: Client)
4545+}
4646+4747+/// The output of a command executed inside a sandbox.
4848+pub type ExecResult {
4949+ ExecResult(stdout: String, stderr: String, exit_code: Int)
5050+}
5151+5252+/// A builder for configuring a new sandbox before calling `create`.
5353+pub opaque type SandboxBuilder {
5454+ SandboxBuilder(
5555+ client: Client,
5656+ name: String,
5757+ base: String,
5858+ provider: String,
5959+ repo: Option(String),
6060+ description: Option(String),
6161+ )
6262+}
6363+6464+/// Starts a sandbox builder with the three required fields.
6565+/// Chain optional `with_*` setters then call `create()`.
6666+///
6767+/// ## Example
6868+///
6969+/// ```gleam
7070+/// let assert Ok(sb) =
7171+/// client
7272+/// |> sandbox.new("my-app", "openclaw", "cloudflare")
7373+/// |> sandbox.with_description("My app sandbox")
7474+/// |> sandbox.create()
7575+/// ```
7676+pub fn new(
7777+ client: Client,
7878+ name: String,
7979+ base: String,
8080+ provider: String,
8181+) -> SandboxBuilder {
8282+ SandboxBuilder(
8383+ client: client,
8484+ name: name,
8585+ base: base,
8686+ provider: provider,
8787+ repo: None,
8888+ description: None,
8989+ )
9090+}
9191+9292+/// Sets the Git repo URL to clone when the sandbox starts.
9393+pub fn with_repo(builder: SandboxBuilder, repo: String) -> SandboxBuilder {
9494+ SandboxBuilder(..builder, repo: Some(repo))
9595+}
9696+9797+/// Sets a human-readable description for the sandbox.
9898+pub fn with_description(
9999+ builder: SandboxBuilder,
100100+ description: String,
101101+) -> SandboxBuilder {
102102+ SandboxBuilder(..builder, description: Some(description))
103103+}
104104+105105+/// Creates the sandbox and returns a `ConnectedSandbox` ready for use.
106106+///
107107+/// ## Example
108108+///
109109+/// ```gleam
110110+/// let assert Ok(sb) =
111111+/// client
112112+/// |> sandbox.new("my-app", "openclaw", "cloudflare")
113113+/// |> sandbox.create()
114114+///
115115+/// sb |> sandbox.start(None, None)
116116+/// sb |> sandbox.exec("echo hello")
117117+/// sb |> sandbox.stop()
118118+/// ```
119119+pub fn create(
120120+ builder: SandboxBuilder,
121121+) -> Result(ConnectedSandbox, PocketenvError) {
122122+ let fields = [
123123+ #("name", json.string(builder.name)),
124124+ #("base", json.string(builder.base)),
125125+ #("provider", json.string(builder.provider)),
126126+ ]
127127+ let fields = case builder.repo {
128128+ Some(r) -> list.append(fields, [#("repo", json.string(r))])
129129+ None -> fields
130130+ }
131131+ let fields = case builder.description {
132132+ Some(d) -> list.append(fields, [#("description", json.string(d))])
133133+ None -> fields
134134+ }
135135+ let body = json.to_string(json.object(fields))
136136+ use resp_body <- result.try(do_post(
137137+ builder.client,
138138+ "/xrpc/io.pocketenv.sandbox.createSandbox",
139139+ [],
140140+ body,
141141+ ))
142142+ use sb <- result.try(
143143+ json.parse(resp_body, sandbox_decoder())
144144+ |> result.map_error(JsonDecodeError),
145145+ )
146146+ Ok(ConnectedSandbox(data: sb, client: builder.client))
147147+}
148148+149149+/// Wraps a `Sandbox` obtained from `get`/`list` with a client, producing a
150150+/// `ConnectedSandbox` that can be passed to `start`, `stop`, `exec`, etc.
151151+///
152152+/// ## Example
153153+///
154154+/// ```gleam
155155+/// let assert Ok(#(sandboxes, _)) = sandbox.list(client, None, None)
156156+/// let assert [first, ..] = sandboxes
157157+/// let sb = first |> sandbox.connect(client)
158158+/// sb |> sandbox.exec("ls")
159159+/// ```
160160+pub fn connect(sandbox: Sandbox, client: Client) -> ConnectedSandbox {
161161+ ConnectedSandbox(data: sandbox, client: client)
162162+}
163163+164164+/// Fetches a single sandbox by `id`. Returns `None` if not found.
165165+///
166166+/// ## Example
167167+///
168168+/// ```gleam
169169+/// let assert Ok(Some(sb)) = sandbox.get(client, "sandbox-abc123")
170170+/// ```
171171+pub fn get(
172172+ client: Client,
173173+ id: String,
174174+) -> Result(Option(Sandbox), PocketenvError) {
175175+ use body <- result.try(
176176+ do_get(client, "/xrpc/io.pocketenv.sandbox.getSandbox", [#("id", id)]),
177177+ )
178178+ json.parse(body, {
179179+ use sandbox <- decode.optional_field(
180180+ "sandbox",
181181+ None,
182182+ decode.optional(sandbox_decoder()),
183183+ )
184184+ decode.success(sandbox)
185185+ })
186186+ |> result.map_error(JsonDecodeError)
187187+}
188188+189189+/// Lists sandboxes visible to the authenticated actor.
190190+/// Returns a tuple of `#(sandboxes, total_count)`.
191191+/// Optionally paginate with `limit` and `offset`.
192192+///
193193+/// ## Example
194194+///
195195+/// ```gleam
196196+/// let assert Ok(#(sandboxes, total)) = sandbox.list(client, Some(10), None)
197197+/// ```
198198+pub fn list(
199199+ client: Client,
200200+ limit: Option(Int),
201201+ offset: Option(Int),
202202+) -> Result(#(List(Sandbox), Int), PocketenvError) {
203203+ let query = []
204204+ let query = case limit {
205205+ Some(l) -> list.append(query, [#("limit", int.to_string(l))])
206206+ None -> query
207207+ }
208208+ let query = case offset {
209209+ Some(o) -> list.append(query, [#("offset", int.to_string(o))])
210210+ None -> query
211211+ }
212212+ use body <- result.try(do_get(
213213+ client,
214214+ "/xrpc/io.pocketenv.sandbox.getSandboxes",
215215+ query,
216216+ ))
217217+ json.parse(body, {
218218+ use sandboxes <- decode.field("sandboxes", decode.list(sandbox_decoder()))
219219+ use total <- decode.optional_field(
220220+ "total",
221221+ None,
222222+ decode.optional(decode.int),
223223+ )
224224+ decode.success(#(sandboxes, option.unwrap(total, 0)))
225225+ })
226226+ |> result.map_error(JsonDecodeError)
227227+}
228228+229229+/// Lists sandboxes owned by the actor identified by `did`.
230230+pub fn get_actor_sandboxes(
231231+ client: Client,
232232+ did: String,
233233+ limit: Option(Int),
234234+ offset: Option(Int),
235235+) -> Result(List(Sandbox), PocketenvError) {
236236+ let query = [#("did", did)]
237237+ let query = case limit {
238238+ Some(l) -> list.append(query, [#("limit", int.to_string(l))])
239239+ None -> query
240240+ }
241241+ let query = case offset {
242242+ Some(o) -> list.append(query, [#("offset", int.to_string(o))])
243243+ None -> query
244244+ }
245245+ use body <- result.try(do_get(
246246+ client,
247247+ "/xrpc/io.pocketenv.actor.getActorSandboxes",
248248+ query,
249249+ ))
250250+ json.parse(body, {
251251+ use sandboxes <- decode.field("sandboxes", decode.list(sandbox_decoder()))
252252+ decode.success(sandboxes)
253253+ })
254254+ |> result.map_error(JsonDecodeError)
255255+}
256256+257257+/// Starts the sandbox.
258258+/// Optionally clone a `repo` on start and keep the sandbox alive with `keep_alive`.
259259+///
260260+/// ## Example
261261+///
262262+/// ```gleam
263263+/// sb |> sandbox.start(None, Some(True))
264264+/// ```
265265+pub fn start(
266266+ sb: ConnectedSandbox,
267267+ repo: Option(String),
268268+ keep_alive: Option(Bool),
269269+) -> Result(Nil, PocketenvError) {
270270+ let fields = case repo {
271271+ Some(r) -> [#("repo", json.string(r))]
272272+ None -> []
273273+ }
274274+ let fields = case keep_alive {
275275+ Some(k) -> list.append(fields, [#("keepAlive", json.bool(k))])
276276+ None -> fields
277277+ }
278278+ let body = json.to_string(json.object(fields))
279279+ use _ <- result.try(do_post(
280280+ sb.client,
281281+ "/xrpc/io.pocketenv.sandbox.startSandbox",
282282+ [#("id", sb.data.id)],
283283+ body,
284284+ ))
285285+ Ok(Nil)
286286+}
287287+288288+/// Stops the sandbox.
289289+///
290290+/// ## Example
291291+///
292292+/// ```gleam
293293+/// sb |> sandbox.stop()
294294+/// ```
295295+pub fn stop(sb: ConnectedSandbox) -> Result(Nil, PocketenvError) {
296296+ use _ <- result.try(do_post(
297297+ sb.client,
298298+ "/xrpc/io.pocketenv.sandbox.stopSandbox",
299299+ [#("id", sb.data.id)],
300300+ "{}",
301301+ ))
302302+ Ok(Nil)
303303+}
304304+305305+/// Permanently deletes the sandbox.
306306+///
307307+/// ## Example
308308+///
309309+/// ```gleam
310310+/// sb |> sandbox.delete()
311311+/// ```
312312+pub fn delete(sb: ConnectedSandbox) -> Result(Nil, PocketenvError) {
313313+ use _ <- result.try(do_post(
314314+ sb.client,
315315+ "/xrpc/io.pocketenv.sandbox.deleteSandbox",
316316+ [#("id", sb.data.id)],
317317+ "{}",
318318+ ))
319319+ Ok(Nil)
320320+}
321321+322322+/// Executes a shell `command` inside the running sandbox.
323323+/// Returns stdout, stderr, and the exit code.
324324+///
325325+/// ## Example
326326+///
327327+/// ```gleam
328328+/// let assert Ok(res) = sb |> sandbox.exec("ls /app")
329329+/// io.println(res.stdout)
330330+/// ```
331331+pub fn exec(
332332+ sb: ConnectedSandbox,
333333+ command: String,
334334+) -> Result(ExecResult, PocketenvError) {
335335+ let body = json.to_string(json.object([#("command", json.string(command))]))
336336+ use resp_body <- result.try(do_post(
337337+ sb.client,
338338+ "/xrpc/io.pocketenv.sandbox.exec",
339339+ [#("id", sb.data.id)],
340340+ body,
341341+ ))
342342+ json.parse(resp_body, exec_decoder())
343343+ |> result.map_error(JsonDecodeError)
344344+}
345345+346346+/// Exposes a VS Code server on the sandbox.
347347+///
348348+/// ## Example
349349+///
350350+/// ```gleam
351351+/// sb |> sandbox.expose_vscode()
352352+/// ```
353353+pub fn expose_vscode(sb: ConnectedSandbox) -> Result(Nil, PocketenvError) {
354354+ use _ <- result.try(do_post(
355355+ sb.client,
356356+ "/xrpc/io.pocketenv.sandbox.exposeVscode",
357357+ [#("id", sb.data.id)],
358358+ "{}",
359359+ ))
360360+ Ok(Nil)
361361+}
362362+363363+/// JSON decoder for `Sandbox`.
364364+pub fn sandbox_decoder() -> decode.Decoder(Sandbox) {
365365+ use id <- decode.field("id", decode.string)
366366+ use name <- decode.field("name", decode.string)
367367+ use provider <- decode.optional_field(
368368+ "provider",
369369+ None,
370370+ decode.optional(decode.string),
371371+ )
372372+ use base_sandbox <- decode.optional_field(
373373+ "baseSandbox",
374374+ None,
375375+ decode.optional(decode.string),
376376+ )
377377+ use display_name <- decode.optional_field(
378378+ "displayName",
379379+ None,
380380+ decode.optional(decode.string),
381381+ )
382382+ use uri <- decode.optional_field("uri", None, decode.optional(decode.string))
383383+ use description <- decode.optional_field(
384384+ "description",
385385+ None,
386386+ decode.optional(decode.string),
387387+ )
388388+ use topics <- decode.optional_field(
389389+ "topics",
390390+ None,
391391+ decode.optional(decode.list(decode.string)),
392392+ )
393393+ use logo <- decode.optional_field(
394394+ "logo",
395395+ None,
396396+ decode.optional(decode.string),
397397+ )
398398+ use readme <- decode.optional_field(
399399+ "readme",
400400+ None,
401401+ decode.optional(decode.string),
402402+ )
403403+ use repo <- decode.optional_field(
404404+ "repo",
405405+ None,
406406+ decode.optional(decode.string),
407407+ )
408408+ use vcpus <- decode.optional_field("vcpus", None, decode.optional(decode.int))
409409+ use memory <- decode.optional_field(
410410+ "memory",
411411+ None,
412412+ decode.optional(decode.int),
413413+ )
414414+ use disk <- decode.optional_field("disk", None, decode.optional(decode.int))
415415+ use installs <- decode.optional_field(
416416+ "installs",
417417+ None,
418418+ decode.optional(decode.int),
419419+ )
420420+ use status <- decode.optional_field(
421421+ "status",
422422+ None,
423423+ decode.optional(decode.string),
424424+ )
425425+ use started_at <- decode.optional_field(
426426+ "startedAt",
427427+ None,
428428+ decode.optional(decode.string),
429429+ )
430430+ use created_at <- decode.field("createdAt", decode.string)
431431+ decode.success(Sandbox(
432432+ id: id,
433433+ name: name,
434434+ provider: provider,
435435+ base_sandbox: base_sandbox,
436436+ display_name: display_name,
437437+ uri: uri,
438438+ description: description,
439439+ topics: topics,
440440+ logo: logo,
441441+ readme: readme,
442442+ repo: repo,
443443+ vcpus: vcpus,
444444+ memory: memory,
445445+ disk: disk,
446446+ installs: installs,
447447+ status: status,
448448+ started_at: started_at,
449449+ created_at: created_at,
450450+ ))
451451+}
452452+453453+/// JSON decoder for `ExecResult`.
454454+pub fn exec_decoder() -> decode.Decoder(ExecResult) {
455455+ use stdout <- decode.field("stdout", decode.string)
456456+ use stderr <- decode.field("stderr", decode.string)
457457+ use exit_code <- decode.field("exitCode", decode.int)
458458+ decode.success(ExecResult(
459459+ stdout: stdout,
460460+ stderr: stderr,
461461+ exit_code: exit_code,
462462+ ))
463463+}
+106
src/pocketenv/secrets.gleam
···11+//// Manage encrypted secrets attached to a sandbox.
22+////
33+//// Secrets are similar to environment variables but their values are write-only:
44+//// the API never returns the stored value. Use them for API keys, passwords, and
55+//// other sensitive data.
66+77+import gleam/dynamic/decode
88+import gleam/int
99+import gleam/json
1010+import gleam/list
1111+import gleam/option.{type Option, None, Some}
1212+import gleam/result
1313+import pocketenv.{
1414+ type Client, type PocketenvError, JsonDecodeError, do_get, do_post,
1515+}
1616+1717+/// A secret stored in a sandbox. Only the `name` is exposed; the value is never returned.
1818+pub type Secret {
1919+ Secret(id: String, name: String, created_at: String)
2020+}
2121+2222+/// Lists secret names for `sandbox_id`. Optionally paginate with `limit` and `offset`.
2323+///
2424+/// ## Example
2525+///
2626+/// ```gleam
2727+/// let assert Ok(secrets) = secrets.list(client, sandbox_id, None, None)
2828+/// ```
2929+pub fn list(
3030+ client: Client,
3131+ sandbox_id: String,
3232+ limit: Option(Int),
3333+ offset: Option(Int),
3434+) -> Result(List(Secret), PocketenvError) {
3535+ let query = [#("sandboxId", sandbox_id)]
3636+ let query = case limit {
3737+ Some(l) -> list.append(query, [#("limit", int.to_string(l))])
3838+ None -> query
3939+ }
4040+ let query = case offset {
4141+ Some(o) -> list.append(query, [#("offset", int.to_string(o))])
4242+ None -> query
4343+ }
4444+ use body <- result.try(do_get(
4545+ client,
4646+ "/xrpc/io.pocketenv.secret.getSecrets",
4747+ query,
4848+ ))
4949+ json.parse(body, {
5050+ use secrets <- decode.field("secrets", decode.list(secret_decoder()))
5151+ decode.success(secrets)
5252+ })
5353+ |> result.map_error(JsonDecodeError)
5454+}
5555+5656+/// Creates or updates a secret named `name` with `value` in `sandbox_id`.
5757+///
5858+/// ## Example
5959+///
6060+/// ```gleam
6161+/// let assert Ok(Nil) = secrets.put(client, sandbox_id, "DB_PASSWORD", "s3cr3t")
6262+/// ```
6363+pub fn put(
6464+ client: Client,
6565+ sandbox_id: String,
6666+ name: String,
6767+ value: String,
6868+) -> Result(Nil, PocketenvError) {
6969+ let body =
7070+ json.to_string(json.object([
7171+ #(
7272+ "secret",
7373+ json.object([
7474+ #("sandboxId", json.string(sandbox_id)),
7575+ #("name", json.string(name)),
7676+ #("value", json.string(value)),
7777+ ]),
7878+ ),
7979+ ]))
8080+ use _ <- result.try(do_post(
8181+ client,
8282+ "/xrpc/io.pocketenv.secret.addSecret",
8383+ [],
8484+ body,
8585+ ))
8686+ Ok(Nil)
8787+}
8888+8989+/// Deletes the secret identified by `id`.
9090+pub fn delete(client: Client, id: String) -> Result(Nil, PocketenvError) {
9191+ use _ <- result.try(do_post(
9292+ client,
9393+ "/xrpc/io.pocketenv.secret.deleteSecret",
9494+ [#("id", id)],
9595+ "{}",
9696+ ))
9797+ Ok(Nil)
9898+}
9999+100100+/// JSON decoder for `Secret`.
101101+pub fn secret_decoder() -> decode.Decoder(Secret) {
102102+ use id <- decode.field("id", decode.string)
103103+ use name <- decode.field("name", decode.string)
104104+ use created_at <- decode.field("createdAt", decode.string)
105105+ decode.success(Secret(id: id, name: name, created_at: created_at))
106106+}
+168
src/pocketenv/services.gleam
···11+//// Manage long-running services inside a sandbox.
22+////
33+//// A service is a named process (e.g. a web server or background worker) that
44+//// the platform can start, stop, and restart independently.
55+66+import gleam/dynamic/decode
77+import gleam/json
88+import gleam/list
99+import gleam/option.{type Option, None, Some}
1010+import gleam/result
1111+import pocketenv.{
1212+ type Client, type PocketenvError, JsonDecodeError, do_get, do_post,
1313+}
1414+1515+/// A service running (or registered to run) inside a sandbox.
1616+pub type Service {
1717+ Service(
1818+ id: String,
1919+ name: String,
2020+ command: String,
2121+ ports: Option(List(Int)),
2222+ description: Option(String),
2323+ status: String,
2424+ created_at: String,
2525+ )
2626+}
2727+2828+/// Lists all services registered in `sandbox_id`.
2929+///
3030+/// ## Example
3131+///
3232+/// ```gleam
3333+/// let assert Ok(svcs) = services.list(client, sandbox_id)
3434+/// ```
3535+pub fn list(
3636+ client: Client,
3737+ sandbox_id: String,
3838+) -> Result(List(Service), PocketenvError) {
3939+ use body <- result.try(do_get(
4040+ client,
4141+ "/xrpc/io.pocketenv.service.getServices",
4242+ [#("sandboxId", sandbox_id)],
4343+ ))
4444+ json.parse(body, {
4545+ use services <- decode.field("services", decode.list(service_decoder()))
4646+ decode.success(services)
4747+ })
4848+ |> result.map_error(JsonDecodeError)
4949+}
5050+5151+/// Registers a new service in `sandbox_id`.
5252+///
5353+/// - `name` — unique name for the service
5454+/// - `command` — shell command to run
5555+/// - `ports` — optional list of ports the service listens on
5656+/// - `description` — optional human-readable description
5757+///
5858+/// ## Example
5959+///
6060+/// ```gleam
6161+/// let assert Ok(Nil) =
6262+/// services.create(client, sandbox_id, "api", "node server.js", Some([3000]), None)
6363+/// ```
6464+pub fn create(
6565+ client: Client,
6666+ sandbox_id: String,
6767+ name: String,
6868+ command: String,
6969+ ports: Option(List(Int)),
7070+ description: Option(String),
7171+) -> Result(Nil, PocketenvError) {
7272+ let service_fields = [
7373+ #("name", json.string(name)),
7474+ #("command", json.string(command)),
7575+ ]
7676+ let service_fields = case ports {
7777+ Some(ps) ->
7878+ list.append(service_fields, [#("ports", json.array(ps, json.int))])
7979+ None -> service_fields
8080+ }
8181+ let service_fields = case description {
8282+ Some(d) ->
8383+ list.append(service_fields, [#("description", json.string(d))])
8484+ None -> service_fields
8585+ }
8686+ let body =
8787+ json.to_string(json.object([#("service", json.object(service_fields))]))
8888+ use _ <- result.try(do_post(
8989+ client,
9090+ "/xrpc/io.pocketenv.service.addService",
9191+ [#("sandboxId", sandbox_id)],
9292+ body,
9393+ ))
9494+ Ok(Nil)
9595+}
9696+9797+/// Starts the service identified by `service_id`.
9898+pub fn start(
9999+ client: Client,
100100+ service_id: String,
101101+) -> Result(Nil, PocketenvError) {
102102+ use _ <- result.try(do_post(
103103+ client,
104104+ "/xrpc/io.pocketenv.service.startService",
105105+ [#("serviceId", service_id)],
106106+ "{}",
107107+ ))
108108+ Ok(Nil)
109109+}
110110+111111+/// Stops the service identified by `service_id`.
112112+pub fn stop(client: Client, service_id: String) -> Result(Nil, PocketenvError) {
113113+ use _ <- result.try(do_post(
114114+ client,
115115+ "/xrpc/io.pocketenv.service.stopService",
116116+ [#("serviceId", service_id)],
117117+ "{}",
118118+ ))
119119+ Ok(Nil)
120120+}
121121+122122+/// Restarts the service identified by `service_id`.
123123+pub fn restart(
124124+ client: Client,
125125+ service_id: String,
126126+) -> Result(Nil, PocketenvError) {
127127+ use _ <- result.try(do_post(
128128+ client,
129129+ "/xrpc/io.pocketenv.service.restartService",
130130+ [#("serviceId", service_id)],
131131+ "{}",
132132+ ))
133133+ Ok(Nil)
134134+}
135135+136136+/// Deletes the service identified by `service_id`.
137137+pub fn delete(
138138+ client: Client,
139139+ service_id: String,
140140+) -> Result(Nil, PocketenvError) {
141141+ use _ <- result.try(do_post(
142142+ client,
143143+ "/xrpc/io.pocketenv.service.deleteService",
144144+ [#("serviceId", service_id)],
145145+ "{}",
146146+ ))
147147+ Ok(Nil)
148148+}
149149+150150+/// JSON decoder for `Service`.
151151+pub fn service_decoder() -> decode.Decoder(Service) {
152152+ use id <- decode.field("id", decode.string)
153153+ use name <- decode.field("name", decode.string)
154154+ use command <- decode.field("command", decode.string)
155155+ use ports <- decode.optional_field("ports", None, decode.optional(decode.list(decode.int)))
156156+ use description <- decode.optional_field("description", None, decode.optional(decode.string))
157157+ use status <- decode.field("status", decode.string)
158158+ use created_at <- decode.field("createdAt", decode.string)
159159+ decode.success(Service(
160160+ id: id,
161161+ name: name,
162162+ command: command,
163163+ ports: ports,
164164+ description: description,
165165+ status: status,
166166+ created_at: created_at,
167167+ ))
168168+}
+91
src/pocketenv/volume.gleam
···11+//// Manage persistent volumes mounted in a sandbox.
22+////
33+//// Volumes provide durable storage that survives sandbox restarts.
44+55+import gleam/dynamic/decode
66+import gleam/json
77+import gleam/result
88+import pocketenv.{
99+ type Client, type PocketenvError, JsonDecodeError, do_get, do_post,
1010+}
1111+1212+/// A persistent volume mounted in a sandbox.
1313+pub type Volume {
1414+ Volume(id: String, name: String, path: String, created_at: String)
1515+}
1616+1717+/// Lists all volumes attached to `sandbox_id`.
1818+///
1919+/// ## Example
2020+///
2121+/// ```gleam
2222+/// let assert Ok(vols) = volume.list(client, sandbox_id)
2323+/// ```
2424+pub fn list(
2525+ client: Client,
2626+ sandbox_id: String,
2727+) -> Result(List(Volume), PocketenvError) {
2828+ use body <- result.try(do_get(
2929+ client,
3030+ "/xrpc/io.pocketenv.volume.getVolumes",
3131+ [#("sandboxId", sandbox_id)],
3232+ ))
3333+ json.parse(body, {
3434+ use volumes <- decode.field("volumes", decode.list(volume_decoder()))
3535+ decode.success(volumes)
3636+ })
3737+ |> result.map_error(JsonDecodeError)
3838+}
3939+4040+/// Creates a volume named `name` mounted at `path` in `sandbox_id`.
4141+///
4242+/// ## Example
4343+///
4444+/// ```gleam
4545+/// let assert Ok(Nil) = volume.create(client, sandbox_id, "data", "/mnt/data")
4646+/// ```
4747+pub fn create(
4848+ client: Client,
4949+ sandbox_id: String,
5050+ name: String,
5151+ path: String,
5252+) -> Result(Nil, PocketenvError) {
5353+ let body =
5454+ json.to_string(json.object([
5555+ #(
5656+ "volume",
5757+ json.object([
5858+ #("sandboxId", json.string(sandbox_id)),
5959+ #("name", json.string(name)),
6060+ #("path", json.string(path)),
6161+ ]),
6262+ ),
6363+ ]))
6464+ use _ <- result.try(do_post(
6565+ client,
6666+ "/xrpc/io.pocketenv.volume.addVolume",
6767+ [],
6868+ body,
6969+ ))
7070+ Ok(Nil)
7171+}
7272+7373+/// Deletes the volume identified by `id`.
7474+pub fn delete(client: Client, id: String) -> Result(Nil, PocketenvError) {
7575+ use _ <- result.try(do_post(
7676+ client,
7777+ "/xrpc/io.pocketenv.volume.deleteVolume",
7878+ [#("id", id)],
7979+ "{}",
8080+ ))
8181+ Ok(Nil)
8282+}
8383+8484+/// JSON decoder for `Volume`.
8585+pub fn volume_decoder() -> decode.Decoder(Volume) {
8686+ use id <- decode.field("id", decode.string)
8787+ use name <- decode.field("name", decode.string)
8888+ use path <- decode.field("path", decode.string)
8989+ use created_at <- decode.field("createdAt", decode.string)
9090+ decode.success(Volume(id: id, name: name, path: path, created_at: created_at))
9191+}