From 6ee733af175658b0f4cf80e530b797197ae1edba Mon Sep 17 00:00:00 2001 From: Wayland Yang Date: Tue, 19 May 2026 23:28:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk/typescript):=20@deeplethe/forkd=20v0.3?= =?UTF-8?q?.1=20=E2=80=94=20TypeScript=20surface=20parity=20with=20Python?= =?UTF-8?q?=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #115 (item 3 of meta #112). Distribution track. What ships: - sdk/typescript/ — new workspace member; ESM, Node 18+, no runtime deps (uses global fetch). - Controller class with full surface parity to Python SDK: listSnapshots, deleteSnapshot, spawnSandboxes, listSandboxes, getSandbox, killSandbox, branchSandbox, execCommand, evalCode, pingSandbox. Wire-level still snake_case; TS-side camelCase. - branchSandbox + spawnSandboxes leapfrog the Python SDK by exposing v0.3's diff / measure_diff / prewarm options. Follow-up PR will back-port to the Python side. - Sandbox higher-level wrapper (mirrors E2B/Daytona patterns): Sandbox.create, .exec, .eval, .ping, .branch, .kill, .with. - ControllerError carries status + body + url for HTTP failure inspection. - 9 vitest unit tests, all green: snake_case serialization, diff option pass-through, ControllerError on 4xx, auth header, FORKD_URL env default, DELETE empty body, URL encoding, abort-on-timeout. - README with API table, quick-start, v0.3 fast-BRANCH section, error handling, links to forkd-mcp + forkd-action. CI / publish: - .github/workflows/ci-typescript.yml — Node 18/20/22 matrix, build + test on every PR touching sdk/typescript/. - .github/workflows/publish-npm.yml — fires on ts-v* tag push, publishes @deeplethe/forkd to npm with provenance via OIDC. Needs NPM_TOKEN secret in the `npm` environment. Scoped npm name (@deeplethe/forkd) since `forkd` on npm is taken. Version 0.3.1 matches the rest of the v0.3 release line. Not in this PR (follow-ups): - Back-port diff / prewarm / measure_diff options to Python SDK (Python SDK is currently behind the daemon API). - Add API-key auth helper for managed-cloud scenarios. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci-typescript.yml | 35 + .github/workflows/publish-npm.yml | 46 + sdk/typescript/.gitignore | 5 + sdk/typescript/README.md | 142 +++ sdk/typescript/package-lock.json | 1465 +++++++++++++++++++++++ sdk/typescript/package.json | 57 + sdk/typescript/src/controller.ts | 257 ++++ sdk/typescript/src/index.ts | 32 + sdk/typescript/src/sandbox.ts | 134 +++ sdk/typescript/src/types.ts | 86 ++ sdk/typescript/tests/controller.test.ts | 149 +++ sdk/typescript/tsconfig.json | 25 + 12 files changed, 2433 insertions(+) create mode 100644 .github/workflows/ci-typescript.yml create mode 100644 .github/workflows/publish-npm.yml create mode 100644 sdk/typescript/.gitignore create mode 100644 sdk/typescript/README.md create mode 100644 sdk/typescript/package-lock.json create mode 100644 sdk/typescript/package.json create mode 100644 sdk/typescript/src/controller.ts create mode 100644 sdk/typescript/src/index.ts create mode 100644 sdk/typescript/src/sandbox.ts create mode 100644 sdk/typescript/src/types.ts create mode 100644 sdk/typescript/tests/controller.test.ts create mode 100644 sdk/typescript/tsconfig.json diff --git a/.github/workflows/ci-typescript.yml b/.github/workflows/ci-typescript.yml new file mode 100644 index 0000000..2c3cf25 --- /dev/null +++ b/.github/workflows/ci-typescript.yml @@ -0,0 +1,35 @@ +name: ci-typescript + +on: + push: + branches: [main] + paths: + - 'sdk/typescript/**' + - '.github/workflows/ci-typescript.yml' + pull_request: + paths: + - 'sdk/typescript/**' + - '.github/workflows/ci-typescript.yml' + +jobs: + test: + name: build + test on Node ${{ matrix.node }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ['18', '20', '22'] + defaults: + run: + working-directory: sdk/typescript + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + cache-dependency-path: sdk/typescript/package-lock.json + - run: npm ci || npm install + - run: npm run lint + - run: npm run build + - run: npm test diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..1742d45 --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,46 @@ +name: publish-npm + +on: + push: + tags: + - 'ts-v*' + workflow_dispatch: + +jobs: + publish: + name: build + upload @deeplethe/forkd to npm + runs-on: ubuntu-latest + environment: npm + permissions: + id-token: write # for npm provenance + contents: read + defaults: + run: + working-directory: sdk/typescript + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + cache-dependency-path: sdk/typescript/package-lock.json + + - name: Verify SDK version matches the release tag + if: github.event_name == 'push' + run: | + ref="${GITHUB_REF#refs/tags/ts-v}" + pkg_ver=$(jq -r '.version' package.json) + if [[ "$pkg_ver" != "$ref" ]]; then + echo "::error::@deeplethe/forkd version ${pkg_ver} != release tag ${ref}" + exit 1 + fi + + - run: npm ci || npm install + - run: npm run build + - run: npm test + + - name: Publish to npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public --provenance diff --git a/sdk/typescript/.gitignore b/sdk/typescript/.gitignore new file mode 100644 index 0000000..19a4b5e --- /dev/null +++ b/sdk/typescript/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.tsbuildinfo +.DS_Store +*.log diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md new file mode 100644 index 0000000..074878c --- /dev/null +++ b/sdk/typescript/README.md @@ -0,0 +1,142 @@ +# @deeplethe/forkd + +TypeScript client for [forkd](https://github.com/deeplethe/forkd) — the open-source fork-on-write microVM primitive for AI agents. + +```bash +npm install @deeplethe/forkd +# or pnpm add @deeplethe/forkd +``` + +Requires Node 18+ (uses the global `fetch`). + +## Quick start + +```ts +import { Controller, Sandbox } from "@deeplethe/forkd"; + +// Either: spawn + use + cleanup +const result = await Sandbox.with( + { snapshotTag: "python-3-12-slim" }, + async (sb) => sb.exec(["python3", "-c", "print(2+2)"]), +); +console.log(result.stdout); // "4\n" + +// Or: long-lived sandbox with BRANCH (forkd's killer move) +const ctrl = new Controller({ + baseUrl: "http://127.0.0.1:8889", + token: process.env.FORKD_TOKEN, +}); +const [source] = await ctrl.spawnSandboxes({ + snapshotTag: "langgraph-react", +}); +await ctrl.execCommand(source.id, ["python3", "/opt/agent.py"]); + +// BRANCH: pause source briefly, snapshot, resume. Children inherit +// source's exact state and diverge under copy-on-write. +// v0.3+: opt into diff mode for ~200 ms source-pause regardless of +// memory size (143× ceiling on 4 GiB SSD). +const checkpoint = await ctrl.branchSandbox(source.id, { + tag: "after-warmup", + diff: true, +}); + +// Fan out N children from the checkpoint. +const children = await ctrl.spawnSandboxes({ + snapshotTag: checkpoint.tag, + n: 4, +}); +``` + +## API surface + +Surface parity with the Python SDK (`pip install forkd`): + +| Python | TypeScript | +|---|---| +| `Controller.list_snapshots()` | `ctrl.listSnapshots()` | +| `Controller.delete_snapshot(tag)` | `ctrl.deleteSnapshot(tag)` | +| `Controller.spawn_sandboxes(...)` | `ctrl.spawnSandboxes({ snapshotTag, n, prewarm, ... })` | +| `Controller.list_sandboxes()` | `ctrl.listSandboxes()` | +| `Controller.get_sandbox(id)` | `ctrl.getSandbox(id)` | +| `Controller.kill_sandbox(id)` | `ctrl.killSandbox(id)` | +| `Controller.branch_sandbox(id, tag)` | `ctrl.branchSandbox(id, { tag, diff, measure_diff })` | +| `Controller.exec_command(id, args)` | `ctrl.execCommand(id, args, { timeoutSecs })` | +| `Controller.eval_code(id, code)` | `ctrl.evalCode(id, code)` | +| `Controller.ping_sandbox(id)` | `ctrl.pingSandbox(id)` | + +Snake-case API field names are preserved over the wire (the daemon +expects them), but TypeScript-side argument names are camelCase. + +### `Sandbox` (higher-level wrapper) + +```ts +const sb = await Sandbox.create({ snapshotTag: "python-3-12-slim" }); +const result = await sb.exec(["echo", "hi"]); +const value = await sb.eval("2+2"); // 4 +const branch = await sb.branch({ diff: true }); +await sb.kill(); +``` + +`Sandbox.with(options, fn)` is the recommended pattern for short-lived +work — automatic cleanup even on exception. + +## Configuration + +```ts +new Controller({ + baseUrl: "http://127.0.0.1:8889", // or env FORKD_URL + token: "abc123", // or env FORKD_TOKEN + timeoutMs: 60_000, // default + fetch: customFetch, // optional (testing / older Node) +}); +``` + +## v0.3 fast-BRANCH (diff snapshots) + +forkd v0.3 added diff-snapshot BRANCH. Opt in per-call: + +```ts +const branch = await ctrl.branchSandbox(sandboxId, { diff: true }); +``` + +Measured numbers (full table in +[RESULTS-v0.3.md](https://github.com/deeplethe/forkd/blob/main/bench/pause-window/RESULTS-v0.3.md)): + +- Idle 4 GiB SSD source: 29 s → 205 ms = **143×** +- Typical agent workload (30-300 MiB dirty): **6-15×** +- 5 consecutive BRANCHes (v0.3.1+): **14× aggregate** + +Requires `forkd-controller >= 0.3.0`. Older daemons return 400 on `diff: true`. + +## Error handling + +```ts +import { ControllerError } from "@deeplethe/forkd"; + +try { + await ctrl.getSandbox("sb-missing"); +} catch (e) { + if (e instanceof ControllerError && e.status === 404) { + // sandbox doesn't exist + } +} +``` + +## Testing + +```bash +pnpm install +pnpm test +``` + +Mock fetch by passing your own implementation to the `Controller` constructor (see [`tests/controller.test.ts`](./tests/controller.test.ts)). + +## See also + +- [forkd-mcp](https://pypi.org/project/forkd-mcp/) — MCP server for Claude Desktop / Cursor / Cline +- [forkd-action](https://github.com/deeplethe/forkd-action) — GitHub Action +- [Python SDK](https://pypi.org/project/forkd/) — `pip install forkd` + +## License + +Apache-2.0. diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json new file mode 100644 index 0000000..9b37850 --- /dev/null +++ b/sdk/typescript/package-lock.json @@ -0,0 +1,1465 @@ +{ + "name": "@deeplethe/forkd", + "version": "0.3.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@deeplethe/forkd", + "version": "0.3.1", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.5.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json new file mode 100644 index 0000000..845f64f --- /dev/null +++ b/sdk/typescript/package.json @@ -0,0 +1,57 @@ +{ + "name": "@deeplethe/forkd", + "version": "0.3.1", + "description": "TypeScript client for forkd — open-source fork-on-write microVM primitive for AI agents", + "license": "Apache-2.0", + "author": "Deeplethe ", + "homepage": "https://github.com/deeplethe/forkd", + "repository": { + "type": "git", + "url": "git+https://github.com/deeplethe/forkd.git", + "directory": "sdk/typescript" + }, + "bugs": "https://github.com/deeplethe/forkd/issues", + "keywords": [ + "sandbox", + "microvm", + "firecracker", + "fork", + "ai-agents", + "branch", + "snapshot" + ], + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "src", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run", + "test:watch": "vitest", + "lint": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.5.0", + "vitest": "^2.1.0" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/sdk/typescript/src/controller.ts b/sdk/typescript/src/controller.ts new file mode 100644 index 0000000..a27ff44 --- /dev/null +++ b/sdk/typescript/src/controller.ts @@ -0,0 +1,257 @@ +import type { + BranchOptions, + EvalResult, + ExecOptions, + ExecResult, + PingResult, + SandboxInfo, + SnapshotInfo, + SpawnOptions, +} from "./types.js"; + +/** + * Raised on non-2xx responses from the daemon. + * + * Inspect `status` and `body` to distinguish 404 (sandbox/snapshot + * missing) from 409 (tag collision) from 500 (internal). + */ +export class ControllerError extends Error { + readonly status: number; + readonly body: unknown; + readonly url: string; + + constructor(status: number, body: unknown, url: string) { + const snippet = + typeof body === "string" ? body : JSON.stringify(body); + super(`controller ${url}: HTTP ${status}: ${snippet}`); + this.name = "ControllerError"; + this.status = status; + this.body = body; + this.url = url; + } +} + +export interface ControllerOptions { + /** + * Daemon base URL. Defaults to env `FORKD_URL` then + * `http://127.0.0.1:8889`. + */ + baseUrl?: string; + /** + * Bearer token. Defaults to env `FORKD_TOKEN`. Required when the + * daemon was started with `--token-file`. + */ + token?: string; + /** + * Per-request timeout in milliseconds. Default 60_000. + * Branching a large source can take several seconds before v0.3's + * diff mode kicks in. + */ + timeoutMs?: number; + /** + * Custom fetch implementation. Defaults to global `fetch`. Provide + * `undici`'s fetch in Node test scenarios that want to record + * traffic, or polyfill for older Node. + */ + fetch?: typeof fetch; +} + +/** + * Client for the forkd-controller daemon's REST API. + * + * @example + * ```ts + * import { Controller } from '@deeplethe/forkd'; + * const ctrl = new Controller({ + * baseUrl: 'http://127.0.0.1:8889', + * token: process.env.FORKD_TOKEN, + * }); + * const snapshots = await ctrl.listSnapshots(); + * const [sb] = await ctrl.spawnSandboxes({ snapshotTag: 'python-3-12-slim' }); + * const result = await ctrl.execCommand(sb.id, ['python3', '-c', 'print(2+2)']); + * const branch = await ctrl.branchSandbox(sb.id, { diff: true }); + * await ctrl.killSandbox(sb.id); + * ``` + */ +export class Controller { + readonly baseUrl: string; + readonly token: string | undefined; + readonly timeoutMs: number; + private readonly fetchImpl: typeof fetch; + + constructor(opts: ControllerOptions = {}) { + const envUrl = + typeof process !== "undefined" ? process.env.FORKD_URL : undefined; + const envToken = + typeof process !== "undefined" ? process.env.FORKD_TOKEN : undefined; + this.baseUrl = (opts.baseUrl ?? envUrl ?? "http://127.0.0.1:8889").replace( + /\/+$/, + "", + ); + this.token = opts.token ?? envToken ?? undefined; + this.timeoutMs = opts.timeoutMs ?? 60_000; + this.fetchImpl = opts.fetch ?? globalThis.fetch; + if (typeof this.fetchImpl !== "function") { + throw new Error( + "Controller: no fetch implementation. Node 18+ ships fetch globally; otherwise pass `fetch` in options.", + ); + } + } + + // --- snapshots ---------------------------------------------------- + + async listSnapshots(): Promise { + return this.request("GET", "/v1/snapshots"); + } + + async deleteSnapshot(tag: string): Promise { + await this.request("DELETE", `/v1/snapshots/${encodeURIComponent(tag)}`); + } + + // --- sandboxes ---------------------------------------------------- + + /** + * Fork N children from a registered snapshot. + * + * @param options.snapshotTag snake_case in the wire format, + * camelCase here. + * @param options.prewarm v0.2.5+. Relocates the cold-cache + * penalty from the first BRANCH to + * sandbox-creation time. + */ + async spawnSandboxes(options: { + snapshotTag: string; + n?: number; + perChildNetns?: boolean; + memoryLimitMib?: number; + prewarm?: boolean; + }): Promise { + const body: SpawnOptions = { + snapshot_tag: options.snapshotTag, + n: options.n ?? 1, + per_child_netns: options.perChildNetns ?? false, + }; + if (options.memoryLimitMib !== undefined) { + body.memory_limit_mib = options.memoryLimitMib; + } + if (options.prewarm !== undefined) { + body.prewarm = options.prewarm; + } + return this.request("POST", "/v1/sandboxes", body); + } + + async listSandboxes(): Promise { + return this.request("GET", "/v1/sandboxes"); + } + + async getSandbox(sandboxId: string): Promise { + return this.request( + "GET", + `/v1/sandboxes/${encodeURIComponent(sandboxId)}`, + ); + } + + async killSandbox(sandboxId: string): Promise { + await this.request( + "DELETE", + `/v1/sandboxes/${encodeURIComponent(sandboxId)}`, + ); + } + + /** + * Branch a running sandbox into a new snapshot. + * + * Pauses the source briefly, snapshots, resumes. With + * `options.diff = true` (v0.3+) the user-visible source-pause window + * collapses to the diff write — sub-second across all memory sizes + * for idle sources, 6-15× speedup on typical agent workloads, 143× + * ceiling on 4 GiB SSD. + * + * Returns a {@link SnapshotInfo}; pass its `tag` back into + * {@link spawnSandboxes} to fan out grandchildren. + */ + async branchSandbox( + sandboxId: string, + options: BranchOptions = {}, + ): Promise { + const body: BranchOptions = {}; + if (options.tag !== undefined) body.tag = options.tag; + if (options.diff) body.diff = true; + if (options.measure_diff) body.measure_diff = true; + return this.request( + "POST", + `/v1/sandboxes/${encodeURIComponent(sandboxId)}/branch`, + body, + ); + } + + async execCommand( + sandboxId: string, + args: string[], + options: { timeoutSecs?: number } = {}, + ): Promise { + const body: ExecOptions = { + args, + timeout_secs: options.timeoutSecs ?? 30, + }; + return this.request( + "POST", + `/v1/sandboxes/${encodeURIComponent(sandboxId)}/exec`, + body, + ); + } + + async evalCode(sandboxId: string, code: string): Promise { + return this.request( + "POST", + `/v1/sandboxes/${encodeURIComponent(sandboxId)}/eval`, + { code }, + ); + } + + async pingSandbox(sandboxId: string): Promise { + return this.request( + "POST", + `/v1/sandboxes/${encodeURIComponent(sandboxId)}/ping`, + ); + } + + // --- internals ---------------------------------------------------- + + private async request( + method: string, + path: string, + body?: unknown, + ): Promise { + const url = `${this.baseUrl}${path}`; + const headers: Record = {}; + if (body !== undefined) headers["content-type"] = "application/json"; + if (this.token) headers["authorization"] = `Bearer ${this.token}`; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeoutMs); + try { + const resp = await this.fetchImpl(url, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + signal: controller.signal, + }); + if (!resp.ok) { + let parsed: unknown; + const text = await resp.text(); + try { + parsed = text ? JSON.parse(text) : {}; + } catch { + parsed = text; + } + throw new ControllerError(resp.status, parsed, url); + } + // DELETE returns 204 / empty; tolerate. + const text = await resp.text(); + if (!text) return null as T; + return JSON.parse(text) as T; + } finally { + clearTimeout(timer); + } + } +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts new file mode 100644 index 0000000..a949e82 --- /dev/null +++ b/sdk/typescript/src/index.ts @@ -0,0 +1,32 @@ +/** + * `@deeplethe/forkd` — TypeScript client for forkd. + * + * @example + * ```ts + * import { Controller, Sandbox } from '@deeplethe/forkd'; + * + * const ctrl = new Controller({ baseUrl: 'http://127.0.0.1:8889' }); + * const snapshots = await ctrl.listSnapshots(); + * + * // Spawn + run + cleanup in one go: + * const out = await Sandbox.with( + * { snapshotTag: 'python-3-12-slim' }, + * async (sb) => sb.exec(['python3', '-c', 'print(2+2)']), + * ); + * console.log(out.stdout); // "4\n" + * ``` + * + * See {@link Controller} and {@link Sandbox} for the API surface. + */ +export { Controller, ControllerError, type ControllerOptions } from "./controller.js"; +export { Sandbox } from "./sandbox.js"; +export type { + BranchOptions, + EvalResult, + ExecOptions, + ExecResult, + PingResult, + SandboxInfo, + SnapshotInfo, + SpawnOptions, +} from "./types.js"; diff --git a/sdk/typescript/src/sandbox.ts b/sdk/typescript/src/sandbox.ts new file mode 100644 index 0000000..826fa73 --- /dev/null +++ b/sdk/typescript/src/sandbox.ts @@ -0,0 +1,134 @@ +import { Controller, type ControllerOptions } from "./controller.js"; +import type { ExecResult, SandboxInfo } from "./types.js"; + +/** + * Higher-level wrapper around a single live sandbox. Mirrors the + * shape of E2B / Daytona SDKs so existing agent code can swap in + * forkd with minimal changes. + * + * Lifecycle: + * + * ```ts + * import { Sandbox } from '@deeplethe/forkd'; + * + * // Either create + own: + * const sb = await Sandbox.create({ snapshotTag: 'python-3-12-slim' }); + * try { + * const r = await sb.exec(['python3', '-c', 'print(2+2)']); + * console.log(r.stdout); // "4\n" + * } finally { + * await sb.kill(); + * } + * + * // Or "attach to an existing id" (e.g., after spawning N via + * // Controller.spawnSandboxes): + * const sb2 = new Sandbox(controller, info); + * ``` + */ +export class Sandbox { + readonly id: string; + readonly info: SandboxInfo; + private readonly controller: Controller; + private killed = false; + + constructor(controller: Controller, info: SandboxInfo) { + this.controller = controller; + this.info = info; + this.id = info.id; + } + + /** + * Spawn one sandbox + wrap. The most common entry point. + * + * @example + * ```ts + * const sb = await Sandbox.create({ + * snapshotTag: 'python-3-12-slim', + * prewarm: true, + * }); + * ``` + */ + static async create( + options: { + snapshotTag: string; + perChildNetns?: boolean; + memoryLimitMib?: number; + prewarm?: boolean; + } & ControllerOptions, + ): Promise { + const { snapshotTag, perChildNetns, memoryLimitMib, prewarm, ...ctrlOpts } = + options; + const ctrl = new Controller(ctrlOpts); + const [info] = await ctrl.spawnSandboxes({ + snapshotTag, + n: 1, + perChildNetns, + memoryLimitMib, + prewarm, + }); + if (!info) { + throw new Error("spawn returned no sandboxes (n=1 expected)"); + } + return new Sandbox(ctrl, info); + } + + /** Run a subprocess in the sandbox. */ + async exec( + args: string[], + options: { timeoutSecs?: number } = {}, + ): Promise { + return this.controller.execCommand(this.id, args, options); + } + + /** Evaluate Python against the warmed PID-1. */ + async eval(code: string): Promise { + const r = await this.controller.evalCode(this.id, code); + if (r.error) { + throw new Error(`eval error: ${r.error}`); + } + return r.result; + } + + /** Round-trip to the in-guest agent for health + version info. */ + async ping(): Promise> { + return this.controller.pingSandbox(this.id); + } + + /** + * Branch this sandbox into a new snapshot tag. Returns the snapshot + * info; you can pass its `tag` to `Controller.spawnSandboxes` to + * fan out grandchildren that inherit this sandbox's exact state. + * + * Opt into v0.3's diff path with `{ diff: true }` for sub-second + * source-pause. + */ + async branch( + options: { + tag?: string; + diff?: boolean; + measure_diff?: boolean; + } = {}, + ): Promise { + return this.controller.branchSandbox(this.id, options); + } + + /** Terminate the sandbox. Idempotent. */ + async kill(): Promise { + if (this.killed) return; + await this.controller.killSandbox(this.id); + this.killed = true; + } + + /** Convenience: spawn + run callback + kill. */ + static async with( + options: Parameters[0], + fn: (sb: Sandbox) => Promise, + ): Promise { + const sb = await Sandbox.create(options); + try { + return await fn(sb); + } finally { + await sb.kill(); + } + } +} diff --git a/sdk/typescript/src/types.ts b/sdk/typescript/src/types.ts new file mode 100644 index 0000000..84b7549 --- /dev/null +++ b/sdk/typescript/src/types.ts @@ -0,0 +1,86 @@ +/** + * Wire-level types for the forkd-controller REST API. + * + * Source of truth: `crates/forkd-controller/src/api.rs`. Optional + * fields are marked optional here for v0.x compatibility — older + * daemons may omit fields added in later releases. + */ + +export interface SnapshotInfo { + tag: string; + dir: string; + created_at_unix: number; + /** Set when produced by BRANCH; the source sandbox id. */ + branched_from?: string; + /** v0.2.5+: source-VM pause window in milliseconds during BRANCH. */ + pause_ms?: number; + /** v0.3+: time spent in the Diff snapshot call (subset of pause_ms). */ + diff_ms?: number; + /** v0.3+: on-disk bytes of the diff = dirty page count. */ + diff_physical_bytes?: number; + /** v0.3+: full guest-RAM size (what a Full snapshot would have written). */ + diff_logical_bytes?: number; +} + +export interface SandboxInfo { + id: string; + snapshot_tag: string; + netns: string | null; + guest_addr: string; + created_at_unix: number; + pid: number | null; + memory_limit_mib: number | null; + /** v0.3+: any BRANCH has been taken from this sandbox. */ + has_branched?: boolean; + /** v0.3.1+: chain head for the next diff BRANCH. */ + last_branch_memory_path?: string | null; +} + +export interface SpawnOptions { + snapshot_tag: string; + n?: number; + per_child_netns?: boolean; + memory_limit_mib?: number; + /** v0.2.5+: pre-warm sandbox after restore to relocate cold-cache. */ + prewarm?: boolean; +} + +export interface BranchOptions { + /** Optional tag for the new snapshot. Daemon generates one when unset. */ + tag?: string; + /** + * v0.3+: use Firecracker Diff snapshot mode. Source pause window + * collapses to the diff write only (~200 ms idle source, 6-15× + * speedup on typical agent workloads, 143× ceiling on 4 GiB SSD). + * Multi-BRANCH supported in v0.3.1+ via the previous-output chain. + */ + diff?: boolean; + /** + * v0.3+: measurement-only hook. Take a Diff snapshot inside the + * existing Full pause to report what diff would have cost, without + * changing semantics. Mutually exclusive with `diff` (400 if both). + */ + measure_diff?: boolean; +} + +export interface ExecOptions { + args: string[]; + timeout_secs?: number; +} + +export interface ExecResult { + stdout: string; + stderr: string; + exit_code: number; +} + +export interface EvalResult { + result: unknown; + error: string | null; + exit_code: number; +} + +export interface PingResult { + /** Whatever the in-guest agent returns. Shape stable per recipe. */ + [key: string]: unknown; +} diff --git a/sdk/typescript/tests/controller.test.ts b/sdk/typescript/tests/controller.test.ts new file mode 100644 index 0000000..55f3396 --- /dev/null +++ b/sdk/typescript/tests/controller.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it, vi } from "vitest"; +import { Controller, ControllerError } from "../src/controller.js"; + +/** + * Mock fetch helper. Returns a fetch impl that records calls and + * replies with the given status + body for the next request. + */ +function mockFetch(replies: Array<{ status: number; body: unknown }>) { + const calls: Array<{ url: string; init: RequestInit | undefined }> = []; + let i = 0; + const f: typeof fetch = (async (input, init) => { + calls.push({ url: String(input), init }); + const reply = replies[i++]; + if (!reply) { + throw new Error(`unexpected fetch call: ${input}`); + } + const text = typeof reply.body === "string" ? reply.body : JSON.stringify(reply.body); + return { + ok: reply.status >= 200 && reply.status < 300, + status: reply.status, + text: async () => text, + } as Response; + }) as typeof fetch; + return { fetch: f, calls }; +} + +describe("Controller", () => { + it("listSnapshots returns parsed JSON", async () => { + const { fetch: f, calls } = mockFetch([ + { status: 200, body: [{ tag: "py", dir: "/x", created_at_unix: 1 }] }, + ]); + const c = new Controller({ baseUrl: "http://test/", fetch: f }); + const result = await c.listSnapshots(); + expect(result).toEqual([{ tag: "py", dir: "/x", created_at_unix: 1 }]); + expect(calls[0]!.url).toBe("http://test/v1/snapshots"); + expect((calls[0]!.init as RequestInit).method).toBe("GET"); + }); + + it("spawnSandboxes serializes camelCase → snake_case", async () => { + const { fetch: f, calls } = mockFetch([ + { status: 201, body: [{ id: "sb-1", snapshot_tag: "py" }] }, + ]); + const c = new Controller({ fetch: f }); + await c.spawnSandboxes({ + snapshotTag: "py", + n: 3, + perChildNetns: true, + memoryLimitMib: 512, + prewarm: true, + }); + const init = calls[0]!.init as RequestInit; + const body = JSON.parse(init.body as string); + expect(body).toEqual({ + snapshot_tag: "py", + n: 3, + per_child_netns: true, + memory_limit_mib: 512, + prewarm: true, + }); + }); + + it("branchSandbox passes diff option through", async () => { + const { fetch: f, calls } = mockFetch([ + { status: 201, body: { tag: "b1", dir: "/x", created_at_unix: 1 } }, + ]); + const c = new Controller({ fetch: f }); + await c.branchSandbox("sb-1", { tag: "b1", diff: true }); + const body = JSON.parse( + (calls[0]!.init as RequestInit).body as string, + ); + expect(body).toEqual({ tag: "b1", diff: true }); + }); + + it("raises ControllerError on non-2xx with JSON body", async () => { + // Two replies because we call getSandbox twice (one rejects.toBe, + // one in a try/catch to inspect status+body). + const { fetch: f } = mockFetch([ + { status: 404, body: { error: "not found" } }, + { status: 404, body: { error: "not found" } }, + ]); + const c = new Controller({ fetch: f }); + await expect(c.getSandbox("sb-missing")).rejects.toBeInstanceOf( + ControllerError, + ); + try { + await c.getSandbox("sb-missing"); + throw new Error("should have thrown"); + } catch (e) { + expect(e).toBeInstanceOf(ControllerError); + expect((e as ControllerError).status).toBe(404); + expect((e as ControllerError).body).toEqual({ error: "not found" }); + } + }); + + it("authorization header set when token provided", async () => { + const { fetch: f, calls } = mockFetch([{ status: 200, body: [] }]); + const c = new Controller({ token: "abc123", fetch: f }); + await c.listSnapshots(); + const headers = (calls[0]!.init as RequestInit).headers as Record< + string, + string + >; + expect(headers.authorization).toBe("Bearer abc123"); + }); + + it("FORKD_URL env honored as default baseUrl", async () => { + const orig = process.env.FORKD_URL; + process.env.FORKD_URL = "http://from-env"; + try { + const c = new Controller(); + expect(c.baseUrl).toBe("http://from-env"); + } finally { + if (orig === undefined) delete process.env.FORKD_URL; + else process.env.FORKD_URL = orig; + } + }); + + it("DELETE returns void / does not parse empty body", async () => { + const { fetch: f } = mockFetch([{ status: 204, body: "" }]); + const c = new Controller({ fetch: f }); + // Should not throw on empty body. + await c.killSandbox("sb-1"); + }); + + it("encodes sandbox id in URL path", async () => { + const { fetch: f, calls } = mockFetch([{ status: 200, body: { id: "x" } }]); + const c = new Controller({ baseUrl: "http://x", fetch: f }); + await c.getSandbox("sb id with space"); + expect(calls[0]!.url).toBe("http://x/v1/sandboxes/sb%20id%20with%20space"); + }); + + it("aborts after timeout", async () => { + // Realistic abort path: fetch that respects AbortSignal. The + // Controller schedules a setTimeout(timeoutMs) that calls + // controller.abort(); a well-behaved fetch rejects with AbortError. + const abortableFetch: typeof fetch = ((_url, init) => + new Promise((_, reject) => { + const sig = (init as RequestInit | undefined)?.signal; + if (!sig) return; + sig.addEventListener("abort", () => { + const err = new Error("aborted"); + err.name = "AbortError"; + reject(err); + }); + })) as typeof fetch; + const c = new Controller({ timeoutMs: 10, fetch: abortableFetch }); + await expect(c.listSnapshots()).rejects.toThrow(/abort/i); + }); +}); diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json new file mode 100644 index 0000000..9ace774 --- /dev/null +++ b/sdk/typescript/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +}