Compare commits

..

7 Commits

Author SHA1 Message Date
349b230913 feat: align SDK with updated OpenAPI spec and add missing unit tests
- Update generated types from new openapi.yaml (capsule stats, usage,
  metrics, pause/resume lifecycle, host/channel management, auth flow)
- Add Capsule pause/resume/ping/getMetrics lifecycle methods
- Add Capsule.waitForReady abort signal support
- Add PtyManager.connect and PtySession disposal
- Fix HttpClient empty-body response handling (content-length: 0)
- Add streamProcess() to CommandManager for background process streams
- Add integration tests for capsule lifecycle, git, and PTY features
- Add unit tests for AsyncQueue error paths, PtySession.close,
  Git.checkout without create, Git.add single string,
  Notebook.execCell error case, and PtyStartOptions fields
2026-05-16 19:14:55 +06:00
a69118aa2d chore: switch package manager to bun and create docs for codebase 2026-05-16 17:27:47 +06:00
b35df41f08 test public entry point exports 2026-05-16 16:23:27 +06:00
1f74d48576 feat: complete public entry point exports 2026-05-16 16:11:05 +06:00
573506b4c5 feat: add higher-level git and code interpreter APIs 2026-05-15 16:04:59 +06:00
c6322d8601 feat: add high-level capsule feature modules 2026-05-14 23:22:53 +06:00
8fb9753fde feat: add high-level Capsule lifecycle API 2026-05-14 22:51:01 +06:00
34 changed files with 4284 additions and 2387 deletions

4
.gitignore vendored
View File

@ -137,4 +137,6 @@ dist
.pnp.*
# AI agents
.opencode
.opencode*
# Added by code-review-graph
.code-review-graph/

View File

@ -65,3 +65,42 @@ Woodpecker CI (`.woodpecker/check.yml`) runs on push to `main` and `dev`:
## Dependencies
Runtime: `ws` (WebSocket), `zod` (validation). Everything else is dev-only.
<!-- code-review-graph MCP tools -->
## MCP Tools: code-review-graph
**IMPORTANT: This project has a knowledge graph. ALWAYS use the
code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore
the codebase.** The graph is faster, cheaper (fewer tokens), and gives
you structural context (callers, dependents, test coverage) that file
scanning cannot.
### When to use graph tools FIRST
- **Exploring code**: `semantic_search_nodes` or `query_graph` instead of Grep
- **Understanding impact**: `get_impact_radius` instead of manually tracing imports
- **Code review**: `detect_changes` + `get_review_context` instead of reading entire files
- **Finding relationships**: `query_graph` with callers_of/callees_of/imports_of/tests_for
- **Architecture questions**: `get_architecture_overview` + `list_communities`
Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need.
### Key Tools
| Tool | Use when |
| ------ | ---------- |
| `detect_changes` | Reviewing code changes — gives risk-scored analysis |
| `get_review_context` | Need source snippets for review — token-efficient |
| `get_impact_radius` | Understanding blast radius of a change |
| `get_affected_flows` | Finding which execution paths are impacted |
| `query_graph` | Tracing callers, callees, imports, tests, dependencies |
| `semantic_search_nodes` | Finding functions/classes by name or keyword |
| `get_architecture_overview` | Understanding high-level codebase structure |
| `refactor_tool` | Planning renames, finding dead code |
### Workflow
1. The graph auto-updates on file changes (via hooks).
2. Use `detect_changes` for code review.
3. Use `get_affected_flows` to understand impact.
4. Use `query_graph` pattern="tests_for" to check coverage.

View File

@ -1,7 +1,7 @@
# Makefile
.PHONY: generate lint test test-integration check build
SPEC_URL = "https://raw.githubusercontent.com/wrennhq/wrenn/refs/heads/main/internal/api/openapi.yaml"
SPEC_URL = "https://git.omukk.dev/wrenn/wrenn/raw/branch/feat/migrate-to-ch/internal/api/openapi.yaml"
SPEC_PATH = "api/openapi.yaml"
generate:
@ -10,20 +10,20 @@ generate:
curl -fsSL $(SPEC_URL) -o $(SPEC_PATH)
@echo "Generating TypeScript types..."
mkdir -p src/models
pnpm generate
bun run generate
lint:
pnpm exec biome check .
bunx biome check .
test:
pnpm vitest run --exclude tests/integration
bunx vitest run --exclude tests/integration
test-integration:
pnpm test:integration
bun run test:integration
check:
$(MAKE) lint
$(MAKE) test
build:
pnpm build
bun run build

554
README.md
View File

@ -1 +1,553 @@
# js-sdk
# Wrenn JavaScript SDK
JavaScript and TypeScript client for the [Wrenn](https://wrenn.dev) microVM platform. Create isolated capsules, execute commands, manage files, run interactive terminals, and execute Python code -- all from Node.js.
Designed as an e2b-style SDK. If you're migrating, `Sandbox` is available as a deprecated alias for `Capsule`.
## Installation
```bash
npm install @wrenn/sdk
```
Requires Node.js 18+.
## Authentication
Set the `WRENN_API_KEY` environment variable:
```bash
export WRENN_API_KEY="wrn_your_api_key_here"
```
Optionally override the API base URL:
```bash
export WRENN_BASE_URL="https://app.wrenn.dev/api" # default
```
You can also pass credentials directly:
```ts
import { Capsule } from "@wrenn/sdk";
const capsule = await Capsule.create("minimal", {
apiKey: "wrn_...",
baseUrl: "https://app.wrenn.dev/api",
});
```
---
## Wrenn Capsules
### Quick Start
```ts
import { Capsule } from "@wrenn/sdk";
const capsule = await Capsule.create("minimal");
try {
await capsule.waitForReady();
const result = await capsule.commands.exec("echo", { args: ["hello"] });
console.log(result.stdout); // "hello\n"
} finally {
await capsule.destroy();
}
```
### Creating Capsules
```ts
import { Capsule } from "@wrenn/sdk";
// Create with defaults: template="minimal", vcpus=1, memory_mb=512.
const capsule = await Capsule.create();
// Create with an explicit template.
const python = await Capsule.create("base-python");
// Create with resource and client options.
const larger = await Capsule.create("minimal", {
vcpus: 2,
memory_mb: 1024,
timeout_sec: 300,
apiKey: "wrn_...",
});
// Equivalent options-object form.
const fromOptions = await Capsule.create({
template: "minimal",
vcpus: 2,
memory_mb: 1024,
});
```
### Resource Cleanup
Capsules created with `Capsule.create()` are owned by that SDK instance. When used with `await using`, the remote capsule is destroyed automatically when the block exits:
```ts
await using capsule = await Capsule.create("minimal");
await capsule.waitForReady();
await capsule.commands.exec("echo", { args: ["work"] });
// capsule is automatically destroyed here
```
Capsules attached with `new Capsule(id)` or `Capsule.connect(id)` are not owned. `await using` a connected capsule only runs local cleanup and does not destroy the remote capsule.
Use `destroy()` when you want to delete the remote capsule:
```ts
const capsule = await Capsule.create("minimal");
try {
await capsule.waitForReady();
await capsule.commands.exec("echo", { args: ["work"] });
} finally {
await capsule.destroy();
}
```
### Connecting to Existing Capsules
Attach to an existing capsule by ID. This wraps the ID locally and does not fetch or validate it until you call an API method:
```ts
const capsule = Capsule.connect("cl-abc123");
const info = await capsule.getInfo();
if (info.status === "paused") {
await capsule.resume({ wait: true });
}
const result = await capsule.commands.exec("echo", { args: ["still running"] });
console.log(result.stdout);
```
For code interpreter capsules:
```ts
import { CodeInterpreter } from "@wrenn/sdk";
const interpreter = CodeInterpreter.connect("cl-abc123");
const result = await interpreter.notebook.execCell("print('reconnected')");
console.log(result.stdout);
```
### Lifecycle Management
```ts
// Instance methods.
await capsule.pause(); // returns status like "pausing"
await capsule.resume(); // returns status like "resuming"
await capsule.resume({ wait: true });
await capsule.destroy();
await capsule.ping(); // reset inactivity timer
await capsule.waitForReady();
const info = await capsule.getInfo();
console.log(info.status); // "running"
const metrics = await capsule.getMetrics({ range: "10m" });
// Static helper.
await Capsule.destroy("cl-abc123", { apiKey: "wrn_..." });
```
### Command Execution
Commands are accessed via `capsule.commands`:
```ts
// Foreground command.
const result = await capsule.commands.exec("python3", {
args: ["-c", "print(42)"],
timeoutSec: 30,
cwd: "/app",
});
console.log(result.stdout); // "42\n"
console.log(result.stderr);
console.log(result.exit_code); // 0
// Background process.
const process = await capsule.commands.start("python3", {
args: ["server.py"],
tag: "web-server",
envs: { PORT: "8000" },
cwd: "/app",
});
console.log(process.pid);
console.log(process.tag);
```
#### Streaming Output
```ts
// Stream a new command.
for await (const event of capsule.commands.stream("python3", {
args: ["-u", "train.py"],
})) {
if (event.type === "stdout") {
process.stdout.write(String(event.data ?? ""));
}
if (event.type === "stderr") {
process.stderr.write(String(event.data ?? ""));
}
if (event.type === "exit") {
console.log("exited", event.exit_code);
}
}
// Connect to a running background process stream.
await using connection = await capsule.commands.streamProcess("web-server");
connection.send({ type: "ping" });
// connection is automatically closed here
```
#### Process Management
```ts
const processes = await capsule.commands.list();
for (const proc of processes.processes ?? []) {
console.log(proc.pid, proc.tag);
}
await capsule.commands.kill("web-server", "SIGTERM");
```
### Filesystem
Files are accessed via `capsule.files`:
```ts
// Write and read files.
await capsule.files.write("/app/main.py", "print('hello')");
const content = await capsule.files.read("/app/main.py");
console.log(content.toString());
// List directory.
const listing = await capsule.files.list("/app", { depth: 1 });
for (const entry of listing.entries ?? []) {
console.log(entry.name, entry.type, entry.size);
}
// Create directory.
await capsule.files.mkdir("/app/data");
// Remove file or directory.
await capsule.files.remove("/app/old_data");
```
#### Streaming Large Files
```ts
await capsule.files.uploadStream(
"/data/large.txt",
Buffer.from("large file content"),
);
const chunks: Buffer[] = [];
for await (const chunk of capsule.files.downloadStream("/data/large.txt")) {
chunks.push(chunk);
}
console.log(Buffer.concat(chunks).toString());
```
### Git
Git operations are accessed via `capsule.git`. All commands execute the real `git` binary inside the capsule:
```ts
// Clone a repository.
await capsule.git.clone("https://github.com/org/repo.git", {
path: "/app/repo",
branch: "main",
});
// Use repository-scoped commands.
const status = await capsule.git.status({ cwd: "/app/repo" });
console.log(status.stdout);
const log = await capsule.git.log({ cwd: "/app/repo", maxCount: 5 });
console.log(log.stdout);
// Branches.
await capsule.git.checkout("feature", { cwd: "/app/repo", create: true });
console.log((await capsule.git.branch({ cwd: "/app/repo" })).stdout);
// Stage and commit.
await capsule.git.add(["README.md", "src/index.ts"], { cwd: "/app/repo" });
await capsule.git.commit("initial commit", { cwd: "/app/repo" });
// Push and pull.
await capsule.git.pull({ cwd: "/app/repo", remote: "origin", branch: "main" });
await capsule.git.push({ cwd: "/app/repo", remote: "origin", branch: "main" });
```
Git helpers return command results. Check `exit_code`, `stdout`, and `stderr` for command-level failures.
### Interactive Terminal (PTY)
```ts
await using term = await capsule.pty.start({
cmd: "/bin/bash",
cols: 120,
rows: 40,
cwd: "/home/user",
});
term.input("ls -la\n");
for await (const event of term.events) {
if (event.type === "data") {
process.stdout.write(String(event.data ?? ""));
}
if (event.type === "exit") {
break;
}
}
// terminal WebSocket is automatically closed here
```
Reconnect to a tagged session:
```ts
await using term = await capsule.pty.connect("my-session-tag");
term.input("echo reconnected\n");
// terminal WebSocket is automatically closed here
```
**PtySession methods:**
| Method | Description |
|--------|-------------|
| `input(data)` | Send text or bytes to stdin |
| `resize(cols, rows)` | Resize the terminal |
| `kill()` | Send a kill control message |
| `close()` | Close the WebSocket connection |
| `events` | Async iterator of PTY events |
---
## Code Interpreter
`CodeInterpreter` creates or connects to a capsule intended for Python code execution. It uses the capsule command API to execute cells with `python3 -c`.
### Quick Start
```ts
import { CodeInterpreter } from "@wrenn/sdk";
await using interpreter = await CodeInterpreter.create();
await interpreter.capsule.waitForReady();
const result = await interpreter.notebook.execCell("print('hello')");
console.log(result.stdout); // "hello\n"
// interpreter.capsule is automatically destroyed here
```
Interpreters created with `CodeInterpreter.create()` own their capsule and destroy it on `await using` disposal. Interpreters attached with `CodeInterpreter.connect(id)` follow connected capsule semantics and leave the remote capsule running.
### Custom Templates
By default, `CodeInterpreter.create()` uses the `jupyter` template. You can specify a custom template:
```ts
const interpreter = await CodeInterpreter.create({
template: "my-custom-python-template",
timeout_sec: 300,
});
const result = await interpreter.notebook.execCell("print('custom template')", {
timeoutSec: 60,
});
```
### Code Interpreter + Commands/Files
The code interpreter wrapper exposes the underlying standard capsule:
```ts
const interpreter = await CodeInterpreter.create();
const { capsule } = interpreter;
await interpreter.notebook.execCell("open('/tmp/data.csv', 'w').write('a,b\\n1,2')");
const content = await capsule.files.read("/tmp/data.csv");
console.log(content.toString());
const result = await capsule.commands.exec("wc", { args: ["-l", "/tmp/data.csv"] });
console.log(result.stdout);
```
---
## Error Handling
The SDK maps unsuccessful API responses to typed errors:
```ts
import {
AuthenticationError,
BadRequestError,
ConflictError,
ForbiddenError,
HostHasCapsulesError,
NotFoundError,
PayloadTooLargeError,
ServerError,
TimeoutError,
WrennError,
} from "@wrenn/sdk";
try {
await Capsule.connect("missing").getInfo();
} catch (error) {
if (error instanceof NotFoundError) {
console.log(error.code);
console.log(error.message);
console.log(error.statusCode); // 404
}
if (error instanceof WrennError) {
console.log(error.body);
}
}
```
All SDK errors inherit from `WrennError` and expose `statusCode`, `code`, `message`, and `body`.
---
## Migrating from e2b
Replace your imports and prefer `Capsule` for new code:
```ts
// Before
import { Sandbox } from "e2b";
const sandbox = await Sandbox.create();
// After
import { Capsule } from "@wrenn/sdk";
const capsule = await Capsule.create();
```
The `Sandbox` name is available as a deprecated alias:
```ts
import { Sandbox } from "@wrenn/sdk";
const sandbox = await Sandbox.create("minimal");
```
---
## Low-Level Client
For direct API access, use `WrennClient`:
```ts
import { WrennClient } from "@wrenn/sdk";
const client = new WrennClient({ apiKey: "wrn_..." });
const capsule = await client.capsules.create({
template: "minimal",
vcpus: 1,
memory_mb: 512,
timeout_sec: 300,
});
await client.capsules.pause(capsule.id);
await client.capsules.resume(capsule.id);
await client.capsules.ping(capsule.id);
await client.capsules.destroy(capsule.id);
const templates = await client.snapshots.list();
console.log(templates);
```
Available resource groups:
| Resource | Property |
|----------|----------|
| Auth | `client.auth` |
| Account | `client.account` |
| API keys | `client.apiKeys` |
| Users | `client.users` |
| Teams | `client.teams` |
| Capsules | `client.capsules` |
| Files | `client.files` |
| Snapshots | `client.snapshots` |
| Hosts | `client.hosts` |
| Channels | `client.channels` |
Generated OpenAPI types are exported from the package root:
```ts
import type { components, operations, paths } from "@wrenn/sdk";
type CapsuleSchema = components["schemas"]["Capsule"];
type GetCapsuleOperation = operations["getCapsule"];
type CapsulePath = paths["/v1/capsules/{id}"];
```
---
## Development
This project uses [Bun](https://bun.sh) for dependency management and script execution. The package build still uses `tsup`, and tests use Vitest.
```bash
# Install dependencies
bun install
# Run linting
make lint
# Run unit tests
make test
# Build CJS, ESM, and declaration files
make build
# Run lint + unit tests
make check
```
### Running Integration Tests
Integration tests require a live Wrenn server. Set credentials via environment variables:
```bash
export WRENN_API_KEY="wrn_..."
export WRENN_BASE_URL="https://app.wrenn.dev/api" # optional
```
Then run:
```bash
make test-integration
```
Tests are automatically skipped when `WRENN_API_KEY` is not available.
## License
MIT

View File

@ -1,8 +1,8 @@
openapi: "3.1.0"
info:
title: Wrenn API
description: MicroVM-based code execution platform API.
version: "0.1.4"
description: AI agent execution platform API.
version: "0.2.0"
servers:
- url: http://localhost:8080
@ -866,8 +866,8 @@ paths:
schema:
$ref: "#/components/schemas/CreateCapsuleRequest"
responses:
"201":
description: Capsule created
"202":
description: Capsule creation initiated (status will be "starting")
content:
application/json:
schema:
@ -988,8 +988,8 @@ paths:
security:
- apiKeyAuth: []
responses:
"204":
description: Capsule destroyed
"202":
description: Capsule destruction initiated
/v1/capsules/{id}/exec:
parameters:
@ -1260,8 +1260,8 @@ paths:
destroys all running resources. The capsule exists only as files on
disk and can be resumed later.
responses:
"200":
description: Capsule paused (snapshot taken, resources released)
"202":
description: Capsule pause initiated (status will be "pausing")
content:
application/json:
schema:
@ -1289,11 +1289,11 @@ paths:
- apiKeyAuth: []
description: |
Restores a paused capsule from its snapshot using UFFD for lazy
memory loading. Boots a fresh Firecracker process, sets up a new
memory loading. Boots a fresh Cloud Hypervisor process, sets up a new
network slot, and waits for envd to become ready.
responses:
"200":
description: Capsule resumed (new VM booted from snapshot)
"202":
description: Capsule resume initiated (status will be "resuming")
content:
application/json:
schema:
@ -2035,6 +2035,51 @@ paths:
schema:
$ref: "#/components/schemas/Error"
/v1/hosts/sandbox-events:
post:
summary: Sandbox lifecycle event callback
operationId: sandboxEventCallback
tags: [hosts]
security:
- hostTokenAuth: []
description: |
Receives autonomous lifecycle events from host agents (e.g. auto-pause
from the TTL reaper). The event is published to an internal Redis stream
for the control plane's event consumer to process.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [event, sandbox_id, host_id]
properties:
event:
type: string
enum: [sandbox.auto_paused]
sandbox_id:
type: string
host_id:
type: string
timestamp:
type: integer
format: int64
responses:
"204":
description: Event accepted
"400":
description: Invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
description: Host ID mismatch
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/hosts/auth/refresh:
post:
summary: Refresh host JWT
@ -2346,7 +2391,63 @@ paths:
schema:
$ref: "#/components/schemas/Error"
/v1/admin/users/{id}/admin:
put:
summary: Grant or revoke platform admin
operationId: setUserAdmin
tags: [admin]
description: |
Sets the platform admin flag on a user. Cannot remove the last admin.
Requires platform admin access (JWT + is_admin).
The target user's JWT is not re-issued — their frontend will reflect the
change on next login or team switch.
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
example: "usr-a1b2c3d4"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [admin]
properties:
admin:
type: boolean
description: true to grant admin, false to revoke.
responses:
"204":
description: Admin status updated
"400":
$ref: "#/components/responses/BadRequest"
"403":
description: Caller is not a platform admin
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: User not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
responses:
BadRequest:
description: Invalid request parameters
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
securitySchemes:
apiKeyAuth:
type: apiKey
@ -2366,14 +2467,6 @@ components:
name: X-Host-Token
description: Host JWT returned from POST /v1/hosts/register or POST /v1/hosts/auth/refresh. Valid for 7 days.
responses:
BadRequest:
description: Invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
schemas:
SignupRequest:
type: object
@ -2552,7 +2645,7 @@ components:
type: string
status:
type: string
enum: [pending, starting, running, paused, hibernated, stopped, missing, error]
enum: [pending, starting, running, pausing, paused, resuming, stopping, hibernated, stopped, missing, error]
template:
type: string
vcpus:
@ -3019,7 +3112,7 @@ components:
mem_bytes:
type: integer
format: int64
description: "Resident memory in bytes (VmRSS of Firecracker process)"
description: "Resident memory in bytes (VmRSS of Cloud Hypervisor process)"
disk_bytes:
type: integer
format: int64

538
bun.lock Normal file
View File

@ -0,0 +1,538 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "js-sdk",
"dependencies": {
"ws": "^8.20.0",
"zod": "^4.4.3",
},
"devDependencies": {
"@biomejs/biome": "^2.4.14",
"@types/node": "^25.6.0",
"@types/ws": "^8.18.1",
"msw": "^2.14.3",
"openapi-typescript": "^7.13.0",
"tsup": "^8.5.1",
"typescript": "^6.0.3",
"vitest": "^4.1.5",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "7.28.5", "js-tokens": "4.0.0", "picocolors": "1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@biomejs/biome": ["@biomejs/biome@2.4.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.14", "@biomejs/cli-darwin-x64": "2.4.14", "@biomejs/cli-linux-arm64": "2.4.14", "@biomejs/cli-linux-arm64-musl": "2.4.14", "@biomejs/cli-linux-x64": "2.4.14", "@biomejs/cli-linux-x64-musl": "2.4.14", "@biomejs/cli-win32-arm64": "2.4.14", "@biomejs/cli-win32-x64": "2.4.14" }, "bin": { "biome": "bin/biome" } }, "sha512-TmAvxOEgrpLypzVGJ8FulIZnlyA9TxrO1hyqYrCz9r+bwma9xXxuLA5IuYnj55XQneFx460KjRbx6SWGLkg3bQ=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XvgoE9XOawUOQPdmvs4J7wPhi/DLwSCGks3AlPJDmh34O0awRTqCED1HRcRDdpf1Zrp4us4MGOOdIxNpbqNF5Q=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-jE7hKBCFhOx3uUh+ZkWBfOHxAcILPfhFplNkuID/eZeSTLHzfZzoZxW8fbqY9xXRnPi7jGNAf1iPVR+0yWsM/Q=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-2TELhZnW5RSLL063l9rc5xLpA0ZIw0Ccwy/0q384rvNAgFw3yI76bd59547yxowdQr5MNPET/xDLrLuvgSeeWQ=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-/z+6gqAqqUQTHazwStxSXKHg9b8UvqBmDFRp+c4wYbq2KXhELQDon9EoC9RpmQ8JWkqQx/lIUy/cs+MhzDZp6A=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.14", "", { "os": "linux", "cpu": "x64" }, "sha512-zHrlQZDBDUz4OLAraYpWKcnLS6HOewBFWYOzY91d1ZjdqZwibOyb6BEu6WuWLugyo0P3riCmsbV9UqV1cSXwQg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.14", "", { "os": "linux", "cpu": "x64" }, "sha512-R6BWgJdQOwW9ulJatuTVrQkjnODjqHZkKNOqb1sz++3Noe5LYd0i3PchnOBUCYAPHoPWHhjJqbdZlHEu0hpjdA=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-M3EH5hqOI/F/FUA2u4xcLoUgmxd218mvuj/6JL7Hv2toQvr2/AdOvKSpGkoRuWFCtQPVa+ZqkEV3Q5xBA9+XSA=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.14", "", { "os": "win32", "cpu": "x64" }, "sha512-WL0EG5qE+EAKomGXbf2g6VnSKJhTL3tXC0QRzWRwA5VpjxNYa6H4P7ZWfymbGE4IhZZQi1KXQ2R0YjwInmz2fA=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "2.8.1" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
"@inquirer/ansi": ["@inquirer/ansi@2.0.5", "", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="],
"@inquirer/confirm": ["@inquirer/confirm@6.0.12", "", { "dependencies": { "@inquirer/core": "11.1.9", "@inquirer/type": "4.0.5" }, "optionalDependencies": { "@types/node": "25.6.0" } }, "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og=="],
"@inquirer/core": ["@inquirer/core@11.1.9", "", { "dependencies": { "@inquirer/ansi": "2.0.5", "@inquirer/figures": "2.0.5", "@inquirer/type": "4.0.5", "cli-width": "4.1.0", "fast-wrap-ansi": "0.2.0", "mute-stream": "3.0.0", "signal-exit": "4.1.0" }, "optionalDependencies": { "@types/node": "25.6.0" } }, "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg=="],
"@inquirer/figures": ["@inquirer/figures@2.0.5", "", {}, "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ=="],
"@inquirer/type": ["@inquirer/type@4.0.5", "", { "optionalDependencies": { "@types/node": "25.6.0" } }, "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5", "@jridgewell/trace-mapping": "0.3.31" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.8", "", { "dependencies": { "@open-draft/deferred-promise": "2.2.0", "@open-draft/logger": "0.3.0", "@open-draft/until": "2.1.0", "is-node-process": "1.2.0", "outvariant": "1.4.3", "strict-event-emitter": "0.5.1" } }, "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "0.10.2" }, "peerDependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@open-draft/deferred-promise": ["@open-draft/deferred-promise@3.0.0", "", {}, "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA=="],
"@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "1.2.0", "outvariant": "1.4.3" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="],
"@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
"@redocly/ajv": ["@redocly/ajv@8.11.2", "", { "dependencies": { "fast-deep-equal": "3.1.3", "json-schema-traverse": "1.0.0", "require-from-string": "2.0.2", "uri-js-replace": "1.0.1" } }, "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg=="],
"@redocly/config": ["@redocly/config@0.22.0", "", {}, "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ=="],
"@redocly/openapi-core": ["@redocly/openapi-core@1.34.14", "", { "dependencies": { "@redocly/ajv": "8.11.2", "@redocly/config": "0.22.0", "colorette": "1.4.0", "https-proxy-agent": "7.0.6", "js-levenshtein": "1.1.6", "js-yaml": "4.1.1", "minimatch": "5.1.9", "pluralize": "8.0.0", "yaml-ast-parser": "0.0.43" } }, "sha512-y+xFx+Zz54Xhr8jUdnLENYnt7Y7GEDL6Q03ga7rTtX8DVwefX9H+hQEPgJp1nda7vdH+wJ9/HBVvyfBuW9x6rA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.3", "", { "os": "android", "cpu": "arm" }, "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.3", "", { "os": "android", "cpu": "arm64" }, "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.3", "", { "os": "none", "cpu": "arm64" }, "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "4.0.2", "assertion-error": "2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "7.19.2" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"@types/set-cookie-parser": ["@types/set-cookie-parser@2.4.10", "", { "dependencies": { "@types/node": "25.6.0" } }, "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw=="],
"@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "25.6.0" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@vitest/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "@types/chai": "5.2.3", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "6.2.2", "tinyrainbow": "3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="],
"@vitest/mocker": ["@vitest/mocker@4.1.5", "", { "dependencies": { "@vitest/spy": "4.1.5", "estree-walker": "3.0.3", "magic-string": "0.30.21" }, "optionalDependencies": { "msw": "2.14.3", "vite": "8.0.10" } }, "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="],
"@vitest/runner": ["@vitest/runner@4.1.5", "", { "dependencies": { "@vitest/utils": "4.1.5", "pathe": "2.0.3" } }, "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "@vitest/utils": "4.1.5", "magic-string": "0.30.21", "pathe": "2.0.3" } }, "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ=="],
"@vitest/spy": ["@vitest/spy@4.1.5", "", {}, "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ=="],
"@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "2.0.0", "tinyrainbow": "3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "1.0.2" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "0.2.5" }, "peerDependencies": { "esbuild": "0.27.7" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"change-case": ["change-case@5.4.4", "", {}, "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "4.1.2" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "4.2.3", "strip-ansi": "6.0.1", "wrap-ansi": "7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="],
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" }, "optionalDependencies": { "supports-color": "10.2.2" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "1.0.8" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
"fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "3.0.3" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
"fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="],
"fdir": ["fdir@6.5.0", "", { "optionalDependencies": { "picomatch": "4.0.4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "0.30.21", "mlly": "1.8.2", "rollup": "4.60.3" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"graphql": ["graphql@16.13.2", "", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="],
"headers-polyfill": ["headers-polyfill@5.0.1", "", { "dependencies": { "@types/set-cookie-parser": "2.4.10", "set-cookie-parser": "3.1.0" } }, "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "7.1.4", "debug": "4.4.3" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "2.1.0" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "8.16.0", "pathe": "2.0.3", "pkg-types": "1.3.1", "ufo": "1.6.4" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msw": ["msw@2.14.3", "", { "dependencies": { "@inquirer/confirm": "6.0.12", "@mswjs/interceptors": "0.41.8", "@open-draft/deferred-promise": "3.0.0", "@types/statuses": "2.0.6", "cookie": "1.1.1", "graphql": "16.13.2", "headers-polyfill": "5.0.1", "is-node-process": "1.2.0", "outvariant": "1.4.3", "path-to-regexp": "6.3.0", "picocolors": "1.1.1", "rettime": "0.11.11", "statuses": "2.0.2", "strict-event-emitter": "0.5.1", "tough-cookie": "6.0.1", "type-fest": "5.6.0", "until-async": "3.0.2", "yargs": "17.7.2" }, "optionalDependencies": { "typescript": "6.0.3" }, "bin": { "msw": "cli/index.js" } }, "sha512-kk8G5cocVlJ4wsKMGZegn2H6XLOEKjbA+nSJE2354e/SRp4mDicCHUYnMXpymzVcVDCs+GUAsmNqSn+yHv4T2A=="],
"mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "1.3.0", "object-assign": "4.1.1", "thenify-all": "1.6.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"openapi-typescript": ["openapi-typescript@7.13.0", "", { "dependencies": { "@redocly/openapi-core": "1.34.14", "ansi-colors": "4.1.3", "change-case": "5.4.4", "parse-json": "8.3.0", "supports-color": "10.2.2", "yargs-parser": "21.1.1" }, "peerDependencies": { "typescript": "6.0.3" }, "bin": { "openapi-typescript": "bin/cli.js" } }, "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ=="],
"outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="],
"parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "7.29.0", "index-to-position": "1.2.0", "type-fest": "4.41.0" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="],
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "0.1.8", "mlly": "1.8.2", "pathe": "2.0.3" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "3.3.12", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "3.1.3" }, "optionalDependencies": { "postcss": "8.5.14" } }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"rettime": ["rettime@0.11.11", "", {}, "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ=="],
"rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
"rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "2.3.3" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="],
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="],
"strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "8.0.0", "is-fullwidth-code-point": "3.0.0", "strip-ansi": "6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "0.3.13", "commander": "4.1.1", "lines-and-columns": "1.2.4", "mz": "2.7.0", "pirates": "4.0.7", "tinyglobby": "0.2.16", "ts-interface-checker": "0.1.13" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
"supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "1.3.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": "3.3.1" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"tldts": ["tldts@7.0.30", "", { "dependencies": { "tldts-core": "7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw=="],
"tldts-core": ["tldts-core@7.0.30", "", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="],
"tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "7.0.30" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "5.1.0", "cac": "6.7.14", "chokidar": "4.0.3", "consola": "3.4.2", "debug": "4.4.3", "esbuild": "0.27.7", "fix-dts-default-cjs-exports": "1.0.1", "joycon": "3.1.1", "picocolors": "1.1.1", "postcss-load-config": "6.0.1", "resolve-from": "5.0.0", "rollup": "4.60.3", "source-map": "0.7.6", "sucrase": "3.35.1", "tinyexec": "0.3.2", "tinyglobby": "0.2.16", "tree-kill": "1.2.2" }, "optionalDependencies": { "postcss": "8.5.14", "typescript": "6.0.3" }, "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
"type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="],
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
"uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="],
"vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "1.32.0", "picomatch": "4.0.4", "postcss": "8.5.14", "rolldown": "1.0.0-rc.17", "tinyglobby": "0.2.16" }, "optionalDependencies": { "@types/node": "25.6.0", "esbuild": "0.27.7", "fsevents": "2.3.3" }, "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
"vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "2.1.0", "expect-type": "1.3.0", "magic-string": "0.30.21", "obug": "2.1.1", "pathe": "2.0.3", "picomatch": "4.0.4", "std-env": "4.1.0", "tinybench": "2.9.0", "tinyexec": "1.1.2", "tinyglobby": "0.2.16", "tinyrainbow": "3.1.0", "why-is-node-running": "2.3.0" }, "optionalDependencies": { "@types/node": "25.6.0" }, "peerDependencies": { "vite": "8.0.10" }, "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "4.3.0", "string-width": "4.2.3", "strip-ansi": "6.0.1" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"ws": ["ws@8.20.0", "", {}, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "8.0.1", "escalade": "3.2.0", "get-caller-file": "2.0.5", "require-directory": "2.1.1", "string-width": "4.2.3", "y18n": "5.0.8", "yargs-parser": "21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"@mswjs/interceptors/@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="],
"parse-json/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"vitest/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
}
}

View File

@ -26,7 +26,7 @@
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.26.1",
"packageManager": "bun@1.3.14",
"devDependencies": {
"@biomejs/biome": "^2.4.14",
"@types/node": "^25.6.0",

2321
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,98 @@
/**
* Small async iterator queue used to bridge callback-based streams to generators.
*
* Producers call {@link push}, {@link end}, or {@link fail}; consumers iterate the
* queue with `for await` or call {@link next} directly.
*
* @template T - Event type yielded by the queue.
*/
export class AsyncQueue<T> implements AsyncIterableIterator<T> {
private readonly values: T[] = [];
private readonly waiters: Array<{
reject: (reason?: unknown) => void;
resolve: (value: IteratorResult<T>) => void;
}> = [];
private closed = false;
private error: unknown;
/** Returns this queue as its own async iterator. */
[Symbol.asyncIterator](): AsyncIterableIterator<T> {
return this;
}
/**
* Reads the next queued value, waits for one, or completes when the queue ends.
*
* @returns Next async iterator result.
*/
next(): Promise<IteratorResult<T>> {
if (this.values.length) {
return Promise.resolve({ done: false, value: this.values.shift() as T });
}
if (this.error) return Promise.reject(this.error);
if (this.closed) return Promise.resolve({ done: true, value: undefined });
return new Promise((resolve, reject) => {
this.waiters.push({ reject, resolve });
});
}
/**
* Ends the queue when a consumer stops iteration early.
*
* @returns Completed async iterator result.
*/
return(): Promise<IteratorResult<T>> {
this.end();
return Promise.resolve({ done: true, value: undefined });
}
/**
* Fails the queue when a consumer throws into the iterator.
*
* @param error - Error to propagate to waiting consumers.
* @returns Rejected promise containing the provided error.
*/
throw(error?: unknown): Promise<IteratorResult<T>> {
this.fail(error);
return Promise.reject(error);
}
/**
* Pushes a value to the next waiting consumer or buffers it for later reads.
*
* @param value - Value to yield from the iterator.
*/
push(value: T): void {
if (this.closed) return;
const waiter = this.waiters.shift();
if (waiter) {
waiter.resolve({ done: false, value });
return;
}
this.values.push(value);
}
/** Completes the queue and resolves all waiting consumers as done. */
end(): void {
if (this.closed) return;
this.closed = true;
for (const waiter of this.waiters.splice(0)) {
waiter.resolve({ done: true, value: undefined });
}
}
/**
* Fails the queue and rejects all waiting consumers.
*
* @param error - Error to propagate through the iterator.
*/
fail(error: unknown): void {
if (this.closed) return;
this.closed = true;
this.error = error;
for (const waiter of this.waiters.splice(0)) {
waiter.reject(error);
}
}
}

View File

@ -1,5 +1,6 @@
import { TimeoutError, throwErrorFromResponse } from "../exceptions.js";
/** Configuration for the low-level HTTP client. */
export interface HttpClientConfig {
/** API origin used for all relative request paths. */
baseUrl: string;
@ -60,27 +61,70 @@ export class HttpClient {
}
}
/** Sends a GET request and parses the JSON response. */
/**
* Sends a GET request and parses the JSON response.
*
* @param path - API path relative to the configured base URL.
* @param opts - Optional query, header, auth, timeout, and cancellation settings.
* @returns Parsed JSON response.
* @throws TimeoutError when the configured timeout aborts the request.
* @throws WrennError subclasses for unsuccessful responses.
*/
get<T>(path: string, opts?: RequestOptions): Promise<T> {
return this.request<T>("GET", path, undefined, opts);
}
/** Sends a POST request with an optional JSON body. */
/**
* Sends a POST request with an optional JSON body.
*
* @param path - API path relative to the configured base URL.
* @param body - Optional JSON request body.
* @param opts - Optional query, header, auth, timeout, and cancellation settings.
* @returns Parsed JSON response.
* @throws TimeoutError when the configured timeout aborts the request.
* @throws WrennError subclasses for unsuccessful responses.
*/
post<T>(path: string, body?: unknown, opts?: RequestOptions): Promise<T> {
return this.request<T>("POST", path, body, opts);
}
/** Sends a PATCH request with an optional JSON body. */
/**
* Sends a PATCH request with an optional JSON body.
*
* @param path - API path relative to the configured base URL.
* @param body - Optional JSON request body.
* @param opts - Optional query, header, auth, timeout, and cancellation settings.
* @returns Parsed JSON response.
* @throws TimeoutError when the configured timeout aborts the request.
* @throws WrennError subclasses for unsuccessful responses.
*/
patch<T>(path: string, body?: unknown, opts?: RequestOptions): Promise<T> {
return this.request<T>("PATCH", path, body, opts);
}
/** Sends a PUT request with an optional JSON body. */
/**
* Sends a PUT request with an optional JSON body.
*
* @param path - API path relative to the configured base URL.
* @param body - Optional JSON request body.
* @param opts - Optional query, header, auth, timeout, and cancellation settings.
* @returns Parsed JSON response.
* @throws TimeoutError when the configured timeout aborts the request.
* @throws WrennError subclasses for unsuccessful responses.
*/
put<T>(path: string, body?: unknown, opts?: RequestOptions): Promise<T> {
return this.request<T>("PUT", path, body, opts);
}
/** Sends a DELETE request and expects no response body. */
/**
* Sends a DELETE request and expects no response body.
*
* @param path - API path relative to the configured base URL.
* @param opts - Optional query, header, auth, timeout, and cancellation settings.
* @returns Resolves when the response succeeds.
* @throws TimeoutError when the configured timeout aborts the request.
* @throws WrennError subclasses for unsuccessful responses.
*/
delete(path: string, opts?: RequestOptions): Promise<void> {
return this.request<void>("DELETE", path, undefined, opts);
}
@ -181,19 +225,22 @@ export class HttpClient {
): Promise<T> {
const res = await this.rawRequest(method, path, body, opts);
if (res.status === 204) {
return undefined as T;
}
if (!res.ok) {
await throwErrorFromResponse(res);
}
if (res.status === 204 || res.headers.get("content-length") === "0") {
return undefined as T;
}
if (opts?.asText) {
return (await res.text()) as T;
}
return (await res.json()) as T;
const text = await res.text();
if (!text) return undefined as T;
return JSON.parse(text) as T;
}
private async rawRequest(

View File

@ -46,6 +46,11 @@ export class WsConnection {
this.ws.close();
}
/** Closes the WebSocket when used with `await using`. */
async [Symbol.asyncDispose](): Promise<void> {
this.close();
}
/** Indicates whether the connection has closed or failed. */
get isClosed(): boolean {
return this.closed;

325
src/capsule.ts Normal file
View File

@ -0,0 +1,325 @@
import {
type OperationJsonBody,
type OperationJsonResponse,
type OperationQueryParams,
WrennClient,
} from "./client.js";
import { CommandManager } from "./commands.js";
import type { ClientConfig } from "./config.js";
import { TimeoutError, WrennError } from "./exceptions.js";
import { FileManager } from "./files.js";
import { Git } from "./git/index.js";
import { PtyManager } from "./pty.js";
/** Capsule metadata returned by lifecycle endpoints. */
export type CapsuleInfo = OperationJsonResponse<"getCapsule", 200>;
/** Resource metrics returned for a capsule. */
export type CapsuleMetrics = OperationJsonResponse<"getCapsuleMetrics", 200>;
/** Query parameters accepted by capsule metrics requests. */
export type CapsuleMetricsOptions = OperationQueryParams<"getCapsuleMetrics">;
type CapsuleStatus = NonNullable<CapsuleInfo["status"]>;
const DEFAULT_TEMPLATE = "minimal";
const DEFAULT_VCPUS = 1;
const DEFAULT_MEMORY_MB = 512;
const DEFAULT_TIMEOUT_SEC = 0;
const DEFAULT_WAIT_TIMEOUT_MS = 60_000;
const DEFAULT_WAIT_INTERVAL_MS = 1_000;
const TERMINAL_STATUSES = new Set<CapsuleStatus>([
"error",
"missing",
"stopping",
"stopped",
]);
/** Options accepted when creating a new capsule. */
export interface CapsuleCreateOptions extends ClientConfig {
/** Template name to boot. Defaults to `minimal`. */
template?: string;
/** Number of virtual CPUs. Defaults to `1`. */
vcpus?: number;
/** Memory allocation in MiB. Defaults to `512`. */
memory_mb?: number;
/** Auto-pause TTL in seconds. Defaults to `0`, meaning no auto-pause. */
timeout_sec?: number;
}
/** Options used by lifecycle operations that wait for a running capsule. */
export interface WaitForReadyOptions {
/** Maximum time to wait before failing. Defaults to 60 seconds. */
timeoutMs?: number;
/** Delay between status polls. Defaults to 1 second. */
intervalMs?: number;
/** Optional cancellation signal. */
signal?: AbortSignal;
}
export type CapsuleResumeOptions = WaitForReadyOptions & {
/** When true, wait until the resumed capsule reports `running`. */
wait?: boolean;
};
function clientConfigFrom(opts?: ClientConfig): ClientConfig | undefined {
if (!opts) return undefined;
const config: ClientConfig = {};
if (opts.baseUrl !== undefined) config.baseUrl = opts.baseUrl;
if (opts.apiKey !== undefined) config.apiKey = opts.apiKey;
if (opts.token !== undefined) config.token = opts.token;
if (opts.hostToken !== undefined) config.hostToken = opts.hostToken;
return config;
}
function assertNotAborted(signal?: AbortSignal): void {
if (!signal?.aborted) return;
throw new DOMException("Operation aborted", "AbortError");
}
function delay(ms: number, signal?: AbortSignal): Promise<void> {
assertNotAborted(signal);
return new Promise((resolve, reject) => {
const cleanup = () => signal?.removeEventListener("abort", abort);
const timeout = setTimeout(() => {
cleanup();
resolve();
}, ms);
const abort = () => {
clearTimeout(timeout);
cleanup();
reject(new DOMException("Operation aborted", "AbortError"));
};
signal?.addEventListener("abort", abort, { once: true });
});
}
/** Main user-facing handle for a Wrenn capsule. */
export class Capsule {
private disposed = false;
private ownsRemote = false;
/** Capsule identifier used for all instance operations. */
readonly id: string;
/** Low-level client backing this capsule handle. */
readonly client: WrennClient;
/** Command execution helper bound to this capsule. */
readonly commands: CommandManager;
/** File operation helper bound to this capsule. */
readonly files: FileManager;
/** Git helper bound to this capsule. */
readonly git: Git;
/** Interactive terminal helper bound to this capsule. */
readonly pty: PtyManager;
/**
* Wraps an existing capsule ID without fetching or creating remote resources.
*
* @param id - Existing capsule identifier.
* @param opts - Optional client configuration.
*/
constructor(id: string, opts?: ClientConfig) {
this.id = id;
this.client = new WrennClient(opts);
this.commands = new CommandManager(id, this.client);
this.files = new FileManager(id, this.client);
this.git = new Git(this);
this.pty = new PtyManager(id, this.client);
}
static create(opts?: CapsuleCreateOptions): Promise<Capsule>;
static create(
template: string,
opts?: CapsuleCreateOptions,
): Promise<Capsule>;
/**
* Creates a capsule and returns a high-level capsule handle.
*
* @param templateOrOpts - Template name or creation options. Defaults to `minimal`.
* @param opts - Creation options when the first argument is a template name.
* @returns Capsule handle bound to the created capsule ID.
* @throws WrennError subclasses for unsuccessful API responses.
*/
static async create(
templateOrOpts?: string | CapsuleCreateOptions,
opts?: CapsuleCreateOptions,
): Promise<Capsule> {
const template =
typeof templateOrOpts === "string"
? templateOrOpts
: (templateOrOpts?.template ?? DEFAULT_TEMPLATE);
const createOpts =
typeof templateOrOpts === "string" ? opts : templateOrOpts;
const clientConfig = clientConfigFrom(createOpts);
const client = new WrennClient(clientConfig);
const body: OperationJsonBody<"createCapsule"> = {
memory_mb: createOpts?.memory_mb ?? DEFAULT_MEMORY_MB,
template,
timeout_sec: createOpts?.timeout_sec ?? DEFAULT_TIMEOUT_SEC,
vcpus: createOpts?.vcpus ?? DEFAULT_VCPUS,
};
const capsule = await client.capsules.create(body);
if (!capsule.id) {
throw new WrennError(
500,
"Created capsule response did not include an id",
);
}
return new Capsule(capsule.id, clientConfig).markOwned();
}
/**
* Wraps an existing capsule ID without validating it remotely.
*
* @param id - Existing capsule identifier.
* @param opts - Optional client configuration.
* @returns Capsule handle bound to the existing capsule ID.
*/
static connect(id: string, opts?: ClientConfig): Capsule {
return new Capsule(id, opts);
}
/**
* Destroys a capsule by ID without constructing an instance.
*
* @param id - Capsule identifier to destroy.
* @param opts - Optional client configuration.
* @returns Resolves when the capsule is destroyed.
* @throws WrennError subclasses for unsuccessful API responses.
*/
static destroy(id: string, opts?: ClientConfig): Promise<void> {
return new WrennClient(opts).capsules.destroy(id);
}
/**
* Fetches the latest capsule metadata.
*
* @returns Latest capsule metadata.
* @throws WrennError subclasses for unsuccessful API responses.
*/
getInfo(): Promise<CapsuleInfo> {
return this.client.capsules.get(this.id);
}
/**
* Polls capsule metadata until the capsule reaches `running`.
*
* @param opts - Optional timeout, interval, and abort signal.
* @returns Capsule metadata after it reaches `running`.
* @throws TimeoutError when the wait deadline expires.
* @throws WrennError when the capsule reaches a terminal non-running status.
*/
async waitForReady(opts?: WaitForReadyOptions): Promise<CapsuleInfo> {
const timeoutMs = opts?.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
const intervalMs = opts?.intervalMs ?? DEFAULT_WAIT_INTERVAL_MS;
const deadline = Date.now() + timeoutMs;
let isFirstPoll = true;
while (true) {
assertNotAborted(opts?.signal);
if (!isFirstPoll && Date.now() >= deadline) {
throw new TimeoutError(
`Timed out waiting for capsule ${this.id} to become running`,
);
}
const requestOpts = opts?.signal ? { signal: opts.signal } : undefined;
const capsule = await this.client.capsules.get(this.id, requestOpts);
isFirstPoll = false;
if (capsule.status === "running") return capsule;
if (capsule.status && TERMINAL_STATUSES.has(capsule.status)) {
throw new WrennError(
409,
`Capsule ${this.id} reached terminal status "${capsule.status}"`,
);
}
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) {
throw new TimeoutError(
`Timed out waiting for capsule ${this.id} to become running`,
);
}
await delay(Math.min(intervalMs, remainingMs), opts?.signal);
}
}
/**
* Destroys this capsule.
*
* @returns Resolves when the capsule is destroyed.
* @throws WrennError subclasses for unsuccessful API responses.
*/
async destroy(): Promise<void> {
if (this.disposed) return;
await this.client.capsules.destroy(this.id);
this.disposed = true;
}
/**
* Pauses this capsule and returns the updated capsule metadata.
*
* @returns Updated capsule metadata after pause.
* @throws WrennError subclasses for unsuccessful API responses.
*/
pause(): Promise<CapsuleInfo> {
return this.client.capsules.pause(this.id);
}
/**
* Resumes this capsule and optionally waits until it becomes ready.
*
* @param opts - Optional readiness wait settings.
* @returns Updated capsule metadata, or running metadata when `wait` is true.
* @throws TimeoutError when waiting for readiness times out.
* @throws WrennError subclasses for unsuccessful API responses.
*/
async resume(opts?: CapsuleResumeOptions): Promise<CapsuleInfo> {
const capsule = await this.client.capsules.resume(this.id);
if (!opts?.wait) return capsule;
return this.waitForReady(opts);
}
/**
* Resets this capsule's inactivity timer.
*
* @returns Resolves when the ping is accepted.
* @throws WrennError subclasses for unsuccessful API responses.
*/
ping(): Promise<void> {
return this.client.capsules.ping(this.id);
}
/**
* Fetches resource metrics for this capsule.
*
* @param opts - Optional metrics query parameters.
* @returns Resource metrics for the capsule.
* @throws WrennError subclasses for unsuccessful API responses.
*/
getMetrics(opts?: CapsuleMetricsOptions): Promise<CapsuleMetrics> {
return this.client.capsules.metrics(this.id, opts);
}
/** Local cleanup hook. This does not mutate or destroy the remote capsule. */
close(): void {}
/** Destroys capsules created by this SDK instance when used with `await using`. */
async [Symbol.asyncDispose](): Promise<void> {
if (this.ownsRemote) {
await this.destroy();
return;
}
this.close();
}
private markOwned(): this {
this.ownsRemote = true;
return this;
}
}
/** @deprecated Use {@link Capsule} instead. */
export const Sandbox = Capsule;

View File

@ -78,7 +78,7 @@ function createFileFormData(input: FileUploadInput): FormData {
const formData = new FormData();
formData.append("path", input.path);
if (typeof input.file === "string") {
formData.append("file", input.file);
formData.append("file", new Blob([input.file]), input.filename ?? "file");
} else {
if (input.filename) {
formData.append("file", input.file, input.filename);
@ -589,7 +589,7 @@ export class CapsulesResource extends BaseResource {
create(
body: JsonBody<"createCapsule">,
opts?: RequestOptions,
): Promise<JsonResponse<"createCapsule", 201>> {
): Promise<JsonResponse<"createCapsule", 202>> {
return this.http.post("/v1/capsules", body, opts);
}
@ -779,7 +779,7 @@ export class CapsulesResource extends BaseResource {
pause(
id: string,
opts?: RequestOptions,
): Promise<JsonResponse<"pauseCapsule", 200>> {
): Promise<JsonResponse<"pauseCapsule", 202>> {
return this.http.post(
`/v1/capsules/${encodePath(id)}/pause`,
undefined,
@ -798,7 +798,7 @@ export class CapsulesResource extends BaseResource {
resume(
id: string,
opts?: RequestOptions,
): Promise<JsonResponse<"resumeCapsule", 200>> {
): Promise<JsonResponse<"resumeCapsule", 202>> {
return this.http.post(
`/v1/capsules/${encodePath(id)}/resume`,
undefined,

View File

@ -0,0 +1,101 @@
import { Capsule, type CapsuleCreateOptions } from "../capsule.js";
import type { ClientConfig } from "../config.js";
/** Options for executing a Python cell through the code interpreter. */
export interface ExecCellOptions {
/** Command timeout in seconds. Defaults to 30. */
timeoutSec?: number;
}
/** Result returned after executing a Python cell. */
export interface ExecCellResult {
/** Captured standard output from Python. */
stdout: string;
/** Captured standard error from Python. */
stderr: string;
/** Process exit code returned by Python. */
exitCode: number | undefined;
}
/** Executes notebook-style Python cells inside a capsule. */
export class Notebook {
/**
* Creates a notebook helper bound to a capsule.
*
* @param capsule - Capsule used to run Python code.
*/
constructor(private readonly capsule: Capsule) {}
/**
* Executes Python code using the capsule's Python 3 CLI.
*
* @param code - Python source code to execute.
* @param opts - Optional execution timeout.
* @returns Captured stdout, stderr, and exit code.
* @throws WrennError subclasses for unsuccessful command execution requests.
*/
async execCell(
code: string,
opts?: ExecCellOptions,
): Promise<ExecCellResult> {
const result = await this.capsule.commands.exec("python3", {
args: ["-c", code],
timeoutSec: opts?.timeoutSec ?? 30,
});
return {
exitCode: result.exit_code,
stderr: result.stderr ?? "",
stdout: result.stdout ?? "",
};
}
}
/** Specialized capsule wrapper for Python/Jupyter code execution. */
export class CodeInterpreter {
/** Notebook-style Python execution helper. */
readonly notebook: Notebook;
/**
* Creates a code interpreter around an existing capsule instance.
*
* @param capsule - Capsule that provides command execution.
*/
constructor(readonly capsule: Capsule) {
this.notebook = new Notebook(capsule);
}
/**
* Creates a capsule using the Jupyter template by default.
*
* @param opts - Optional capsule creation and client configuration.
* @returns Code interpreter bound to the created capsule.
* @throws WrennError subclasses for unsuccessful API responses.
*/
static async create(opts?: CapsuleCreateOptions): Promise<CodeInterpreter> {
const template = opts?.template ?? "jupyter";
const capsule = await Capsule.create(template, opts);
return new CodeInterpreter(capsule);
}
/**
* Wraps an existing capsule ID without validating it remotely.
*
* @param id - Existing capsule identifier.
* @param opts - Optional client configuration.
* @returns Code interpreter bound to the existing capsule ID.
*/
static connect(id: string, opts?: ClientConfig): CodeInterpreter {
return new CodeInterpreter(Capsule.connect(id, opts));
}
/**
* Disposes the underlying capsule according to its ownership semantics.
*
* Interpreters created with {@link CodeInterpreter.create} destroy their remote
* capsule on disposal. Interpreters attached with {@link CodeInterpreter.connect}
* leave the remote capsule running.
*/
async [Symbol.asyncDispose](): Promise<void> {
await this.capsule[Symbol.asyncDispose]();
}
}

209
src/commands.ts Normal file
View File

@ -0,0 +1,209 @@
import { AsyncQueue } from "./_shared/async-queue.js";
import type { WsConnection } from "./_shared/websocket.js";
import type {
OperationJsonBody,
OperationJsonResponse,
WrennClient,
} from "./client.js";
/** Result returned by a foreground command execution. */
export type CommandResult = OperationJsonResponse<"execCommand", 200>;
/** Metadata returned when a command starts as a background process. */
export type BackgroundProcess = OperationJsonResponse<"execCommand", 202>;
/** Running process list returned by the capsule process endpoint. */
export type ProcessList = OperationJsonResponse<"listProcesses", 200>;
/** Options for foreground command execution. */
export interface CommandOptions {
/** Command-line arguments passed after the executable. */
args?: string[];
/** Server-side timeout in seconds. */
timeoutSec?: number;
/** Working directory inside the capsule. */
cwd?: string;
}
/** Options for starting a background command. */
export interface BackgroundCommandOptions extends CommandOptions {
/** Optional process tag used to reconnect or kill the process later. */
tag?: string;
/** Environment variables applied to the command process. */
envs?: Record<string, string>;
/** Working directory inside the capsule. */
cwd?: string;
}
/** Options for command WebSocket streaming. */
export interface CommandStreamOptions {
/** Command-line arguments sent with the stream start message. */
args?: string[];
/** WebSocket connection timeout in milliseconds. */
timeoutMs?: number;
}
/** Event emitted by command streaming endpoints. */
export interface CommandStreamEvent {
/** Event discriminator such as `start`, `stdout`, `stderr`, `exit`, or `error`. */
type: string;
/** Additional event fields returned by the server. */
[key: string]: unknown;
}
const DEFAULT_COMMAND_TIMEOUT_SEC = 30;
function commandBody(
cmd: string,
background: boolean,
opts?: BackgroundCommandOptions,
): OperationJsonBody<"execCommand"> {
const body: Partial<OperationJsonBody<"execCommand">> = { cmd };
if (background) body.background = true;
if (background || opts?.timeoutSec !== undefined) {
body.timeout_sec = opts?.timeoutSec ?? DEFAULT_COMMAND_TIMEOUT_SEC;
}
if (opts?.args) body.args = opts.args;
if (opts?.tag) body.tag = opts.tag;
if (opts?.envs) body.envs = opts.envs;
if (opts?.cwd) body.cwd = opts.cwd;
return body as OperationJsonBody<"execCommand">;
}
function isTerminalEvent(event: CommandStreamEvent): boolean {
return event.type === "exit" || event.type === "error";
}
/** User-friendly command execution API bound to one capsule. */
export class CommandManager {
/**
* Creates a command manager for one capsule.
*
* @param capsuleId - Capsule identifier to run commands in.
* @param client - Low-level client used for HTTP and WebSocket calls.
*/
constructor(
private readonly capsuleId: string,
private readonly client: WrennClient,
) {}
/**
* Executes a command and waits for foreground output.
*
* @param cmd - Executable or shell command to run.
* @param opts - Optional arguments, timeout, and working directory.
* @returns Command output, stderr, and exit status returned by the API.
* @throws WrennError subclasses for unsuccessful API responses.
*/
exec(cmd: string, opts?: CommandOptions): Promise<CommandResult> {
return this.client.capsules.exec(
this.capsuleId,
commandBody(cmd, false, opts),
) as Promise<CommandResult>;
}
/**
* Starts a command as a background process.
*
* @param cmd - Executable or shell command to run.
* @param opts - Optional arguments, tag, environment, timeout, and working directory.
* @returns Background process metadata, including PID or tag when returned.
* @throws WrennError subclasses for unsuccessful API responses.
*/
start(
cmd: string,
opts?: BackgroundCommandOptions,
): Promise<BackgroundProcess> {
return this.client.capsules.exec(
this.capsuleId,
commandBody(cmd, true, opts),
) as Promise<BackgroundProcess>;
}
/**
* Lists running background processes in the capsule.
*
* @returns Process list returned by the capsule API.
* @throws WrennError subclasses for unsuccessful API responses.
*/
list(): Promise<ProcessList> {
return this.client.capsules.listProcesses(this.capsuleId);
}
/**
* Kills a process by PID or tag.
*
* @param selector - Process PID or tag.
* @param signal - Optional POSIX signal. Defaults to server behavior when omitted.
* @returns Resolves when the process is killed.
* @throws WrennError subclasses for unsuccessful API responses.
*/
kill(selector: string, signal?: "SIGKILL" | "SIGTERM"): Promise<void> {
const params = signal ? { signal } : undefined;
return this.client.capsules.killProcess(this.capsuleId, selector, params);
}
/**
* Streams command events over WebSocket until an `exit` or `error` event arrives.
*
* @param cmd - Executable or shell command to run.
* @param opts - Optional arguments and WebSocket timeout.
* @returns Async generator of command stream events.
* @throws TimeoutError when the WebSocket connection times out.
*/
async *stream(
cmd: string,
opts?: CommandStreamOptions,
): AsyncGenerator<CommandStreamEvent> {
const queue = new AsyncQueue<CommandStreamEvent>();
let connection: WsConnection | undefined;
const socketOpts: Parameters<typeof this.client.capsules.execStream>[1] = {
onClose: () => queue.end(),
onError: (error) => queue.fail(error),
onMessage: (message) => {
const event = message as CommandStreamEvent;
queue.push(event);
if (isTerminalEvent(event)) queue.end();
},
};
if (opts?.timeoutMs !== undefined) socketOpts.timeoutMs = opts.timeoutMs;
connection = await this.client.capsules.execStream(
this.capsuleId,
socketOpts,
);
const start: CommandStreamEvent = { cmd, type: "start" };
if (opts?.args) start.args = opts.args;
connection.send(start);
try {
yield* queue;
} finally {
if (!connection.isClosed) connection.close();
}
}
/**
* Connects to an existing background process stream.
*
* @param selector - Process PID or tag.
* @param opts - Optional WebSocket timeout.
* @returns Established WebSocket connection for the process stream.
* @throws TimeoutError when the WebSocket connection times out.
*/
streamProcess(
selector: string,
opts?: Pick<CommandStreamOptions, "timeoutMs">,
): Promise<WsConnection> {
const socketOpts: Parameters<
typeof this.client.capsules.connectProcess
>[2] = {
onMessage: () => undefined,
};
if (opts?.timeoutMs !== undefined) socketOpts.timeoutMs = opts.timeoutMs;
return this.client.capsules.connectProcess(
this.capsuleId,
selector,
socketOpts,
);
}
}

View File

@ -1,5 +1,5 @@
/** Default Wrenn API origin used when no base URL is supplied. */
export const DEFAULT_BASE_URL = "https://api.wrenn.dev";
export const DEFAULT_BASE_URL = "https://app.wrenn.dev/api";
/** Environment variable used for API-key authentication. */
export const ENV_API_KEY = "WRENN_API_KEY";

143
src/files.ts Normal file
View File

@ -0,0 +1,143 @@
import type { OperationJsonResponse, WrennClient } from "./client.js";
/** Directory listing returned by capsule file APIs. */
export type FileList = OperationJsonResponse<"listDir", 200>;
/** Directory creation response returned by capsule file APIs. */
export type MakeDirectoryResult = OperationJsonResponse<"makeDir", 200>;
/** Options for listing capsule directory contents. */
export interface ListFilesOptions {
/** Maximum traversal depth. Defaults to `1`. */
depth?: number;
}
/** File content accepted by high-level upload helpers. */
export type FileContent = Blob | string | Uint8Array | ArrayBuffer;
async function streamToBuffer(
stream: ReadableStream<Uint8Array>,
): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
function uploadContent(content: FileContent): Blob | string {
if (typeof content === "string" || content instanceof Blob) return content;
if (content instanceof ArrayBuffer) return new Blob([content]);
const buffer = new ArrayBuffer(content.byteLength);
new Uint8Array(buffer).set(content);
return new Blob([buffer]);
}
/** User-friendly file API bound to one capsule. */
export class FileManager {
/**
* Creates a file manager for one capsule.
*
* @param capsuleId - Capsule identifier to operate on.
* @param client - Low-level client used for file endpoint calls.
*/
constructor(
private readonly capsuleId: string,
private readonly client: WrennClient,
) {}
/**
* Reads a file into a buffer.
*
* @param path - Absolute path inside the capsule.
* @returns File contents as a Node.js `Buffer`.
* @throws WrennError subclasses for unsuccessful API responses.
*/
async read(path: string): Promise<Buffer> {
return streamToBuffer(
await this.client.files.download(this.capsuleId, { path }),
);
}
/**
* Writes content to a file.
*
* @param path - Absolute destination path inside the capsule.
* @param content - Text, binary, or blob content to upload.
* @returns Resolves when the upload completes.
* @throws WrennError subclasses for unsuccessful API responses.
*/
write(path: string, content: FileContent): Promise<void> {
return this.client.files.upload(this.capsuleId, {
file: uploadContent(content),
path,
});
}
/**
* Lists directory contents.
*
* @param path - Directory path inside the capsule.
* @param opts - Optional listing depth.
* @returns Directory listing response.
* @throws WrennError subclasses for unsuccessful API responses.
*/
list(path: string, opts?: ListFilesOptions): Promise<FileList> {
return this.client.files.list(this.capsuleId, {
depth: opts?.depth ?? 1,
path,
});
}
/**
* Creates a directory.
*
* @param path - Directory path inside the capsule.
* @returns Directory creation response.
* @throws WrennError subclasses for unsuccessful API responses.
*/
mkdir(path: string): Promise<MakeDirectoryResult> {
return this.client.files.mkdir(this.capsuleId, { path });
}
/**
* Removes a file or directory.
*
* @param path - Path inside the capsule to remove.
* @returns Resolves when the path is removed.
* @throws WrennError subclasses for unsuccessful API responses.
*/
remove(path: string): Promise<void> {
return this.client.files.remove(this.capsuleId, { path });
}
/**
* Downloads a file as an async stream of buffers.
*
* @param path - Absolute path inside the capsule.
* @returns Async generator yielding file content chunks.
* @throws WrennError subclasses for unsuccessful API responses.
*/
async *downloadStream(path: string): AsyncGenerator<Buffer> {
const stream = await this.client.files.streamDownload(this.capsuleId, {
path,
});
for await (const chunk of stream) {
yield Buffer.from(chunk);
}
}
/**
* Uploads content through the streaming upload endpoint.
*
* @param path - Absolute destination path inside the capsule.
* @param content - Text, binary, or blob content to upload.
* @returns Resolves when the upload completes.
* @throws WrennError subclasses for unsuccessful API responses.
*/
uploadStream(path: string, content: FileContent): Promise<void> {
return this.client.files.streamUpload(this.capsuleId, {
file: uploadContent(content),
path,
});
}
}

170
src/git/index.ts Normal file
View File

@ -0,0 +1,170 @@
import type { Capsule } from "../capsule.js";
import type { CommandOptions, CommandResult } from "../commands.js";
export interface GitOptions {
/** Working directory used for repository-scoped git commands. */
cwd?: string;
}
export interface GitCloneOptions {
/** Destination path for the cloned repository. */
path?: string;
/** Branch to check out during clone. */
branch?: string;
}
export interface GitRemoteBranchOptions extends GitOptions {
/** Remote name. Defaults to git's configured default when omitted. */
remote?: string;
/** Branch name. Defaults to git's configured default when omitted. */
branch?: string;
}
export interface GitLogOptions extends GitOptions {
/** Maximum number of commits to return. */
maxCount?: number;
}
export interface GitCheckoutOptions extends GitOptions {
/** Create the branch before checking it out. */
create?: boolean;
}
/** High-level Git helper that runs git CLI commands inside a capsule. */
export class Git {
/**
* Creates a Git helper bound to one capsule.
*
* @param capsule - Capsule used to execute git commands.
* @param opts - Default git command options, such as repository working directory.
*/
constructor(
private readonly capsule: Capsule,
private readonly opts: GitOptions = {},
) {}
/**
* Clones a repository into the capsule.
*
* @param url - Repository URL accepted by `git clone`.
* @param opts - Optional destination path and branch.
* @returns Command result from the capsule.
*/
clone(url: string, opts?: GitCloneOptions): Promise<CommandResult> {
const args = ["clone"];
if (opts?.branch) args.push("--branch", opts.branch);
args.push(url);
if (opts?.path) args.push(opts.path);
return this.run(args);
}
/**
* Returns porcelain status for the current repository.
*
* @param opts - Optional repository working directory.
* @returns Command result containing `git status --short` output.
*/
status(opts?: GitOptions): Promise<CommandResult> {
return this.run(["status", "--short"], opts);
}
/**
* Pulls changes from a remote branch.
*
* @param opts - Optional repository working directory, remote, and branch.
* @returns Command result from `git pull`.
*/
pull(opts?: GitRemoteBranchOptions): Promise<CommandResult> {
return this.runRemoteBranch("pull", opts);
}
/**
* Pushes changes to a remote branch.
*
* @param opts - Optional repository working directory, remote, and branch.
* @returns Command result from `git push`.
*/
push(opts?: GitRemoteBranchOptions): Promise<CommandResult> {
return this.runRemoteBranch("push", opts);
}
/**
* Returns compact commit history.
*
* @param opts - Optional repository working directory and commit limit.
* @returns Command result containing `git log --oneline` output.
*/
log(opts?: GitLogOptions): Promise<CommandResult> {
const args = ["log", "--oneline"];
if (opts?.maxCount !== undefined) args.push("-n", String(opts.maxCount));
return this.run(args, opts);
}
/**
* Returns the current branch name.
*
* @param opts - Optional repository working directory.
* @returns Command result containing the current branch name.
*/
branch(opts?: GitOptions): Promise<CommandResult> {
return this.run(["branch", "--show-current"], opts);
}
/**
* Checks out an existing branch or creates it when requested.
*
* @param branch - Branch name to check out.
* @param opts - Optional repository working directory and create flag.
* @returns Command result from `git checkout`.
*/
checkout(branch: string, opts?: GitCheckoutOptions): Promise<CommandResult> {
const args = opts?.create
? ["checkout", "-b", branch]
: ["checkout", branch];
return this.run(args, opts);
}
/**
* Stages one or more files.
*
* @param files - File path or file paths to stage.
* @param opts - Optional repository working directory.
* @returns Command result from `git add`.
* @throws Error when no files are provided.
*/
add(files: string | string[], opts?: GitOptions): Promise<CommandResult> {
const fileList = Array.isArray(files) ? files : [files];
if (!fileList.length) {
return Promise.reject(new Error("At least one file is required"));
}
return this.run(["add", ...fileList], opts);
}
/**
* Creates a commit. Git user identity must already be configured inside the capsule.
*
* @param message - Commit message passed to `git commit -m`.
* @param opts - Optional repository working directory.
* @returns Command result from `git commit`.
*/
commit(message: string, opts?: GitOptions): Promise<CommandResult> {
return this.run(["commit", "-m", message], opts);
}
private run(args: string[], opts?: GitOptions): Promise<CommandResult> {
const commandOpts: CommandOptions = { args };
const cwd = opts?.cwd ?? this.opts.cwd;
if (cwd) commandOpts.cwd = cwd;
return this.capsule.commands.exec("git", commandOpts);
}
private runRemoteBranch(
command: "pull" | "push",
opts?: GitRemoteBranchOptions,
): Promise<CommandResult> {
const args: string[] = [command];
if (opts?.remote) args.push(opts.remote);
if (opts?.branch) args.push(opts.branch);
return this.run(args, opts);
}
}

View File

@ -1,7 +1,12 @@
export type { HttpClientConfig, RequestOptions } from "./_shared/http.js";
export { HttpClient } from "./_shared/http.js";
export type { WsConnectionOpts } from "./_shared/websocket.js";
export { WsConnection } from "./_shared/websocket.js";
export type {
CapsuleCreateOptions,
CapsuleInfo,
CapsuleMetrics,
CapsuleMetricsOptions,
CapsuleResumeOptions,
WaitForReadyOptions,
} from "./capsule.js";
export { Capsule, Sandbox } from "./capsule.js";
export type {
FileUploadInput,
OperationJsonBody,
@ -22,6 +27,21 @@ export {
UsersResource,
WrennClient,
} from "./client.js";
export type {
ExecCellOptions,
ExecCellResult,
} from "./code-interpreter/index.js";
export { CodeInterpreter, Notebook } from "./code-interpreter/index.js";
export type {
BackgroundCommandOptions,
BackgroundProcess,
CommandOptions,
CommandResult,
CommandStreamEvent,
CommandStreamOptions,
ProcessList,
} from "./commands.js";
export { CommandManager } from "./commands.js";
export type { ClientConfig, ResolvedClientConfig } from "./config.js";
export {
DEFAULT_BASE_URL,
@ -44,3 +64,27 @@ export {
throwErrorFromResponse,
WrennError,
} from "./exceptions.js";
export type {
FileContent,
FileList,
ListFilesOptions,
MakeDirectoryResult,
} from "./files.js";
export { FileManager } from "./files.js";
export type {
GitCheckoutOptions,
GitCloneOptions,
GitLogOptions,
GitOptions,
GitRemoteBranchOptions,
} from "./git/index.js";
export { Git } from "./git/index.js";
export type {
$defs,
components,
operations,
paths,
webhooks,
} from "./models/generated.js";
export type { PtyEvent, PtyStartOptions } from "./pty.js";
export { PtyManager, PtySession } from "./pty.js";

View File

@ -718,7 +718,7 @@ export interface paths {
/**
* Resume a paused capsule
* @description Restores a paused capsule from its snapshot using UFFD for lazy
* memory loading. Boots a fresh Firecracker process, sets up a new
* memory loading. Boots a fresh Cloud Hypervisor process, sets up a new
* network slot, and waits for envd to become ready.
*/
post: operations["resumeCapsule"];
@ -1146,6 +1146,28 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/hosts/sandbox-events": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Sandbox lifecycle event callback
* @description Receives autonomous lifecycle events from host agents (e.g. auto-pause
* from the TTL reaper). The event is published to an internal Redis stream
* for the control plane's event consumer to process.
*/
post: operations["sandboxEventCallback"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/hosts/auth/refresh": {
parameters: {
query?: never;
@ -1312,6 +1334,29 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/admin/users/{id}/admin": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/**
* Grant or revoke platform admin
* @description Sets the platform admin flag on a user. Cannot remove the last admin.
* Requires platform admin access (JWT + is_admin).
* The target user's JWT is not re-issued — their frontend will reflect the
* change on next login or team switch.
*/
put: operations["setUserAdmin"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
@ -1408,7 +1453,7 @@ export interface components {
Capsule: {
id?: string;
/** @enum {string} */
status?: "pending" | "starting" | "running" | "paused" | "hibernated" | "stopped" | "missing" | "error";
status?: "pending" | "starting" | "running" | "pausing" | "paused" | "resuming" | "stopping" | "hibernated" | "stopped" | "missing" | "error";
template?: string;
vcpus?: number;
memory_mb?: number;
@ -1667,7 +1712,7 @@ export interface components {
cpu_pct?: number;
/**
* Format: int64
* @description Resident memory in bytes (VmRSS of Firecracker process)
* @description Resident memory in bytes (VmRSS of Cloud Hypervisor process)
*/
mem_bytes?: number;
/**
@ -1743,7 +1788,7 @@ export interface components {
};
};
responses: {
/** @description Invalid request */
/** @description Invalid request parameters */
BadRequest: {
headers: {
[name: string]: unknown;
@ -2746,8 +2791,8 @@ export interface operations {
};
};
responses: {
/** @description Capsule created */
201: {
/** @description Capsule creation initiated (status will be "starting") */
202: {
headers: {
[name: string]: unknown;
};
@ -2858,8 +2903,8 @@ export interface operations {
};
requestBody?: never;
responses: {
/** @description Capsule destroyed */
204: {
/** @description Capsule destruction initiated */
202: {
headers: {
[name: string]: unknown;
};
@ -3117,8 +3162,8 @@ export interface operations {
};
requestBody?: never;
responses: {
/** @description Capsule paused (snapshot taken, resources released) */
200: {
/** @description Capsule pause initiated (status will be "pausing") */
202: {
headers: {
[name: string]: unknown;
};
@ -3148,8 +3193,8 @@ export interface operations {
};
requestBody?: never;
responses: {
/** @description Capsule resumed (new VM booted from snapshot) */
200: {
/** @description Capsule resume initiated (status will be "resuming") */
202: {
headers: {
[name: string]: unknown;
};
@ -3895,6 +3940,53 @@ export interface operations {
};
};
};
sandboxEventCallback: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @enum {string} */
event: "sandbox.auto_paused";
sandbox_id: string;
host_id: string;
/** Format: int64 */
timestamp?: number;
};
};
};
responses: {
/** @description Event accepted */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Host ID mismatch */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
};
};
refreshHostToken: {
parameters: {
query?: never;
@ -4249,4 +4341,50 @@ export interface operations {
};
};
};
setUserAdmin: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description true to grant admin, false to revoke. */
admin: boolean;
};
};
};
responses: {
/** @description Admin status updated */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
400: components["responses"]["BadRequest"];
/** @description Caller is not a platform admin */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description User not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
};
};
}

157
src/pty.ts Normal file
View File

@ -0,0 +1,157 @@
import { AsyncQueue } from "./_shared/async-queue.js";
import type { WsConnection } from "./_shared/websocket.js";
import type { WrennClient } from "./client.js";
/** Options for starting a PTY session inside a capsule. */
export interface PtyStartOptions {
/** Command to start. Defaults to server shell behavior when omitted. */
cmd?: string;
/** Command-line arguments passed after `cmd`. */
args?: string[];
/** Initial terminal column count. */
cols?: number;
/** Initial terminal row count. */
rows?: number;
/** Environment variables applied to the PTY process. */
envs?: Record<string, string>;
/** Working directory inside the capsule. */
cwd?: string;
/** User to run the PTY process as, when supported by the backend. */
user?: string;
/** WebSocket connection timeout in milliseconds. */
timeoutMs?: number;
}
/** Event emitted by an interactive PTY session. */
export interface PtyEvent {
/** Event discriminator such as `data`, `exit`, or `error`. */
type: string;
/** Additional event fields returned by the server. */
[key: string]: unknown;
}
/** Active interactive terminal session backed by a WebSocket connection. */
export class PtySession {
/** Async iterator of PTY events received from the server. */
readonly events: AsyncIterableIterator<PtyEvent>;
/**
* Creates a PTY session wrapper around an established WebSocket.
*
* @param connection - WebSocket connection used for PTY control messages.
* @param queue - Queue of server events exposed as `events`.
*/
constructor(
private readonly connection: WsConnection,
queue: AsyncQueue<PtyEvent>,
) {
this.events = queue;
}
/**
* Sends terminal input to the PTY process.
*
* @param data - Text or bytes to send. Bytes are base64-encoded for transport.
*/
input(data: string | Uint8Array): void {
this.connection.send({
data: Buffer.from(data).toString("base64"),
type: "input",
});
}
/**
* Resizes the remote terminal.
*
* @param cols - New terminal column count.
* @param rows - New terminal row count.
*/
resize(cols: number, rows: number): void {
this.connection.send({ cols, rows, type: "resize" });
}
/** Sends a kill control message for the PTY process. */
kill(): void {
this.connection.send({ type: "kill" });
}
/** Closes the underlying WebSocket connection. */
close(): void {
this.connection.close();
}
/** Closes the underlying WebSocket when used with `await using`. */
async [Symbol.asyncDispose](): Promise<void> {
this.close();
}
}
/** Interactive terminal API bound to one capsule. */
export class PtyManager {
/**
* Creates a PTY manager for one capsule.
*
* @param capsuleId - Capsule identifier to open PTY sessions in.
* @param client - Low-level client used to open the PTY WebSocket.
*/
constructor(
private readonly capsuleId: string,
private readonly client: WrennClient,
) {}
/**
* Starts a new interactive PTY session.
*
* @param opts - Optional command, terminal size, environment, user, and timeout.
* @returns Active PTY session with input, resize, kill, close, and event APIs.
* @throws TimeoutError when the WebSocket connection times out.
*/
async start(opts?: PtyStartOptions): Promise<PtySession> {
const session = await this.open(opts?.timeoutMs);
const message: PtyEvent = { type: "start" };
if (opts?.cmd) message.cmd = opts.cmd;
if (opts?.args) message.args = opts.args;
if (opts?.cols) message.cols = opts.cols;
if (opts?.rows) message.rows = opts.rows;
if (opts?.envs) message.envs = opts.envs;
if (opts?.cwd) message.cwd = opts.cwd;
if (opts?.user) message.user = opts.user;
session.connection.send(message);
return session.value;
}
/**
* Reconnects to a tagged PTY session.
*
* @param tag - Server-side PTY session tag.
* @param opts - Optional WebSocket timeout.
* @returns Active PTY session connected to the existing tag.
* @throws TimeoutError when the WebSocket connection times out.
*/
async connect(
tag: string,
opts?: Pick<PtyStartOptions, "timeoutMs">,
): Promise<PtySession> {
const session = await this.open(opts?.timeoutMs);
session.connection.send({ tag, type: "connect" });
return session.value;
}
private async open(timeoutMs?: number): Promise<{
connection: WsConnection;
value: PtySession;
}> {
const queue = new AsyncQueue<PtyEvent>();
const socketOpts: Parameters<typeof this.client.capsules.ptySession>[1] = {
onClose: () => queue.end(),
onError: (error) => queue.fail(error),
onMessage: (message) => queue.push(message as PtyEvent),
};
if (timeoutMs !== undefined) socketOpts.timeoutMs = timeoutMs;
const connection = await this.client.capsules.ptySession(
this.capsuleId,
socketOpts,
);
return { connection, value: new PtySession(connection, queue) };
}
}

303
tests/capsule.test.ts Normal file
View File

@ -0,0 +1,303 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { Capsule, Sandbox } from "../src/capsule.js";
interface CapturedRequest {
url: string;
init: RequestInit;
}
function setupFetch(responses: Response[]) {
const calls: CapturedRequest[] = [];
const fetchMock = vi.fn(
async (url: string | URL | Request, init?: RequestInit) => {
calls.push({ url: String(url), init: init ?? {} });
const response = responses.shift();
if (!response) throw new Error("No mock response configured");
return response;
},
);
vi.stubGlobal("fetch", fetchMock);
return { calls, fetchMock };
}
function capsuleResponse(id: string, status = "running") {
return Response.json({
id,
memory_mb: 512,
status,
template: "minimal",
timeout_sec: 0,
vcpus: 1,
});
}
describe("Capsule", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
vi.useRealTimers();
});
it("wraps an existing capsule without fetching it", () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const capsule = Capsule.connect("cap_1", {
apiKey: "api-key",
baseUrl: "https://api.example.com",
});
expect(capsule.id).toBe("cap_1");
expect(fetchMock).not.toHaveBeenCalled();
expect(capsule).toBeInstanceOf(Capsule);
});
it("creates a capsule from defaulted options", async () => {
const { calls } = setupFetch([capsuleResponse("cap_created")]);
const capsule = await Capsule.create({
apiKey: "api-key",
baseUrl: "https://api.example.com/",
});
expect(capsule.id).toBe("cap_created");
expect(calls.at(-1)?.url).toBe("https://api.example.com/v1/capsules");
expect(calls.at(-1)?.init.method).toBe("POST");
expect(calls.at(-1)?.init.body).toBe(
JSON.stringify({
memory_mb: 512,
template: "minimal",
timeout_sec: 0,
vcpus: 1,
}),
);
});
it("creates a capsule with a template overload and explicit resources", async () => {
const { calls } = setupFetch([capsuleResponse("cap_custom")]);
const capsule = await Capsule.create("node", {
apiKey: "api-key",
baseUrl: "https://api.example.com",
memory_mb: 1024,
timeout_sec: 60,
vcpus: 2,
});
expect(capsule.id).toBe("cap_custom");
expect(calls.at(-1)?.init.body).toBe(
JSON.stringify({
memory_mb: 1024,
template: "node",
timeout_sec: 60,
vcpus: 2,
}),
);
});
it("throws when create does not return an id", async () => {
setupFetch([Response.json({ status: "running" })]);
await expect(
Capsule.create({ baseUrl: "https://api.example.com" }),
).rejects.toThrow("Created capsule response did not include an id");
});
it("maps instance lifecycle methods to the low-level client", async () => {
const { calls } = setupFetch([
capsuleResponse("cap_1"),
Response.json({ sandbox_id: "cap_1", range: "10m", points: [] }),
new Response(null, { status: 204 }),
capsuleResponse("cap_1", "pausing"),
capsuleResponse("cap_1", "resuming"),
new Response(null, { status: 204 }),
]);
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
await expect(capsule.getInfo()).resolves.toMatchObject({ id: "cap_1" });
await expect(capsule.getMetrics({ range: "10m" })).resolves.toMatchObject({
range: "10m",
});
await expect(capsule.ping()).resolves.toBeUndefined();
await expect(capsule.pause()).resolves.toMatchObject({ status: "pausing" });
await expect(capsule.resume()).resolves.toMatchObject({
status: "resuming",
});
await expect(capsule.destroy()).resolves.toBeUndefined();
expect(calls.map((call) => `${call.init.method} ${call.url}`)).toEqual([
"GET https://api.example.com/v1/capsules/cap_1",
"GET https://api.example.com/v1/capsules/cap_1/metrics?range=10m",
"POST https://api.example.com/v1/capsules/cap_1/ping",
"POST https://api.example.com/v1/capsules/cap_1/pause",
"POST https://api.example.com/v1/capsules/cap_1/resume",
"DELETE https://api.example.com/v1/capsules/cap_1",
]);
});
it("waits until the capsule is running", async () => {
vi.useFakeTimers();
const { calls } = setupFetch([
capsuleResponse("cap_1", "pending"),
capsuleResponse("cap_1", "starting"),
capsuleResponse("cap_1", "running"),
]);
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const ready = capsule.waitForReady({ intervalMs: 100, timeoutMs: 1_000 });
await vi.advanceTimersByTimeAsync(100);
await vi.advanceTimersByTimeAsync(100);
await expect(ready).resolves.toMatchObject({ status: "running" });
expect(calls).toHaveLength(3);
});
it.each([
"error",
"missing",
"stopping",
"stopped",
])("fails waitForReady on terminal capsule state %s", async (status) => {
setupFetch([capsuleResponse("cap_1", status)]);
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
await expect(capsule.waitForReady()).rejects.toThrow(
`Capsule cap_1 reached terminal status "${status}"`,
);
});
it("times out while waiting for readiness", async () => {
vi.useFakeTimers();
setupFetch([
capsuleResponse("cap_1", "starting"),
capsuleResponse("cap_1", "starting"),
]);
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const ready = capsule.waitForReady({ intervalMs: 100, timeoutMs: 150 });
const assertion = expect(ready).rejects.toThrow(
"Timed out waiting for capsule cap_1 to become running",
);
await vi.advanceTimersByTimeAsync(100);
await vi.advanceTimersByTimeAsync(50);
await assertion;
});
it("resumes and waits until the capsule is running", async () => {
vi.useFakeTimers();
const { calls } = setupFetch([
capsuleResponse("cap_1", "resuming"),
capsuleResponse("cap_1", "running"),
]);
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const ready = capsule.resume({
wait: true,
intervalMs: 100,
timeoutMs: 1_000,
});
await vi.advanceTimersByTimeAsync(100);
await expect(ready).resolves.toMatchObject({ status: "running" });
expect(calls.map((call) => `${call.init.method} ${call.url}`)).toEqual([
"POST https://api.example.com/v1/capsules/cap_1/resume",
"GET https://api.example.com/v1/capsules/cap_1",
]);
});
it("aborts waitForReady when the signal is already aborted", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const controller = new AbortController();
controller.abort();
await expect(
capsule.waitForReady({ signal: controller.signal }),
).rejects.toThrow("Operation aborted");
});
it("aborts waitForReady when the signal fires during polling", async () => {
vi.useFakeTimers();
setupFetch([capsuleResponse("cap_1", "starting")]);
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const controller = new AbortController();
const ready = capsule.waitForReady({
intervalMs: 100,
timeoutMs: 5_000,
signal: controller.signal,
});
controller.abort();
await expect(ready).rejects.toThrow("Operation aborted");
});
it("does not mutate the remote capsule when connected capsules are closed or disposed", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const capsule = Capsule.connect("cap_1", {
baseUrl: "https://api.example.com",
});
capsule.close();
await capsule[Symbol.asyncDispose]();
expect(fetchMock).not.toHaveBeenCalled();
});
it("destroys created capsules when async disposed", async () => {
const { calls } = setupFetch([
capsuleResponse("cap_created"),
new Response(null, { status: 204 }),
]);
const capsule = await Capsule.create({
baseUrl: "https://api.example.com",
});
await capsule[Symbol.asyncDispose]();
expect(calls.map((call) => `${call.init.method} ${call.url}`)).toEqual([
"POST https://api.example.com/v1/capsules",
"DELETE https://api.example.com/v1/capsules/cap_created",
]);
});
it("does not destroy an already destroyed owned capsule twice", async () => {
const { calls } = setupFetch([
capsuleResponse("cap_created"),
new Response(null, { status: 204 }),
]);
const capsule = await Capsule.create({
baseUrl: "https://api.example.com",
});
await capsule.destroy();
await capsule[Symbol.asyncDispose]();
expect(calls.map((call) => `${call.init.method} ${call.url}`)).toEqual([
"POST https://api.example.com/v1/capsules",
"DELETE https://api.example.com/v1/capsules/cap_created",
]);
});
it("exports Sandbox as a deprecated Capsule alias", () => {
expect(Sandbox).toBe(Capsule);
});
});

View File

@ -323,6 +323,9 @@ describe("WrennClient", () => {
url: "https://api.example.com/v1/capsules/cap_1/files/write",
});
expect(calls.at(-1)?.init.body).toBeInstanceOf(FormData);
expect((calls.at(-1)?.init.body as FormData).get("file")).toBeInstanceOf(
Blob,
);
await client.files.download("cap_1", {} as never);
expectLastCall(calls, {
body: {},

View File

@ -0,0 +1,89 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { Capsule } from "../src/capsule.js";
import { CodeInterpreter } from "../src/code-interpreter/index.js";
describe("CodeInterpreter", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("creates a capsule with the jupyter template by default", async () => {
const create = vi.spyOn(Capsule, "create").mockResolvedValue(
new Capsule("cap_1", {
baseUrl: "https://api.example.com",
}),
);
const interpreter = await CodeInterpreter.create({
baseUrl: "https://api.example.com",
});
expect(interpreter.capsule.id).toBe("cap_1");
expect(create).toHaveBeenCalledWith("jupyter", {
baseUrl: "https://api.example.com",
});
});
it("connects to an existing capsule", () => {
const interpreter = CodeInterpreter.connect("cap_1", {
baseUrl: "https://api.example.com",
});
expect(interpreter.capsule.id).toBe("cap_1");
});
it("executes notebook cells through python3", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const exec = vi.spyOn(capsule.commands, "exec").mockResolvedValue({
exit_code: 0,
stderr: "",
stdout: "42\n",
});
const interpreter = new CodeInterpreter(capsule);
await expect(
interpreter.notebook.execCell("print(6 * 7)"),
).resolves.toEqual({
exitCode: 0,
stderr: "",
stdout: "42\n",
});
expect(exec).toHaveBeenCalledWith("python3", {
args: ["-c", "print(6 * 7)"],
timeoutSec: 30,
});
});
it("returns stderr and non-zero exit code on failure", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
vi.spyOn(capsule.commands, "exec").mockResolvedValue({
exit_code: 1,
stderr: "SyntaxError: invalid syntax\n",
stdout: "",
});
const interpreter = new CodeInterpreter(capsule);
await expect(interpreter.notebook.execCell("invalid(")).resolves.toEqual({
exitCode: 1,
stderr: "SyntaxError: invalid syntax\n",
stdout: "",
});
});
it("disposes the wrapped capsule", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const dispose = vi.spyOn(capsule, Symbol.asyncDispose).mockResolvedValue();
const interpreter = new CodeInterpreter(capsule);
await interpreter[Symbol.asyncDispose]();
expect(dispose).toHaveBeenCalledOnce();
});
});

160
tests/commands.test.ts Normal file
View File

@ -0,0 +1,160 @@
import { describe, expect, it, vi } from "vitest";
import { Capsule } from "../src/capsule.js";
describe("CommandManager", () => {
it("executes foreground commands through the capsule client", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const exec = vi.spyOn(capsule.client.capsules, "exec").mockResolvedValue({
exit_code: 0,
stdout: "ok\n",
});
await expect(
capsule.commands.exec("node", {
args: ["--version"],
timeoutSec: 5,
}),
).resolves.toMatchObject({ stdout: "ok\n" });
expect(exec).toHaveBeenCalledWith("cap_1", {
args: ["--version"],
cmd: "node",
timeout_sec: 5,
});
});
it("starts, lists, and kills background processes", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const exec = vi.spyOn(capsule.client.capsules, "exec").mockResolvedValue({
pid: 123,
tag: "worker",
});
const listProcesses = vi
.spyOn(capsule.client.capsules, "listProcesses")
.mockResolvedValue({ processes: [{ pid: 123, tag: "worker" }] });
const killProcess = vi
.spyOn(capsule.client.capsules, "killProcess")
.mockResolvedValue(undefined);
await expect(
capsule.commands.start("sleep", {
args: ["60"],
cwd: "/tmp",
envs: { A: "1" },
tag: "worker",
}),
).resolves.toMatchObject({ tag: "worker" });
await expect(capsule.commands.list()).resolves.toMatchObject({
processes: [{ tag: "worker" }],
});
await expect(
capsule.commands.kill("worker", "SIGTERM"),
).resolves.toBeUndefined();
expect(exec).toHaveBeenCalledWith("cap_1", {
args: ["60"],
background: true,
cmd: "sleep",
cwd: "/tmp",
envs: { A: "1" },
tag: "worker",
timeout_sec: 30,
});
expect(listProcesses).toHaveBeenCalledWith("cap_1");
expect(killProcess).toHaveBeenCalledWith("cap_1", "worker", {
signal: "SIGTERM",
});
});
it("connects to an existing background process stream", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const mockConnection = {
close: vi.fn(),
get isClosed() {
return false;
},
send: vi.fn(),
};
const connectProcess = vi
.spyOn(capsule.client.capsules, "connectProcess")
.mockResolvedValue(mockConnection as never);
const result = await capsule.commands.streamProcess("123");
expect(connectProcess).toHaveBeenCalledWith("cap_1", "123", {
onMessage: expect.any(Function),
});
expect(result).toBe(mockConnection);
});
it("passes timeout to streamProcess", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const connectProcess = vi
.spyOn(capsule.client.capsules, "connectProcess")
.mockResolvedValue({
close: vi.fn(),
get isClosed() {
return false;
},
send: vi.fn(),
} as never);
await capsule.commands.streamProcess("worker", { timeoutMs: 5000 });
expect(connectProcess).toHaveBeenCalledWith("cap_1", "worker", {
onMessage: expect.any(Function),
timeoutMs: 5000,
});
});
it("streams command events over the exec WebSocket", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const sent: unknown[] = [];
let onMessage: ((message: unknown) => void) | undefined;
vi.spyOn(capsule.client.capsules, "execStream").mockImplementation(
async (_id, opts) => {
onMessage = opts.onMessage;
return {
close: vi.fn(),
get isClosed() {
return false;
},
send: (message: unknown) => sent.push(message),
} as never;
},
);
const events = capsule.commands.stream("printf", { args: ["hello"] });
const first = events.next();
await vi.waitFor(() => expect(sent).toHaveLength(1));
expect(sent).toEqual([{ args: ["hello"], cmd: "printf", type: "start" }]);
onMessage?.({ data: "hello", type: "stdout" });
await expect(first).resolves.toEqual({
done: false,
value: { data: "hello", type: "stdout" },
});
const done = events.next();
onMessage?.({ exit_code: 0, type: "exit" });
await expect(done).resolves.toEqual({
done: false,
value: { exit_code: 0, type: "exit" },
});
await expect(events.next()).resolves.toEqual({
done: true,
value: undefined,
});
});
});

92
tests/files.test.ts Normal file
View File

@ -0,0 +1,92 @@
import { describe, expect, it, vi } from "vitest";
import { Capsule } from "../src/capsule.js";
function streamFrom(text: string): ReadableStream<Uint8Array> {
return new ReadableStream({
start(controller) {
controller.enqueue(Buffer.from(text));
controller.close();
},
});
}
describe("FileManager", () => {
it("reads and writes files through the capsule file client", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const download = vi
.spyOn(capsule.client.files, "download")
.mockResolvedValue(streamFrom("hello"));
const upload = vi
.spyOn(capsule.client.files, "upload")
.mockResolvedValue(undefined);
await expect(capsule.files.read("/tmp/a.txt")).resolves.toEqual(
Buffer.from("hello"),
);
await expect(
capsule.files.write("/tmp/a.txt", "hello"),
).resolves.toBeUndefined();
expect(download).toHaveBeenCalledWith("cap_1", { path: "/tmp/a.txt" });
expect(upload).toHaveBeenCalledWith("cap_1", {
file: "hello",
path: "/tmp/a.txt",
});
});
it("maps directory operations", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const list = vi.spyOn(capsule.client.files, "list").mockResolvedValue({
entries: [{ name: "a.txt", path: "/tmp/a.txt", type: "file" }],
});
const mkdir = vi.spyOn(capsule.client.files, "mkdir").mockResolvedValue({
entry: { path: "/tmp/new", type: "directory" },
});
const remove = vi
.spyOn(capsule.client.files, "remove")
.mockResolvedValue(undefined);
await expect(
capsule.files.list("/tmp", { depth: 2 }),
).resolves.toMatchObject({
entries: [{ name: "a.txt" }],
});
await expect(capsule.files.mkdir("/tmp/new")).resolves.toMatchObject({
entry: { type: "directory" },
});
await expect(capsule.files.remove("/tmp/a.txt")).resolves.toBeUndefined();
expect(list).toHaveBeenCalledWith("cap_1", { depth: 2, path: "/tmp" });
expect(mkdir).toHaveBeenCalledWith("cap_1", { path: "/tmp/new" });
expect(remove).toHaveBeenCalledWith("cap_1", { path: "/tmp/a.txt" });
});
it("streams downloads as Buffer chunks and uploads streaming content", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
vi.spyOn(capsule.client.files, "streamDownload").mockResolvedValue(
streamFrom("hello"),
);
const streamUpload = vi
.spyOn(capsule.client.files, "streamUpload")
.mockResolvedValue(undefined);
const chunks: Buffer[] = [];
for await (const chunk of capsule.files.downloadStream("/tmp/a.txt")) {
chunks.push(chunk);
}
await capsule.files.uploadStream("/tmp/a.txt", Buffer.from("hello"));
expect(Buffer.concat(chunks).toString()).toBe("hello");
expect(streamUpload).toHaveBeenCalledWith("cap_1", {
file: expect.any(Blob),
path: "/tmp/a.txt",
});
});
});

View File

@ -3,6 +3,7 @@ import type { AddressInfo } from "node:net";
import { afterEach, describe, expect, it, vi } from "vitest";
import { WebSocketServer } from "ws";
import { AsyncQueue } from "../src/_shared/async-queue.js";
import { HttpClient } from "../src/_shared/http.js";
import { WsConnection } from "../src/_shared/websocket.js";
import { resolveConfig } from "../src/config.js";
@ -31,7 +32,7 @@ describe("resolveConfig", () => {
vi.stubEnv("WRENN_TOKEN", undefined);
vi.stubEnv("WRENN_HOST_TOKEN", undefined);
expect(resolveConfig()).toEqual({ baseUrl: "https://api.wrenn.dev" });
expect(resolveConfig()).toEqual({ baseUrl: "https://app.wrenn.dev/api" });
});
it("prefers explicit options over environment variables", () => {
@ -233,6 +234,57 @@ describe("HttpClient", () => {
await assertion;
});
it("downloads binary response bodies as ReadableStream", async () => {
const body = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([1, 2, 3]));
controller.close();
},
});
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response(body, { status: 200 })),
);
const client = new HttpClient({ baseUrl: "https://api.example.com" });
const stream = await client.download("/v1/file", { path: "/a.txt" });
expect(stream).toBeInstanceOf(ReadableStream);
const reader = stream.getReader();
const { value } = await reader.read();
expect(value).toEqual(new Uint8Array([1, 2, 3]));
});
it("handles content-length zero as empty response", async () => {
vi.stubGlobal(
"fetch",
vi.fn(
async () =>
new Response(null, {
status: 200,
headers: { "content-length": "0" },
}),
),
);
const client = new HttpClient({ baseUrl: "https://api.example.com" });
const result = await client.get("/v1/empty");
expect(result).toBeUndefined();
});
it("handles empty text body as undefined", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("", { status: 200 })),
);
const client = new HttpClient({ baseUrl: "https://api.example.com" });
const result = await client.get("/v1/blank");
expect(result).toBeUndefined();
});
});
describe("WsConnection", () => {
@ -263,7 +315,8 @@ describe("WsConnection", () => {
await expect(receivedByServer).resolves.toEqual({ type: "start" });
expect(messages).toEqual([{ type: "ready" }]);
connection.close();
await connection[Symbol.asyncDispose]();
expect(connection.isClosed).toBe(true);
server.close();
});
@ -285,3 +338,37 @@ describe("WsConnection", () => {
vi.useRealTimers();
});
});
describe("AsyncQueue", () => {
it("rejects waiting consumers when failed", async () => {
const queue = new AsyncQueue<string>();
const pending = queue.next();
queue.fail(new Error("socket died"));
await expect(pending).rejects.toThrow("socket died");
await expect(queue.next()).rejects.toThrow("socket died");
});
it("ends the queue from return and resolves as done", async () => {
const queue = new AsyncQueue<string>();
const result = await queue.return();
expect(result).toEqual({ done: true, value: undefined });
await expect(queue.next()).resolves.toEqual({
done: true,
value: undefined,
});
});
it("propagates error through throw and rejects future reads", async () => {
const queue = new AsyncQueue<string>();
const error = new Error("consumer threw");
const result = queue.throw(error);
await expect(result).rejects.toThrow("consumer threw");
await expect(queue.next()).rejects.toThrow("consumer threw");
});
});

106
tests/git.test.ts Normal file
View File

@ -0,0 +1,106 @@
import { describe, expect, it, vi } from "vitest";
import { Capsule } from "../src/capsule.js";
import { Git } from "../src/git/index.js";
describe("Git", () => {
it("runs clone with optional path and branch", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const exec = vi.spyOn(capsule.commands, "exec").mockResolvedValue({
exit_code: 0,
stdout: "",
});
await expect(
capsule.git.clone("https://example.com/repo.git", {
branch: "main",
path: "/work/repo",
}),
).resolves.toMatchObject({ exit_code: 0 });
expect(exec).toHaveBeenCalledWith("git", {
args: [
"clone",
"--branch",
"main",
"https://example.com/repo.git",
"/work/repo",
],
});
});
it("runs repository commands in cwd", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const git = new Git(capsule, { cwd: "/work/repo" });
const exec = vi.spyOn(capsule.commands, "exec").mockResolvedValue({
exit_code: 0,
stdout: "main\n",
});
await git.status();
await git.pull({ branch: "main", remote: "origin" });
await git.push({ branch: "main", remote: "origin" });
await git.log({ maxCount: 3 });
await git.branch();
await git.checkout("feature", { create: true });
await git.add(["src/a.ts", "src/b.ts"]);
await git.commit("test commit");
expect(exec.mock.calls).toEqual([
["git", { args: ["status", "--short"], cwd: "/work/repo" }],
["git", { args: ["pull", "origin", "main"], cwd: "/work/repo" }],
["git", { args: ["push", "origin", "main"], cwd: "/work/repo" }],
["git", { args: ["log", "--oneline", "-n", "3"], cwd: "/work/repo" }],
["git", { args: ["branch", "--show-current"], cwd: "/work/repo" }],
["git", { args: ["checkout", "-b", "feature"], cwd: "/work/repo" }],
["git", { args: ["add", "src/a.ts", "src/b.ts"], cwd: "/work/repo" }],
["git", { args: ["commit", "-m", "test commit"], cwd: "/work/repo" }],
]);
});
it("rejects empty git add file lists", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
await expect(capsule.git.add([])).rejects.toThrow(
"At least one file is required",
);
});
it("checks out an existing branch without creating it", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const exec = vi.spyOn(capsule.commands, "exec").mockResolvedValue({
exit_code: 0,
stdout: "",
});
await capsule.git.checkout("main");
expect(exec).toHaveBeenCalledWith("git", {
args: ["checkout", "main"],
});
});
it("normalizes a single string file argument in add", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const exec = vi.spyOn(capsule.commands, "exec").mockResolvedValue({
exit_code: 0,
stdout: "",
});
await capsule.git.add("src/a.ts");
expect(exec).toHaveBeenCalledWith("git", {
args: ["add", "src/a.ts"],
});
});
});

43
tests/index.test.ts Normal file
View File

@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import type { components, operations, paths } from "../src/index.js";
import * as sdk from "../src/index.js";
type CapsuleSchema = components["schemas"]["Capsule"];
type GetCapsuleOperation = operations["getCapsule"];
type CapsulePath = paths["/v1/capsules/{id}"];
function acceptsGeneratedTypes(
_capsule: CapsuleSchema,
_operation: GetCapsuleOperation,
_path: CapsulePath,
): void {}
describe("public entry point", () => {
it("exports the supported runtime API from the package root", () => {
expect(sdk.Capsule).toBeTypeOf("function");
expect(sdk.Sandbox).toBe(sdk.Capsule);
expect(sdk.WrennClient).toBeTypeOf("function");
expect(sdk.CodeInterpreter).toBeTypeOf("function");
expect(sdk.Notebook).toBeTypeOf("function");
expect(sdk.CommandManager).toBeTypeOf("function");
expect(sdk.FileManager).toBeTypeOf("function");
expect(sdk.Git).toBeTypeOf("function");
expect(sdk.PtyManager).toBeTypeOf("function");
expect(sdk.PtySession).toBeTypeOf("function");
expect(sdk.WrennError).toBeTypeOf("function");
expect(sdk.NotFoundError).toBeTypeOf("function");
});
it("keeps internal shared helpers out of the public runtime API", () => {
expect("HttpClient" in sdk).toBe(false);
expect("WsConnection" in sdk).toBe(false);
});
it("exposes generated OpenAPI types from the package root", () => {
acceptsGeneratedTypes(
{} as CapsuleSchema,
{} as GetCapsuleOperation,
{} as CapsulePath,
);
});
});

View File

@ -0,0 +1,181 @@
import { describe, expect, it } from "vitest";
import { Capsule, type CapsuleCreateOptions } from "../../src/capsule.js";
import type { CommandStreamEvent } from "../../src/commands.js";
import { DEFAULT_BASE_URL } from "../../src/config.js";
import type { PtyEvent } from "../../src/pty.js";
const baseUrl = process.env.WRENN_BASE_URL ?? DEFAULT_BASE_URL;
const apiKey = process.env.WRENN_API_KEY;
const template = process.env.WRENN_TEST_TEMPLATE ?? "minimal";
const waitTimeoutMs = Number(process.env.WRENN_TEST_WAIT_TIMEOUT_MS ?? 120_000);
const testTimeoutMs = waitTimeoutMs + 45_000;
const describeWithApiKey = apiKey ? describe : describe.skip;
const clientOpts: CapsuleCreateOptions = { baseUrl };
if (apiKey) clientOpts.apiKey = apiKey;
async function withLiveCapsule(
fn: (capsule: Capsule) => Promise<void>,
): Promise<void> {
let capsule: Capsule | undefined;
try {
capsule = await Capsule.create(template, {
...clientOpts,
timeout_sec: 120,
});
await capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
await fn(capsule);
} finally {
if (capsule) {
await capsule.destroy().catch(() => undefined);
}
}
}
async function collectCommandEvents(
events: AsyncGenerator<CommandStreamEvent>,
): Promise<CommandStreamEvent[]> {
const collected: CommandStreamEvent[] = [];
for await (const event of events) {
collected.push(event);
if (event.type === "exit" || event.type === "error") break;
}
return collected;
}
async function nextPtyEvent(
events: AsyncIterableIterator<PtyEvent>,
): Promise<PtyEvent> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(
() => reject(new Error("Timed out waiting for PTY event")),
15_000,
);
});
const result = await Promise.race([events.next(), timeout]);
if (result.done) throw new Error("PTY closed before next event");
return result.value;
}
describeWithApiKey("Phase 4 live integration", () => {
it(
"executes foreground, streaming, and background commands",
async () => {
await withLiveCapsule(async (capsule) => {
const result = await capsule.commands.exec("printf", {
args: ["phase4-command"],
timeoutSec: 10,
});
expect(result.exit_code).toBe(0);
expect(result.stdout).toContain("phase4-command");
const streamEvents = await collectCommandEvents(
capsule.commands.stream("printf", { args: ["phase4-stream"] }),
);
expect(streamEvents.some((event) => event.type === "stdout")).toBe(
true,
);
expect(streamEvents.at(-1)).toMatchObject({ type: "exit" });
const tag = `phase4-${Date.now()}`;
const process = await capsule.commands.start("sleep", {
args: ["60"],
tag,
});
expect(process.tag).toBe(tag);
const processes = await capsule.commands.list();
expect(processes.processes?.some((entry) => entry.tag === tag)).toBe(
true,
);
await expect(
capsule.commands.kill(tag, "SIGTERM"),
).resolves.toBeUndefined();
});
},
testTimeoutMs,
);
it(
"performs file read/write/list/remove and streaming transfers",
async () => {
await withLiveCapsule(async (capsule) => {
const dir = `/tmp/wrenn-phase4-${Date.now()}`;
const file = `${dir}/hello.txt`;
const streamFile = `${dir}/stream.txt`;
await capsule.files.mkdir(dir);
await capsule.files.write(file, "phase4-file");
const content = await capsule.files.read(file);
expect(content.toString()).toBe("phase4-file");
const listing = await capsule.files.list(dir, { depth: 1 });
expect(
listing.entries?.some((entry) => entry.name === "hello.txt"),
).toBe(true);
await capsule.files.uploadStream(
streamFile,
Buffer.from("phase4-stream-file"),
);
const chunks: Buffer[] = [];
for await (const chunk of capsule.files.downloadStream(streamFile)) {
chunks.push(chunk);
}
expect(Buffer.concat(chunks).toString()).toBe("phase4-stream-file");
await expect(capsule.files.remove(file)).resolves.toBeUndefined();
await expect(capsule.files.remove(streamFile)).resolves.toBeUndefined();
await expect(capsule.files.remove(dir)).resolves.toBeUndefined();
});
},
testTimeoutMs,
);
it(
"starts and controls an interactive PTY session",
async () => {
await withLiveCapsule(async (capsule) => {
const session = await capsule.pty.start({
cmd: "/bin/sh",
cols: 80,
rows: 24,
});
const started = await nextPtyEvent(session.events);
expect(started.type).toBe("started");
session.resize(100, 30);
session.input("printf phase4-pty\\n\nexit\n");
const events: PtyEvent[] = [];
for (let i = 0; i < 10; i += 1) {
const event = await nextPtyEvent(session.events);
events.push(event);
if (event.type === "exit" || event.type === "error") break;
}
const output = events
.filter(
(event) =>
event.type === "output" && typeof event.data === "string",
)
.map((event) =>
Buffer.from(event.data as string, "base64").toString(),
)
.join("");
expect(output).toContain("phase4-pty");
expect(events.at(-1)).toMatchObject({ type: "exit" });
session.close();
});
},
testTimeoutMs,
);
});

View File

@ -0,0 +1,125 @@
import { describe, expect, it } from "vitest";
import { Capsule, type CapsuleCreateOptions } from "../../src/capsule.js";
import { DEFAULT_BASE_URL } from "../../src/config.js";
const baseUrl = process.env.WRENN_BASE_URL ?? DEFAULT_BASE_URL;
const apiKey = process.env.WRENN_API_KEY;
const template = process.env.WRENN_TEST_TEMPLATE ?? "minimal";
const waitTimeoutMs = Number(process.env.WRENN_TEST_WAIT_TIMEOUT_MS ?? 120_000);
const describeWithApiKey = apiKey ? describe : describe.skip;
const clientOpts: CapsuleCreateOptions = { baseUrl };
if (apiKey) clientOpts.apiKey = apiKey;
describeWithApiKey("Capsule live integration", () => {
it(
"creates, waits for, inspects, pings, and destroys a live capsule",
async () => {
let capsule: Capsule | undefined;
try {
capsule = await Capsule.create(template, {
...clientOpts,
timeout_sec: 60,
});
expect(capsule.id).toBeTypeOf("string");
expect(capsule.id.length).toBeGreaterThan(0);
const ready = await capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
expect(ready).toMatchObject({ id: capsule.id, status: "running" });
const connected = Capsule.connect(capsule.id, clientOpts);
const info = await connected.getInfo();
expect(info.id).toBe(capsule.id);
await expect(connected.ping()).resolves.toBeUndefined();
const metrics = await connected.getMetrics({ range: "10m" });
expect(metrics).toBeTypeOf("object");
await connected.close();
await connected[Symbol.asyncDispose]();
await Capsule.destroy(capsule.id, clientOpts);
capsule = undefined;
} finally {
if (capsule) {
await Capsule.destroy(capsule.id, clientOpts).catch(() => undefined);
}
}
},
waitTimeoutMs + 30_000,
);
it(
"pauses and resumes a live capsule through high-level methods",
async () => {
let capsule: Capsule | undefined;
try {
capsule = await Capsule.create(template, {
...clientOpts,
timeout_sec: 60,
});
await capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
const paused = await capsule.pause();
expect(paused).toMatchObject({ id: capsule.id });
expect(paused.status).toBe("paused");
const resumed = await capsule.resume({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
wait: true,
});
expect(resumed).toMatchObject({ id: capsule.id, status: "running" });
} finally {
if (capsule) {
await Capsule.destroy(capsule.id, clientOpts).catch(() => undefined);
}
}
},
waitTimeoutMs * 2 + 30_000,
);
it(
"destroys an owned live capsule when async disposed",
async () => {
let capsuleId: string | undefined;
try {
const capsule = await Capsule.create(template, {
...clientOpts,
timeout_sec: 60,
});
capsuleId = capsule.id;
await capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
await capsule[Symbol.asyncDispose]();
await expect(
Capsule.connect(capsuleId, clientOpts).getInfo(),
).resolves.toMatchObject({ id: capsuleId, status: "stopped" });
capsuleId = undefined;
} finally {
if (capsuleId) {
await Capsule.destroy(capsuleId, clientOpts).catch(() => undefined);
}
}
},
waitTimeoutMs + 30_000,
);
});

View File

@ -1,7 +1,7 @@
import { beforeAll, describe, expect, it } from "vitest";
import { WrennClient } from "../../src/client.js";
import { DEFAULT_BASE_URL } from "../../src/config.js";
import { type ClientConfig, DEFAULT_BASE_URL } from "../../src/config.js";
const baseUrl = process.env.WRENN_BASE_URL ?? DEFAULT_BASE_URL;
const apiKey = process.env.WRENN_API_KEY;
@ -11,8 +11,11 @@ const testPassword = process.env.WRENN_TEST_PASS;
const describeWithApiKey = apiKey ? describe : describe.skip;
const describeWithLogin = testEmail && testPassword ? describe : describe.skip;
const apiKeyClientOpts: ClientConfig = { baseUrl };
if (apiKey) apiKeyClientOpts.apiKey = apiKey;
describeWithApiKey("WrennClient live API key integration", () => {
const client = new WrennClient({ apiKey, baseUrl });
const client = new WrennClient(apiKeyClientOpts);
it("lists capsules from the real Wrenn API", async () => {
const capsules = await client.capsules.list();
@ -50,7 +53,9 @@ describeWithLogin("WrennClient live login integration", () => {
});
expect(auth.token).toBeTypeOf("string");
client = new WrennClient({ baseUrl, token: auth.token });
const token = auth.token;
if (!token) throw new Error("Login response did not include a token");
client = new WrennClient({ baseUrl, token });
});
it("gets the current account profile from the real Wrenn API", async () => {

View File

@ -0,0 +1,232 @@
import { describe, expect, it } from "vitest";
import { Capsule, type CapsuleCreateOptions } from "../../src/capsule.js";
import { CodeInterpreter } from "../../src/code-interpreter/index.js";
import { DEFAULT_BASE_URL } from "../../src/config.js";
import { Git } from "../../src/git/index.js";
const baseUrl = process.env.WRENN_BASE_URL ?? DEFAULT_BASE_URL;
const apiKey = process.env.WRENN_API_KEY;
const template = process.env.WRENN_TEST_TEMPLATE ?? "minimal";
const codeTemplate = process.env.WRENN_TEST_CODE_TEMPLATE ?? "jupyter";
const codeCapsuleId = process.env.WRENN_TEST_CODE_CAPSULE_ID;
const waitTimeoutMs = Number(process.env.WRENN_TEST_WAIT_TIMEOUT_MS ?? 120_000);
const testTimeoutMs = waitTimeoutMs + 60_000;
const describeWithApiKey = apiKey ? describe : describe.skip;
const clientOpts: CapsuleCreateOptions = { baseUrl };
if (apiKey) clientOpts.apiKey = apiKey;
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRetryableServerError(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
"statusCode" in error &&
typeof error.statusCode === "number" &&
error.statusCode >= 500
);
}
async function expectCommandOk(
promise: Promise<{ exit_code?: number; stderr?: string }>,
): Promise<void> {
const result = await promise;
expect(result.exit_code, result.stderr).toBe(0);
}
async function withLiveCapsule(
fn: (capsule: Capsule) => Promise<void>,
): Promise<void> {
let capsule: Capsule | undefined;
try {
capsule = await Capsule.create(template, {
...clientOpts,
timeout_sec: 120,
});
await capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
await fn(capsule);
} finally {
if (capsule) {
await capsule.destroy().catch(() => undefined);
}
}
}
async function createCodeInterpreterWithRetry(): Promise<CodeInterpreter> {
let lastError: unknown;
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
return await CodeInterpreter.create({
...clientOpts,
template: codeTemplate,
timeout_sec: 120,
});
} catch (error) {
lastError = error;
if (!isRetryableServerError(error) || attempt === 3) break;
await delay(2_000);
}
}
throw lastError;
}
async function getCodeInterpreter(): Promise<{
interpreter: CodeInterpreter;
shouldDestroy: boolean;
}> {
if (!codeCapsuleId) {
return {
interpreter: await createCodeInterpreterWithRetry(),
shouldDestroy: true,
};
}
const interpreter = CodeInterpreter.connect(codeCapsuleId, clientOpts);
const info = await interpreter.capsule.getInfo();
if (info.status === "paused") {
await interpreter.capsule.resume({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
wait: true,
});
} else {
await interpreter.capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
}
return { interpreter, shouldDestroy: false };
}
describeWithApiKey("Higher-level abstractions live integration", () => {
it(
"runs git workflows inside a live capsule",
async () => {
await withLiveCapsule(async (capsule) => {
const root = `/tmp/wrenn-git-${Date.now()}`;
const origin = `${root}/origin.git`;
const work = `${root}/work`;
await expectCommandOk(
capsule.commands.exec("mkdir", { args: ["-p", root] }),
);
await expectCommandOk(
capsule.commands.exec("git", { args: ["init", "--bare", origin] }),
);
await expectCommandOk(capsule.git.clone(origin, { path: work }));
const git = new Git(capsule, { cwd: work });
const pwd = await capsule.commands.exec("pwd", { cwd: work });
expect(pwd.stdout?.trim(), pwd.stderr).toBe(work);
await expectCommandOk(
capsule.commands.exec("git", {
args: ["config", "user.email", "integration@example.com"],
cwd: work,
}),
);
await expectCommandOk(
capsule.commands.exec("git", {
args: ["config", "user.name", "Integration Test"],
cwd: work,
}),
);
await expectCommandOk(
capsule.commands.exec("sh", {
args: ["-lc", "printf 'phase5-git\\n' > README.md"],
cwd: work,
}),
);
const dirty = await git.status();
expect(dirty.exit_code, dirty.stderr).toBe(0);
expect(dirty.stdout, dirty.stderr).toContain("README.md");
await expectCommandOk(git.add("README.md"));
const commit = await git.commit("initial commit");
expect(commit.exit_code, commit.stderr).toBe(0);
const log = await git.log({ maxCount: 1 });
expect(log.exit_code, log.stderr).toBe(0);
expect(log.stdout, log.stderr).toContain("initial commit");
await expectCommandOk(git.branch());
await expectCommandOk(git.checkout("feature", { create: true }));
const branch = await git.branch();
expect(branch.exit_code, branch.stderr).toBe(0);
expect(branch.stdout?.trim(), branch.stderr).toBe("feature");
await expectCommandOk(
git.push({ branch: "feature", remote: "origin" }),
);
await expectCommandOk(
git.pull({ branch: "feature", remote: "origin" }),
);
});
},
testTimeoutMs,
);
it(
"executes a code interpreter cell in a live capsule",
async () => {
let interpreter: CodeInterpreter | undefined;
let shouldDestroy = false;
try {
const codeInterpreter = await getCodeInterpreter();
interpreter = codeInterpreter.interpreter;
shouldDestroy = codeInterpreter.shouldDestroy;
const result = await interpreter.notebook.execCell("print(6 * 7)", {
timeoutSec: 15,
});
expect(result.exitCode).toBe(0);
expect(result.stdout.trim()).toBe("42");
expect(result.stderr).toBe("");
} finally {
if (interpreter && shouldDestroy) {
await interpreter.capsule.destroy().catch(() => undefined);
}
}
},
testTimeoutMs,
);
it(
"destroys an owned code interpreter capsule when async disposed",
async () => {
let capsuleId: string | undefined;
try {
const interpreter = await createCodeInterpreterWithRetry();
capsuleId = interpreter.capsule.id;
await interpreter.capsule.waitForReady({
intervalMs: 2_000,
timeoutMs: waitTimeoutMs,
});
await interpreter[Symbol.asyncDispose]();
await expect(
Capsule.connect(capsuleId, clientOpts).getInfo(),
).resolves.toMatchObject({ id: capsuleId, status: "stopped" });
capsuleId = undefined;
} finally {
if (capsuleId) {
await Capsule.destroy(capsuleId, clientOpts).catch(() => undefined);
}
}
},
testTimeoutMs,
);
});

131
tests/pty.test.ts Normal file
View File

@ -0,0 +1,131 @@
import { describe, expect, it, vi } from "vitest";
import { Capsule } from "../src/capsule.js";
describe("PtyManager", () => {
it("starts a PTY, sends controls, and yields events", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const sent: unknown[] = [];
let onMessage: ((message: unknown) => void) | undefined;
const close = vi.fn();
vi.spyOn(capsule.client.capsules, "ptySession").mockImplementation(
async (_id, opts) => {
onMessage = opts.onMessage;
return {
close,
get isClosed() {
return false;
},
send: (message: unknown) => sent.push(message),
} as never;
},
);
const session = await capsule.pty.start({
cmd: "/bin/sh",
cols: 100,
rows: 30,
});
expect(sent).toEqual([
{ cmd: "/bin/sh", cols: 100, rows: 30, type: "start" },
]);
session.input("ls\n");
session.resize(120, 40);
session.kill();
expect(sent.slice(1)).toEqual([
{ data: Buffer.from("ls\n").toString("base64"), type: "input" },
{ cols: 120, rows: 40, type: "resize" },
{ type: "kill" },
]);
const event = session.events.next();
onMessage?.({ data: Buffer.from("ok").toString("base64"), type: "output" });
await expect(event).resolves.toEqual({
done: false,
value: {
data: Buffer.from("ok").toString("base64"),
type: "output",
},
});
await session[Symbol.asyncDispose]();
expect(close).toHaveBeenCalledOnce();
});
it("connects to an existing PTY tag", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const sent: unknown[] = [];
vi.spyOn(capsule.client.capsules, "ptySession").mockResolvedValue({
close: vi.fn(),
get isClosed() {
return false;
},
send: (message: unknown) => sent.push(message),
} as never);
await capsule.pty.connect("pty-tag");
expect(sent).toEqual([{ tag: "pty-tag", type: "connect" }]);
});
it("closes the connection when close() is called directly", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const close = vi.fn();
vi.spyOn(capsule.client.capsules, "ptySession").mockResolvedValue({
close,
get isClosed() {
return false;
},
send: vi.fn(),
} as never);
const session = await capsule.pty.start();
session.close();
expect(close).toHaveBeenCalledOnce();
});
it("sends all PtyStartOptions fields in the start message", async () => {
const capsule = new Capsule("cap_1", {
baseUrl: "https://api.example.com",
});
const sent: unknown[] = [];
vi.spyOn(capsule.client.capsules, "ptySession").mockResolvedValue({
close: vi.fn(),
get isClosed() {
return false;
},
send: (message: unknown) => sent.push(message),
} as never);
await capsule.pty.start({
cmd: "/bin/bash",
args: ["--login"],
cols: 120,
rows: 40,
envs: { TERM: "xterm-256color" },
cwd: "/home/user",
user: "user",
});
expect(sent).toEqual([
{
type: "start",
cmd: "/bin/bash",
args: ["--login"],
cols: 120,
rows: 40,
envs: { TERM: "xterm-256color" },
cwd: "/home/user",
user: "user",
},
]);
});
});