diff --git a/rock/ts-sdk/.eslintrc.js b/rock/ts-sdk/.eslintrc.js new file mode 100644 index 000000000..83e6b054e --- /dev/null +++ b/rock/ts-sdk/.eslintrc.js @@ -0,0 +1,43 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2022, + sourceType: 'module', + }, + rules: { + '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'warn', + 'no-constant-condition': ['error', { checkLoops: false }], + // Enforce camelCase for methods and functions + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'method', + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + { + selector: 'function', + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allow', + }, + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allow', + }, + { + selector: 'parameter', + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + ], + }, + ignorePatterns: ['dist', 'node_modules', 'examples'], +}; diff --git a/rock/ts-sdk/.gitignore b/rock/ts-sdk/.gitignore new file mode 100644 index 000000000..6a9016435 --- /dev/null +++ b/rock/ts-sdk/.gitignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Test coverage +coverage/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Environment +.env +.env.local +.env.*.local + +# Cache +.cache/ +.npm/ diff --git a/rock/ts-sdk/CHANGELOG.md b/rock/ts-sdk/CHANGELOG.md new file mode 100644 index 000000000..a6737157c --- /dev/null +++ b/rock/ts-sdk/CHANGELOG.md @@ -0,0 +1,84 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.2.7] - 2026-03-11 + +### Fixed + +- HTTP errors now preserve `response` property for status code detection + - Previously, `HttpUtils.post()`, `get()`, and `postMultipart()` wrapped errors in generic `Error` objects, losing HTTP status code information + - Now re-throws original `AxiosError`, allowing callers to access `error.response.status` (e.g., 401, 403, 500) + - Consistent with Python SDK behavior + +## [1.2.4] - 2026-02-16 + +### Added + +- `HttpResponse` interface with `status`, `result`, `error`, and `headers` fields +- Response header extraction in `HttpUtils` methods (`get`, `post`, `postMultipart`) +- New fields in `SandboxStatusResponse`: `cluster`, `requestId`, `eagleeyeTraceid` +- Header info extraction in `Sandbox.getStatus()` for debugging and tracing + +### Changed + +- `HttpUtils.get()`, `post()`, `postMultipart()` now return `HttpResponse` instead of `T` +- Improved error messages to include backend `error` field when available +- Updated `EnvHubClient` to adapt to new `HttpResponse` return type + +## [1.2.1] - 2025-02-12 + +### Added + +- Initial TypeScript SDK release based on Python SDK `rl-rock` +- Apache License 2.0 +- **Sandbox Module** + - `Sandbox` class for managing remote container sandboxes + - `SandboxGroup` class for batch sandbox operations + - `Deploy` class for deploying working directories + - `FileSystem` class for file operations (chown, chmod, uploadDir) + - `Network` class for network acceleration configuration + - `Process` class for script execution + - `RemoteUser` class for user management + - `RuntimeEnv` framework for Python/Node.js runtime management + - `SpeedupType` enum for acceleration types (APT, PIP, GitHub) +- **EnvHub Module** + - `EnvHubClient` for environment registration and management + - `RockEnvInfo` schema for environment information +- **Envs Module** + - `RockEnv` class with Gym-style interface (step, reset, close) + - `make()` factory function +- **Model Module** + - `ModelClient` for LLM communication + - `ModelService` for local model service management +- **Common Module** + - `Codes` enum for status codes + - Exception classes (`RockException`, `BadRequestRockError`, etc.) +- **Utils Module** + - `HttpUtils` class with axios backend + - `retryAsync` and `withRetry` decorators + - `deprecated` and `deprecatedClass` decorators +- **Logger** + - Winston-based logging with timezone support +- **Types** + - Zod schemas for request/response validation + - Full TypeScript type definitions + +### Technical Details + +- Built with TypeScript 5.x +- Dual ESM/CommonJS module support via tsup +- Tested with Jest (59 test cases) +- Dependencies: axios, zod, winston, ali-oss + +## [Unreleased] + +### Planned + +- Agent framework (RockAgent, SWEAgent, OpenHands agent) +- More comprehensive test coverage +- Documentation improvements +- Performance optimizations diff --git a/rock/ts-sdk/LICENSE b/rock/ts-sdk/LICENSE new file mode 100644 index 000000000..f49a4e16e --- /dev/null +++ b/rock/ts-sdk/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/rock/ts-sdk/README.md b/rock/ts-sdk/README.md new file mode 100644 index 000000000..3792b36ed --- /dev/null +++ b/rock/ts-sdk/README.md @@ -0,0 +1,321 @@ +# ROCK TypeScript SDK + +[![npm version](https://img.shields.io/npm/v/rl-rock.svg)](https://www.npmjs.com/package/rl-rock) +[![License](https://img.shields.io/npm/l/rl-rock.svg)](https://github.com/Timandes/ROCK/blob/master/LICENSE) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/) + +ROCK (Remote Operation Container Kit) TypeScript SDK - 用于管理远程沙箱环境的客户端库。 + +## 特性 + +- 🚀 **沙箱管理** - 创建、启动、停止远程容器沙箱 +- 📁 **文件系统** - 上传、下载、读取、写入文件 +- 🖥️ **命令执行** - 同步/异步执行 Shell 命令 +- 🔧 **运行时环境** - 支持 Python、Node.js 运行时环境管理(沙箱内安装) +- 🤖 **模型服务** - 沙箱内模型服务安装与生命周期管理 +- 🎯 **Agent 框架** - 内置 Agent 支持自动化任务编排 +- 📦 **EnvHub** - 环境注册与管理 +- 🔄 **双模式构建** - 同时支持 ESM 和 CommonJS + +## 安装 + +```bash +# 使用 pnpm +pnpm add rl-rock + +# 使用 npm +npm install rl-rock + +# 使用 yarn +yarn add rl-rock +``` + +## 快速开始 + +### 创建沙箱 + +```typescript +import { Sandbox, SandboxConfig } from 'rl-rock'; + +// 创建沙箱实例 +const sandbox = new Sandbox({ + image: 'python:3.11', + baseUrl: 'http://localhost:8080', + cluster: 'default', + memory: '8g', + cpus: 2, +}); + +// 启动沙箱 +await sandbox.start(); + +console.log(`Sandbox ID: ${sandbox.getSandboxId()}`); +``` + +### 执行命令 + +```typescript +// 同步执行命令 +const result = await sandbox.arun('ls -la', { + mode: 'normal', +}); + +console.log(result.output); + +// 后台执行命令 (nohup 模式) +const bgResult = await sandbox.arun('python long_running_script.py', { + mode: 'nohup', + waitTimeout: 600, +}); +``` + +### 文件操作 + +```typescript +// 写入文件 +await sandbox.write_file({ + content: 'Hello, ROCK!', + path: '/tmp/hello.txt', +}); + +// 读取文件 +const fileContent = await sandbox.read_file({ + path: '/tmp/hello.txt', +}); + +// 上传本地文件 +await sandbox.upload({ + sourcePath: './local-file.txt', + targetPath: '/remote/path/file.txt', +}); +``` + +### 使用 EnvHub + +```typescript +import { EnvHubClient } from 'rl-rock'; + +const client = new EnvHubClient({ + baseUrl: 'http://localhost:8081', +}); + +// 注册环境 +await client.register({ + envName: 'my-python-env', + image: 'python:3.11', + description: 'My Python environment', + tags: ['python', 'ml'], +}); + +// 获取环境 +const env = await client.getEnv('my-python-env'); +``` + +### 使用 RockEnv (Gym 风格接口) + +```typescript +import { make, RockEnv } from 'rl-rock'; + +// 创建环境 +const env = make('my-env-id'); + +// 重置环境 +const [observation, info] = await env.reset(); + +// 执行步骤 +const [obs, reward, terminated, truncated, info] = await env.step('action'); + +// 关闭环境 +await env.close(); +``` + +### 使用 RuntimeEnv (沙箱内运行时环境) + +```typescript +import { Sandbox, RuntimeEnv, PythonRuntimeEnvConfig } from 'rl-rock'; + +const sandbox = new Sandbox({ ... }); +await sandbox.start(); + +// 创建 Python 运行时环境配置 +const pythonConfig: PythonRuntimeEnvConfig = { + type: 'python', + version: '3.11', + pipPackages: ['requests', 'numpy'], +}; + +// 在沙箱内安装 Python 运行时 +const runtimeEnv = await RuntimeEnv.create(sandbox, pythonConfig); + +// 使用 Python 执行命令 +await runtimeEnv.run('python -c "import requests; print(requests.__version__)"'); +``` + +### 使用 ModelService (沙箱内模型服务) + +```typescript +import { Sandbox, ModelService, ModelServiceConfig } from 'rl-rock'; + +const sandbox = new Sandbox({ ... }); +await sandbox.start(); + +// 创建模型服务配置 +const modelServiceConfig: ModelServiceConfig = { + enabled: true, + installCmd: 'pip install rl_rock[model-service]', +}; + +// 在沙箱内安装并启动模型服务 +const modelService = new ModelService(sandbox, modelServiceConfig); +await modelService.install(); +await modelService.start(); + +// 监控 Agent 进程 +await modelService.watchAgent('12345'); + +// 停止服务 +await modelService.stop(); +``` + +### 使用 Agent (自动化任务编排) + +```typescript +import { Sandbox, DefaultAgent, RockAgentConfig } from 'rl-rock'; + +const sandbox = new Sandbox({ ... }); +await sandbox.start(); + +// 配置 Agent +const agentConfig: RockAgentConfig = { + agentSession: 'my-agent-session', + runCmd: 'python agent.py --prompt {prompt}', + workingDir: './my-project', // 本地目录,自动上传到沙箱 + runtimeEnvConfig: { type: 'python', version: '3.11' }, + modelServiceConfig: { enabled: true }, +}; + +// 创建并安装 Agent +const agent = new DefaultAgent(sandbox); +await agent.install(agentConfig); + +// 运行 Agent +const result = await agent.run('Please analyze the codebase'); +console.log(result.output); +``` + +## 配置 + +### 环境变量 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `ROCK_BASE_URL` | ROCK 服务基础 URL | `http://localhost:8080` | +| `ROCK_ENVHUB_BASE_URL` | EnvHub 服务 URL | `http://localhost:8081` | +| `ROCK_SANDBOX_STARTUP_TIMEOUT_SECONDS` | 沙箱启动超时时间 | `180` | +| `ROCK_OSS_ENABLE` | 是否启用 OSS 上传 | `false` | +| `ROCK_OSS_BUCKET_ENDPOINT` | OSS Endpoint | - | +| `ROCK_OSS_BUCKET_NAME` | OSS Bucket 名称 | - | + +### SandboxConfig 选项 + +```typescript +interface SandboxConfig { + image: string; // Docker 镜像 + baseUrl: string; // 服务 URL + cluster: string; // 集群名称 + memory: string; // 内存限制 (如 '8g') + cpus: number; // CPU 核心数 + autoClearSeconds: number; // 自动清理时间 + startupTimeout: number; // 启动超时 + routeKey?: string; // 路由键 + extraHeaders?: Record; // 额外请求头 +} +``` + +## API 文档 + +详细 API 文档请参阅 [开发者手册](./docs/DEVELOPER_GUIDE.md)。 + +## 示例 + +更多示例请参阅 [examples](./examples/) 目录。 + +### 示例列表 + +| 文件 | 说明 | +|------|------| +| `basic-usage.ts` | 基础沙箱使用示例 | +| `file-operations.ts` | 文件操作示例 | +| `background-tasks.ts` | 后台任务 (nohup) 示例 | +| `envhub-usage.ts` | EnvHub 环境管理示例 | +| `sandbox-group.ts` | 沙箱组批量操作示例 | +| `complete-workflow.ts` | 完整开发工作流示例 | +| `runtime-env-usage.ts` | 运行时环境管理示例 | +| `model-service-usage.ts` | 模型服务使用示例 | +| `agent-usage.ts` | Agent 自动化任务示例 | + +### 运行示例 + +```bash +# 进入 ts-sdk 目录 +cd ts-sdk + +# 安装依赖 (如未安装) +pnpm install + +# 设置环境变量 +export ROCK_BASE_URL=http://your-rock-server:8080 +export ROCK_ENVHUB_BASE_URL=http://your-envhub-server:8081 + +# 运行示例 (使用 tsx) +npx tsx examples/basic-usage.ts + +# 或使用 ts-node +npx ts-node examples/basic-usage.ts +``` + +## 开发 + +```bash +# 安装依赖 +pnpm install + +# 运行测试 +pnpm test + +# 构建 +pnpm build + +# 类型检查 +pnpm exec tsc --noEmit +``` + +## 从 Python SDK 迁移 + +TypeScript SDK 与 Python SDK API 基本一致,主要差异: + +| Python | TypeScript | +|--------|-----------| +| `sandbox.arun(cmd, mode="nohup")` | `sandbox.arun(cmd, { mode: 'nohup' })` | +| `await sandbox.fs.upload_dir(...)` | `await sandbox.getFs().uploadDir(...)` | +| `sandbox.process.execute_script(...)` | `sandbox.getProcess().executeScript(...)` | + +### 响应字段命名 + +SDK 使用 **camelCase** 命名规范,符合 TypeScript 约定。HTTP 层自动处理 snake_case 到 camelCase 的转换: + +```typescript +// 响应示例 - 使用 camelCase +const status = await sandbox.getStatus(); +console.log(status.sandboxId); // ✓ 正确 +console.log(status.hostName); // ✓ 正确 +console.log(status.isAlive); // ✓ 正确 + +const result = await sandbox.arun('ls'); +console.log(result.exitCode); // ✓ 正确 +console.log(result.failureReason);// ✓ 正确 +``` + +## License + +Apache License 2.0 diff --git a/rock/ts-sdk/docs/DEVELOPER_GUIDE.md b/rock/ts-sdk/docs/DEVELOPER_GUIDE.md new file mode 100644 index 000000000..8467738c4 --- /dev/null +++ b/rock/ts-sdk/docs/DEVELOPER_GUIDE.md @@ -0,0 +1,624 @@ +# ROCK TypeScript SDK 开发者手册 + +## 目录 + +1. [架构概览](#架构概览) +2. [核心模块](#核心模块) +3. [API 参考](#api-参考) +4. [配置指南](#配置指南) +5. [错误处理](#错误处理) +6. [日志系统](#日志系统) +7. [高级用法](#高级用法) +8. [最佳实践](#最佳实践) + +--- + +## 架构概览 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ROCK TypeScript SDK │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ Sandbox │ │ EnvHub │ │ Envs │ │ +│ │ 沙箱管理 │ │ 环境注册 │ │ Gym风格接口 │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ +│ │ │ │ │ +│ ┌──────┴──────┐ │ ┌──────┴──────┐ │ +│ │ Sub-modules│ │ │ RockEnv │ │ +│ │ - FileSystem│ │ └─────────────┘ │ +│ │ - Network │ │ │ +│ │ - Process │ │ │ +│ │ - Deploy │ │ │ +│ │ - RuntimeEnv│ │ │ +│ └─────────────┘ │ │ +│ │ │ +│ ┌───────────────────────┴───────────────────────────────┐ │ +│ │ Model Module │ │ +│ │ ModelClient / ModelService │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +├─────────────────────────────────────────────────────────────┤ +│ Foundation Layer │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ +│ │ Types │ │ Utils │ │ Logger │ │ Common │ │ +│ │ (Zod) │ │ (HTTP) │ │ (Winston)│ │ (Exceptions) │ │ +│ └──────────┘ └──────────┘ └──────────┘ └────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 核心模块 + +### Sandbox 模块 + +沙箱管理是 SDK 的核心功能,提供远程容器环境的完整生命周期管理。 + +#### Sandbox 类 + +```typescript +import { Sandbox } from 'rl-rock'; + +const sandbox = new Sandbox({ + image: 'python:3.11', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + memory: '8g', + cpus: 2, + autoClearSeconds: 3600, // 1小时后自动清理 +}); +``` + +#### 生命周期方法 + +| 方法 | 说明 | +|------|------| +| `start()` | 启动沙箱 | +| `stop()` | 停止沙箱 | +| `close()` | 关闭并清理资源 | +| `isAlive()` | 检查沙箱是否存活 | +| `getStatus()` | 获取沙箱状态 | + +#### 命令执行 + +```typescript +// 同步执行 (等待命令完成) +const result = await sandbox.arun('echo "Hello"', { + mode: 'normal', + session: 'default', +}); + +// 后台执行 (nohup 模式) +const bgResult = await sandbox.arun('python train.py', { + mode: 'nohup', + waitTimeout: 3600, // 最长等待1小时 + waitInterval: 30, // 每30秒检查一次 + ignoreOutput: false, // 是否忽略输出 + outputFile: '/tmp/train.log', // 输出文件路径 +}); + +// 结果结构 (使用 camelCase,符合 TypeScript 规范) +interface Observation { + output: string; + exitCode?: number; + failureReason: string; + expectString: string; +} +``` + +#### 会话管理 + +```typescript +// 创建会话 +await sandbox.createSession({ + session: 'my-session', + startupSource: [], + envEnable: true, + env: { MY_VAR: 'value' }, +}); + +// 在会话中执行命令 +await sandbox.arun('source venv/bin/activate', { + session: 'my-session', + mode: 'normal', +}); + +// 关闭会话 +await sandbox.closeSession({ session: 'my-session' }); +``` + +### SandboxGroup 类 + +批量管理多个沙箱实例: + +```typescript +import { SandboxGroup } from 'rl-rock'; + +const group = new SandboxGroup({ + image: 'python:3.11', + size: 10, // 创建10个沙箱 + startConcurrency: 3, // 同时启动3个 + startRetryTimes: 3, // 失败重试3次 +}); + +// 启动所有沙箱 +await group.start(); + +// 获取沙箱列表 +const sandboxes = group.getSandboxList(); + +// 并行执行任务 +await Promise.all( + sandboxes.map((sandbox, i) => + sandbox.arun(`python task_${i}.py`) + ) +); + +// 停止所有沙箱 +await group.stop(); +``` + +--- + +## API 参考 + +### FileSystem + +```typescript +const fs = sandbox.getFs(); + +// 修改文件所有者 +await fs.chown({ + remoteUser: 'appuser', + paths: ['/data/app'], + recursive: true, +}); + +// 修改文件权限 +await fs.chmod({ + paths: ['/data/app/scripts.sh'], + mode: '755', + recursive: false, +}); + +// 上传目录 +const result = await fs.uploadDir( + '/local/project', + '/remote/project', + 600 // 提取超时 +); +``` + +### Network + +```typescript +const network = sandbox.getNetwork(); + +// 配置 APT 镜像加速 +await network.speedup( + SpeedupType.APT, + 'http://mirrors.aliyun.com' +); + +// 配置 PIP 镜像加速 +await network.speedup( + SpeedupType.PIP, + 'https://mirrors.aliyun.com/pypi/simple/' +); + +// 配置 GitHub 加速 +await network.speedup( + SpeedupType.GITHUB, + '11.11.11.11' // GitHub IP +); +``` + +### Process + +```typescript +const process = sandbox.getProcess(); + +// 执行脚本 +const result = await process.executeScript( + `#!/bin/bash +echo "Starting deployment..." +npm install +npm run build +echo "Deployment complete!"`, + { + scriptName: 'deploy.sh', + waitTimeout: 600, + cleanup: true, // 执行后删除脚本 + } +); +``` + +### Deploy + +```typescript +const deploy = sandbox.getDeploy(); + +// 部署工作目录 +await deploy.deployWorkingDir( + './my-project', + '/workspace/my-project' +); + +// 使用模板命令 +const cmd = deploy.format('cd ${working_dir} && npm test'); +await sandbox.arun(cmd); +``` + +--- + +## 配置指南 + +### 环境变量配置 + +创建 `.env` 文件或在系统中设置: + +```bash +# 基础配置 +ROCK_BASE_URL=http://your-rock-server:8080 +ROCK_ENVHUB_BASE_URL=http://your-envhub-server:8081 + +# 沙箱配置 +ROCK_SANDBOX_STARTUP_TIMEOUT_SECONDS=300 + +# OSS 配置 (大文件上传) +ROCK_OSS_ENABLE=true +ROCK_OSS_BUCKET_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com +ROCK_OSS_BUCKET_NAME=your-bucket +ROCK_OSS_BUCKET_REGION=oss-cn-hangzhou + +# 日志配置 +ROCK_LOGGING_PATH=/var/log/rock +ROCK_LOGGING_LEVEL=debug +ROCK_TIME_ZONE=Asia/Shanghai +``` + +### 代码中配置 + +```typescript +import { setEnvVars } from 'rl-rock'; + +// 或通过环境变量 +process.env.ROCK_BASE_URL = 'http://custom-server:8080'; +``` + +--- + +## 错误处理 + +### 异常类型 + +```typescript +import { + RockException, + BadRequestRockError, + InternalServerRockError, + CommandRockError, + raiseForCode, +} from 'rl-rock'; + +try { + await sandbox.start(); +} catch (e) { + if (e instanceof BadRequestRockError) { + // 客户端错误 (4xxx) + console.error('Bad request:', e.message); + } else if (e instanceof InternalServerRockError) { + // 服务端错误 (5xxx) + console.error('Server error:', e.message); + } else if (e instanceof CommandRockError) { + // 命令执行错误 (6xxx) - 不抛出异常,返回在结果中 + console.error('Command failed:', e.message); + } else { + throw e; + } +} +``` + +### 错误码 + +| 范围 | 类型 | 说明 | +|------|------|------| +| 2000-2999 | Success | 成功 | +| 4000-4999 | Client Error | 客户端错误 | +| 5000-5999 | Server Error | 服务端错误 | +| 6000-6999 | Command Error | 命令执行错误 | + +--- + +## 日志系统 + +### 基本使用 + +```typescript +import { initLogger } from 'rl-rock'; + +const logger = initLogger('my-module'); + +logger.debug('Debug message'); +logger.info('Info message'); +logger.warn('Warning message'); +logger.error('Error message'); +``` + +### 日志格式 + +``` +2025-02-12T10:30:45.123+08:00 INFO:client.ts:100 [rock.sandbox] [sandbox-id] [trace-id] -- Sandbox started successfully +``` + +### 创建带沙箱上下文的日志 + +```typescript +import { createSandboxLogger } from 'rl-rock'; + +const logger = createSandboxLogger('my-module', sandbox.getSandboxId()); +logger.info('This message includes sandbox context'); +``` + +--- + +## 高级用法 + +### 自定义 HTTP 客户端 + +```typescript +import { HttpUtils } from 'rl-rock'; + +// 创建自定义 HTTP 客户端 +const client = new HttpUtils({ + timeout: 60000, + baseURL: 'https://custom-api.example.com', +}); + +const response = await client.get('/endpoint'); +``` + +### 重试机制 + +```typescript +import { withRetry, retryAsync } from 'rl-rock'; + +// 使用装饰器 +class MyService { + @withRetry({ maxAttempts: 3, delaySeconds: 1, backoff: 2 }) + async fetchData(): Promise { + // 自动重试 + } +} + +// 使用函数包装 +const result = await retryAsync( + () => sandbox.arun('flaky-command'), + { maxAttempts: 5, delaySeconds: 2 } +); +``` + +### 并发控制 + +```typescript +import { SandboxGroup } from 'rl-rock'; + +// 使用 Semaphore 控制并发 +import { Semaphore } from 'async-mutex'; + +const semaphore = new Semaphore(5); + +const tasks = sandboxes.map(async (sandbox) => { + const [, release] = await semaphore.acquire(); + try { + return await sandbox.arun('heavy-task'); + } finally { + release(); + } +}); +``` + +--- + +## 最佳实践 + +### 1. 资源清理 + +```typescript +// 始终在 finally 中清理资源 +let sandbox: Sandbox | null = null; +try { + sandbox = new Sandbox(config); + await sandbox.start(); + // ... 工作 +} finally { + if (sandbox) { + await sandbox.close(); + } +} + +// 或使用 async context (推荐) +async function withSandbox( + config: SandboxConfig, + fn: (sandbox: Sandbox) => Promise +): Promise { + const sandbox = new Sandbox(config); + try { + await sandbox.start(); + return await fn(sandbox); + } finally { + await sandbox.close(); + } +} +``` + +### 2. 错误处理 + +```typescript +// 区分可恢复和不可恢复错误 +async function executeTask(sandbox: Sandbox) { + const result = await sandbox.arun('task-command'); + + if (result.exitCode !== 0) { + // 命令执行失败,可重试 + logger.warn(`Task failed: ${result.failureReason}`); + return { success: false, error: result.failureReason }; + } + + return { success: true, output: result.output }; +} +``` + +### 3. 超时设置 + +```typescript +// 为长时间操作设置合理超时 +const result = await sandbox.arun('long-running-task', { + mode: 'nohup', + waitTimeout: 3600, // 1小时 + waitInterval: 60, // 每分钟检查 +}); +``` + +### 4. 会话隔离 + +```typescript +// 为不同任务使用独立会话 +const sessionId = `task-${Date.now()}`; + +await sandbox.createSession({ + session: sessionId, + envEnable: true, + env: taskSpecificEnv, +}); + +try { + await sandbox.arun('task', { session: sessionId }); +} finally { + await sandbox.closeSession({ session: sessionId }); +} +``` + +--- + +## 类型定义 + +完整类型定义请参考 `dist/index.d.ts` 或源码中的 Zod schemas。 + +### 命名约定 + +SDK 使用 **camelCase** 命名规范,符合 TypeScript 约定。HTTP 层自动处理 snake_case 到 camelCase 的转换: + +```typescript +// SandboxStatusResponse +status.sandboxId // ✓ 正确 +status.sandbox_id // ✗ 错误 + +// Observation +result.exitCode // ✓ 正确 +result.exit_code // ✗ 错误 + +result.failureReason // ✓ 正确 +result.failure_reason // ✗ 错误 +``` + +### 主要类型 + +```typescript +// 配置类型 +interface SandboxConfig { ... } +interface SandboxGroupConfig { ... } + +// 请求类型 +interface Command { ... } +interface CreateBashSessionRequest { ... } +interface WriteFileRequest { ... } +interface ReadFileRequest { ... } + +// 响应类型 (camelCase,符合 TypeScript 规范) +interface Observation { + output: string; + exitCode?: number; + failureReason: string; + expectString: string; +} + +interface CommandResponse { + stdout: string; + stderr: string; + exitCode?: number; +} + +interface SandboxStatusResponse { + sandboxId?: string; + hostName?: string; + hostIp?: string; + isAlive: boolean; + image?: string; + gatewayVersion?: string; + sweRexVersion?: string; + userId?: string; + experimentId?: string; + namespace?: string; + cpus?: number; + memory?: string; + portMapping?: Record; + status?: Record; +} + +// 枚举 +enum SpeedupType { APT, PIP, GITHUB } +enum RunMode { NORMAL, NOHUP } +``` + +--- + +## 常见问题 + +### Q: 如何调试沙箱问题? + +```typescript +// 启用详细日志 +process.env.ROCK_LOGGING_LEVEL = 'debug'; + +// 获取沙箱状态 +const status = await sandbox.getStatus(); +console.log(status); +console.log(`Sandbox ID: ${status.sandboxId}`); +console.log(`Is alive: ${status.isAlive}`); +console.log(`Port mapping: ${JSON.stringify(status.portMapping)}`); +``` + +### Q: 如何处理大文件上传? + +```typescript +// 启用 OSS 上传 (> 1MB 自动使用 OSS) +process.env.ROCK_OSS_ENABLE = 'true'; +process.env.ROCK_OSS_BUCKET_NAME = 'your-bucket'; +// ... 其他 OSS 配置 + +await sandbox.upload({ + sourcePath: './large-file.zip', + targetPath: '/remote/large-file.zip', +}); +``` + +### Q: 如何实现并行任务? + +```typescript +const group = new SandboxGroup({ + image: 'python:3.11', + size: 10, + startConcurrency: 5, +}); + +await group.start(); + +const results = await Promise.allSettled( + group.getSandboxList().map((s, i) => + s.arun(`python task_${i}.py`) + ) +); +``` diff --git a/rock/ts-sdk/examples/agent-usage.ts b/rock/ts-sdk/examples/agent-usage.ts new file mode 100644 index 000000000..1f088d3f8 --- /dev/null +++ b/rock/ts-sdk/examples/agent-usage.ts @@ -0,0 +1,152 @@ +/** + * Agent 自动化任务示例 + * + * 演示如何使用 Agent 进行自动化任务编排 + * Agent 会自动管理运行时环境、模型服务和命令执行 + */ + +import { Sandbox, DefaultAgent, RockAgentConfig, RunMode } from '../src'; +import * as path from 'path'; + +// 示例 Agent 代码(将被上传到沙箱) +const AGENT_CODE = ` +import sys +import json + +def main(): + prompt = sys.argv[1] if len(sys.argv) > 1 else "Hello" + + result = { + "status": "success", + "prompt_received": prompt, + "message": f"Agent processed: {prompt}" + } + + print(json.dumps(result, indent=2)) + +if __name__ == "__main__": + main() +`; + +async function agentExample() { + console.log('=== ROCK TypeScript SDK Agent 示例 ===\n'); + + // 创建沙箱 + console.log('1. 创建并启动沙箱...'); + const sandbox = new Sandbox({ + image: 'ubuntu:22.04', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + memory: '8g', + cpus: 4, + }); + + try { + await sandbox.start(); + console.log(` 沙箱已启动: ${sandbox.getSandboxId()}\n`); + + // ==================== 准备工作目录 ==================== + console.log('2. 准备 Agent 工作目录...'); + const workDir = path.join(process.cwd(), 'examples', 'temp-agent-workdir'); + + // 创建临时工作目录并写入 Agent 代码 + const fs = await import('fs'); + if (!fs.existsSync(workDir)) { + fs.mkdirSync(workDir, { recursive: true }); + } + fs.writeFileSync(path.join(workDir, 'agent.py'), AGENT_CODE); + console.log(` 工作目录: ${workDir}\n`); + + // ==================== 配置 Agent ==================== + console.log('3. 配置 Agent...'); + const agentConfig: RockAgentConfig = { + // Agent 标识 + agentType: 'custom-agent', + agentSession: 'my-agent-session', + + // 工作目录(本地目录会被上传到沙箱) + workingDir: workDir, + + // 项目路径(沙箱内的路径) + projectPath: '/workspace/agent-project', + useDeployWorkingDirAsFallback: true, + + // 运行时环境配置 + runtimeEnvConfig: { + type: 'python', + version: '3.11', + pipPackages: [], + }, + + // 运行命令({prompt} 会被替换为实际提示) + runCmd: 'python agent.py {prompt}', + skipWrapRunCmd: false, + + // 超时配置 + agentInstallTimeout: 600, + agentRunTimeout: 1800, + agentRunCheckInterval: 30, + + // 环境变量 + env: { + AGENT_MODE: 'demo', + }, + + // 预初始化命令 + preInitCmds: [ + { command: 'echo "Pre-init: Setting up environment"', timeoutSeconds: 60 }, + ], + + // 后初始化命令 + postInitCmds: [ + { command: 'echo "Post-init: Environment ready"', timeoutSeconds: 60 }, + ], + + // 模型服务配置(可选) + modelServiceConfig: null, + }; + console.log(' Agent 配置完成\n'); + + // ==================== 创建并安装 Agent ==================== + console.log('4. 创建 Agent 实例...'); + const agent = new DefaultAgent(sandbox); + console.log(' Agent 实例已创建\n'); + + console.log('5. 安装 Agent(这会安装运行时环境、上传工作目录)...'); + await agent.install(agentConfig); + console.log(' Agent 安装完成\n'); + + // ==================== 运行 Agent ==================== + console.log('6. 运行 Agent...'); + const result = await agent.run('Please analyze the codebase'); + console.log(' Agent 执行结果:'); + console.log(` 退出码: ${result.exitCode}`); + console.log(` 输出: ${result.output.trim()}\n`); + + // ==================== 再次运行 ==================== + console.log('7. 再次运行 Agent(复用已安装的环境)...'); + const result2 = await agent.run('Generate a summary'); + console.log(' Agent 执行结果:'); + console.log(` 退出码: ${result2.exitCode}`); + console.log(` 输出: ${result2.output.trim()}\n`); + + // ==================== 清理 ==================== + console.log('8. 清理资源...'); + await sandbox.close(); + + // 清理临时目录 + fs.rmSync(workDir, { recursive: true, force: true }); + console.log(' 临时目录已清理'); + console.log(' 沙箱已关闭'); + + console.log('\n✅ Agent 示例完成!'); + + } catch (error) { + console.error('\n❌ 示例失败:', error); + await sandbox.close().catch(() => {}); + process.exit(1); + } +} + +// 运行示例 +agentExample().catch(console.error); diff --git a/rock/ts-sdk/examples/background-tasks.ts b/rock/ts-sdk/examples/background-tasks.ts new file mode 100644 index 000000000..82be66bf7 --- /dev/null +++ b/rock/ts-sdk/examples/background-tasks.ts @@ -0,0 +1,109 @@ +/** + * 后台任务示例 + * + * 演示使用 nohup 模式执行长时间运行的任务 + */ + +import { Sandbox, RunMode } from '../src'; + +async function backgroundTaskExample() { + console.log('=== ROCK TypeScript SDK 后台任务示例 ===\n'); + + const sandbox = new Sandbox({ + image: 'reg.docker.alibaba-inc.com/yanan/python:3.11', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + }); + + try { + await sandbox.start(); + console.log(`沙箱已启动: ${sandbox.getSandboxId()}\n`); + + // 1. 创建一个模拟的长时间运行脚本 + console.log('1. 创建长时间运行脚本...'); + const scriptContent = ` +import time +import sys + +print("开始执行长时间任务...") +for i in range(10): + print(f"进度: {i+1}/10") + sys.stdout.flush() + time.sleep(2) +print("任务完成!") +`; + + await sandbox.writeFile({ + content: scriptContent, + path: '/tmp/long_task.py', + }); + console.log(' 脚本已创建: /tmp/long_task.py'); + + // 2. 使用 nohup 模式执行 + console.log('2. 使用 nohup 模式执行 (后台运行)...'); + const startTime = Date.now(); + + const result = await sandbox.arun('python3 /tmp/long_task.py', { + mode: RunMode.NOHUP, + waitTimeout: 60, // 最长等待60秒 + waitInterval: 5, // 每5秒检查一次 + outputFile: '/tmp/task_output.log', + }); + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(` 执行完成,耗时: ${elapsed}秒`); + console.log(` 退出码: ${result.exitCode}`); + console.log(` 输出:\n${result.output}`); + + // 3. 演示 ignoreOutput 模式 + console.log('3. 演示 ignoreOutput 模式 (不读取输出)...'); + + await sandbox.writeFile({ + content: 'import time; time.sleep(3); print("Done!")', + path: '/tmp/quick_task.py', + }); + + const quickResult = await sandbox.arun('python3 /tmp/quick_task.py', { + mode: RunMode.NOHUP, + waitTimeout: 10, + waitInterval: 2, // 每2秒检查一次 + ignoreOutput: true, // 不读取输出,适合超大输出 + }); + console.log(` 任务状态: ${quickResult.exitCode === 0 ? '成功' : '失败'}`); + console.log(` 消息: ${quickResult.output}`); + + // 4. 并行执行多个任务 + console.log('4. 并行执行多个后台任务...'); + + const tasks = [ + 'echo "Task A" && sleep 2 && echo "A done"', + 'echo "Task B" && sleep 3 && echo "B done"', + 'echo "Task C" && sleep 1 && echo "C done"', + ]; + + const parallelStart = Date.now(); + + const results = await Promise.all( + tasks.map((cmd, i) => + sandbox.arun(cmd, { + mode: RunMode.NOHUP, + waitTimeout: 30, + session: `task-${i}`, // 使用独立会话 + }) + ) + ); + + const parallelElapsed = ((Date.now() - parallelStart) / 1000).toFixed(1); + console.log(` 并行任务完成,耗时: ${parallelElapsed}秒`); + results.forEach((r, i) => { + console.log(` 任务 ${i}: 退出码=${r.exitCode}`); + }); + + } finally { + console.log('\n关闭沙箱...'); + await sandbox.close(); + } +} + +// 运行示例 +backgroundTaskExample().catch(console.error); diff --git a/rock/ts-sdk/examples/basic-usage.ts b/rock/ts-sdk/examples/basic-usage.ts new file mode 100644 index 000000000..0ed39f05c --- /dev/null +++ b/rock/ts-sdk/examples/basic-usage.ts @@ -0,0 +1,66 @@ +/** + * 基础沙箱使用示例 + * + * 演示如何创建、管理和使用沙箱环境 + */ + +import { Sandbox, RunMode } from '../src'; + +async function basicExample() { + console.log('=== ROCK TypeScript SDK 基础示例 ===\n'); + + // 1. 创建沙箱 + console.log('1. 创建沙箱...'); + const sandbox = new Sandbox({ + image: 'reg.docker.alibaba-inc.com/yanan/python:3.11', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + memory: '4g', + cpus: 2, + autoClearSeconds: 1800, // 30分钟后自动清理 + }); + + try { + // 2. 启动沙箱 + console.log('2. 启动沙箱...'); + await sandbox.start(); + console.log(` 沙箱已启动: ${sandbox.getSandboxId()}`); + + // 3. 检查状态 + console.log('3. 检查沙箱状态...'); + const status = await sandbox.getStatus(); + console.log(` 状态: ${status.isAlive ? '运行中' : '已停止'}`); + console.log(` 镜像: ${status.image}`); + + // 4. 执行简单命令 + console.log('4. 执行命令...'); + const result = await sandbox.arun('echo "Hello from ROCK SDK!" && pwd', { + mode: RunMode.NORMAL, + }); + console.log(` 输出: ${result.output.trim()}`); + console.log(` 退出码: ${result.exitCode}`); + + // 5. 使用 Python + console.log('5. 运行 Python 代码...'); + const pythonResult = await sandbox.arun( + 'python3 -c "import sys; print(f\'Python {sys.version}\')"', + { mode: RunMode.NORMAL } + ); + console.log(` Python 版本: ${pythonResult.output.trim().split('\n')[0]}`); + + // 6. 文件操作示例 + console.log('6. 文件操作...'); + await sandbox.arun('echo "Hello ROCK" > /tmp/test.txt', { mode: RunMode.NORMAL }); + const catResult = await sandbox.arun('cat /tmp/test.txt', { mode: RunMode.NORMAL }); + console.log(` 文件内容: ${catResult.output.trim()}`); + + } finally { + // 7. 清理 + console.log('\n7. 关闭沙箱...'); + await sandbox.close(); + console.log(' 沙箱已关闭'); + } +} + +// 运行示例 +basicExample().catch(console.error); diff --git a/rock/ts-sdk/examples/complete-workflow.ts b/rock/ts-sdk/examples/complete-workflow.ts new file mode 100644 index 000000000..d205a1c63 --- /dev/null +++ b/rock/ts-sdk/examples/complete-workflow.ts @@ -0,0 +1,204 @@ +/** + * 完整工作流示例 + * + * 演示一个完整的开发工作流: + * 1. 创建沙箱 + * 2. 配置环境 + * 3. 部署代码 + * 4. 运行测试 + * 5. 收集结果 + */ + +import { Sandbox, RunMode, SpeedupType } from '../src'; +import * as fs from 'fs'; +import * as path from 'path'; + +// 示例项目代码 +const PROJECT_CODE = { + 'main.py': ` +import json +from calculator import Calculator + +def main(): + calc = Calculator() + + results = { + 'add': calc.add(10, 5), + 'subtract': calc.subtract(10, 5), + 'multiply': calc.multiply(10, 5), + 'divide': calc.divide(10, 5), + } + + print(json.dumps(results, indent=2)) + +if __name__ == '__main__': + main() +`, + 'calculator.py': ` +class Calculator: + """Simple calculator class.""" + + def add(self, a: float, b: float) -> float: + return a + b + + def subtract(self, a: float, b: float) -> float: + return a - b + + def multiply(self, a: float, b: float) -> float: + return a * b + + def divide(self, a: float, b: float) -> float: + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b +`, + 'test_calculator.py': ` +import unittest +from calculator import Calculator + +class TestCalculator(unittest.TestCase): + def setUp(self): + self.calc = Calculator() + + def test_add(self): + self.assertEqual(self.calc.add(2, 3), 5) + self.assertEqual(self.calc.add(-1, 1), 0) + + def test_subtract(self): + self.assertEqual(self.calc.subtract(5, 3), 2) + self.assertEqual(self.calc.subtract(1, 1), 0) + + def test_multiply(self): + self.assertEqual(self.calc.multiply(3, 4), 12) + self.assertEqual(self.calc.multiply(0, 5), 0) + + def test_divide(self): + self.assertEqual(self.calc.divide(10, 2), 5) + self.assertAlmostEqual(self.calc.divide(7, 2), 3.5) + + def test_divide_by_zero(self): + with self.assertRaises(ValueError): + self.calc.divide(1, 0) + +if __name__ == '__main__': + unittest.main(verbosity=2) +`, + 'requirements.txt': ` +# No external dependencies for this simple project +`, +}; + +async function workflowExample() { + console.log('=== ROCK TypeScript SDK 完整工作流示例 ===\n'); + console.log('演示:创建 Python 项目、部署、测试、运行\n'); + + const sandbox = new Sandbox({ + image: 'reg.docker.alibaba-inc.com/yanan/python:3.11', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + memory: '4g', + cpus: 2, + }); + + try { + // ==================== 阶段 1: 环境准备 ==================== + console.log('📁 阶段 1: 环境准备'); + console.log('-----------------------------------'); + + console.log(' 1.1 启动沙箱...'); + await sandbox.start(); + console.log(` 沙箱ID: ${sandbox.getSandboxId()}`); + + console.log(' 1.2 配置网络加速...'); + const network = sandbox.getNetwork(); + try { + await network.speedup(SpeedupType.PIP, 'https://mirrors.aliyun.com/pypi/simple/'); + console.log(' PIP 镜像已配置'); + } catch { + console.log(' PIP 配置跳过 (可能已在镜像网络)'); + } + + // ==================== 阶段 2: 项目部署 ==================== + console.log('\n📦 阶段 2: 项目部署'); + console.log('-----------------------------------'); + + console.log(' 2.1 创建项目目录...'); + await sandbox.arun('mkdir -p /workspace/my-project', { mode: RunMode.NORMAL }); + + console.log(' 2.2 部署项目文件...'); + for (const [filename, content] of Object.entries(PROJECT_CODE)) { + await sandbox.writeFile({ + content: content.trim(), + path: `/workspace/my-project/${filename}`, + }); + console.log(` 已部署: ${filename}`); + } + + console.log(' 2.3 验证项目结构...'); + const treeResult = await sandbox.arun('ls -la /workspace/my-project/', { + mode: RunMode.NORMAL, + }); + console.log(` 项目文件:\n${treeResult.output.split('\n').map(l => ' ' + l).join('\n')}`); + + // ==================== 阶段 3: 测试运行 ==================== + console.log('\n🧪 阶段 3: 测试运行'); + console.log('-----------------------------------'); + + console.log(' 3.1 运行单元测试...'); + const testResult = await sandbox.arun( + 'cd /workspace/my-project && python3 test_calculator.py', + { mode: RunMode.NORMAL, timeout: 60 } + ); + console.log(` 测试输出:\n${testResult.output.split('\n').slice(0, 15).map(l => ' ' + l).join('\n')}`); + console.log(` 退出码: ${testResult.exitCode}`); + + // ==================== 阶段 4: 执行应用 ==================== + console.log('\n🚀 阶段 4: 执行应用'); + console.log('-----------------------------------'); + + console.log(' 4.1 运行主程序...'); + const runResult = await sandbox.arun( + 'cd /workspace/my-project && python3 main.py', + { mode: RunMode.NORMAL } + ); + console.log(` 输出:\n${runResult.output.split('\n').map(l => ' ' + l).join('\n')}`); + + // ==================== 阶段 5: 结果收集 ==================== + console.log('\n📊 阶段 5: 结果收集'); + console.log('-----------------------------------'); + + // 解析输出结果 + try { + const outputJson = JSON.parse(runResult.output); + console.log(' 计算结果:'); + console.log(` 加法: 10 + 5 = ${outputJson.add}`); + console.log(` 减法: 10 - 5 = ${outputJson.subtract}`); + console.log(` 乘法: 10 * 5 = ${outputJson.multiply}`); + console.log(` 除法: 10 / 5 = ${outputJson.divide}`); + } catch { + console.log(' 结果解析失败'); + } + + // ==================== 阶段 6: 资源清理 ==================== + console.log('\n🧹 阶段 6: 资源清理'); + console.log('-----------------------------------'); + + console.log(' 6.1 清理临时文件...'); + await sandbox.arun('rm -rf /workspace/my-project', { mode: RunMode.NORMAL }); + console.log(' 临时文件已清理'); + + console.log(' 6.2 关闭沙箱...'); + await sandbox.close(); + console.log(' 沙箱已关闭'); + + console.log('\n✅ 工作流完成!'); + + } catch (error) { + console.error('\n❌ 工作流失败:', error); + await sandbox.close().catch(() => {}); + process.exit(1); + } +} + +// 运行示例 +workflowExample().catch(console.error); diff --git a/rock/ts-sdk/examples/envhub-usage.ts b/rock/ts-sdk/examples/envhub-usage.ts new file mode 100644 index 000000000..1ed3feb52 --- /dev/null +++ b/rock/ts-sdk/examples/envhub-usage.ts @@ -0,0 +1,96 @@ +/** + * EnvHub 使用示例 + * + * 演示环境注册和管理 + */ + +import { EnvHubClient, EnvHubClientConfig } from '../src'; + +async function envHubExample() { + console.log('=== ROCK TypeScript SDK EnvHub 示例 ===\n'); + + // 创建 EnvHub 客户端 + const config: EnvHubClientConfig = { + baseUrl: process.env.ROCK_ENVHUB_BASE_URL || 'http://localhost:8081', + }; + + const client = new EnvHubClient(config); + + try { + // 1. 健康检查 + console.log('1. 检查 EnvHub 服务状态...'); + const health = await client.healthCheck(); + console.log(` 服务状态: ${JSON.stringify(health)}`); + + // 2. 注册环境 + console.log('2. 注册新环境...'); + const envInfo = await client.register({ + envName: 'my-python-env', + image: 'python:3.11-slim', + owner: 'developer', + description: 'My Python development environment', + tags: ['python', 'development', 'ml'], + extraSpec: { + pythonVersion: '3.11', + packages: ['numpy', 'pandas', 'scikit-learn'], + }, + }); + console.log(` 环境已注册: ${envInfo.envName}`); + console.log(` 镜像: ${envInfo.image}`); + + // 3. 获取环境信息 + console.log('3. 获取环境信息...'); + const fetchedEnv = await client.getEnv('my-python-env'); + console.log(` 名称: ${fetchedEnv.envName}`); + console.log(` 描述: ${fetchedEnv.description}`); + console.log(` 标签: ${fetchedEnv.tags?.join(', ') || '无'}`); + + // 4. 列出所有环境 + console.log('4. 列出所有环境...'); + const allEnvs = await client.listEnvs(); + console.log(` 共 ${allEnvs.length} 个环境:`); + allEnvs.forEach((env, i) => { + console.log(` ${i + 1}. ${env.envName} (${env.image})`); + }); + + // 5. 按标签筛选 + console.log('5. 按标签筛选环境...'); + const pythonEnvs = await client.listEnvs({ + tags: ['python'], + }); + console.log(` 带 'python' 标签的环境: ${pythonEnvs.length} 个`); + + // 6. 按所有者筛选 + console.log('6. 按所有者筛选环境...'); + const myEnvs = await client.listEnvs({ + owner: 'developer', + }); + console.log(` 所有者为 'developer' 的环境: ${myEnvs.length} 个`); + + // 7. 删除环境 + console.log('7. 删除环境...'); + const deleted = await client.deleteEnv('my-python-env'); + console.log(` 删除结果: ${deleted ? '成功' : '环境不存在'}`); + + // 验证删除 + try { + await client.getEnv('my-python-env'); + console.log(' 警告: 环境仍然存在'); + } catch (e) { + console.log(' 确认: 环境已被删除'); + } + + } catch (error) { + console.error('操作失败:', error); + + // 检查是否是服务不可用 + if (error instanceof Error && error.message.includes('Failed to')) { + console.log('\n提示: 请确保 EnvHub 服务正在运行:'); + console.log(' - 检查 ROCK_ENVHUB_BASE_URL 环境变量'); + console.log(' - 确认服务端口是否正确'); + } + } +} + +// 运行示例 +envHubExample().catch(console.error); diff --git a/rock/ts-sdk/examples/file-operations.ts b/rock/ts-sdk/examples/file-operations.ts new file mode 100644 index 000000000..cfa5159b3 --- /dev/null +++ b/rock/ts-sdk/examples/file-operations.ts @@ -0,0 +1,116 @@ +/** + * 文件操作示例 + * + * 演示文件上传、下载、读写等操作 + */ + +import { Sandbox, RunMode } from '../src'; +import * as fs from 'fs'; +import * as path from 'path'; + +async function fileOperationsExample() { + console.log('=== ROCK TypeScript SDK 文件操作示例 ===\n'); + + const sandbox = new Sandbox({ + image: 'python:3.11', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + }); + + try { + await sandbox.start(); + console.log(`沙箱已启动: ${sandbox.getSandboxId()}\n`); + + // 1. 写入文件 + console.log('1. 写入文件...'); + await sandbox.writeFile({ + content: 'Hello, this is a test file created by ROCK SDK!', + path: '/tmp/test-file.txt', + }); + console.log(' 文件已写入: /tmp/test-file.txt'); + + // 2. 读取文件 + console.log('2. 读取文件...'); + const readResult = await sandbox.readFile({ + path: '/tmp/test-file.txt', + }); + console.log(` 内容: ${readResult.content}`); + + // 3. 创建本地测试文件并上传 + console.log('3. 上传本地文件...'); + const localFile = '/tmp/local-example.txt'; + fs.writeFileSync(localFile, 'This is a local file content.\nSecond line.'); + + await sandbox.upload({ + sourcePath: localFile, + targetPath: '/workspace/uploaded-file.txt', + }); + console.log(' 本地文件已上传: /workspace/uploaded-file.txt'); + + // 验证上传 + const uploadedContent = await sandbox.readFile({ + path: '/workspace/uploaded-file.txt', + }); + console.log(` 验证内容: ${uploadedContent.content.substring(0, 30)}...`); + + // 4. 使用命令操作文件 + console.log('4. 使用命令操作文件...'); + + // 列出目录 + const lsResult = await sandbox.arun('ls -la /workspace/', { + mode: RunMode.NORMAL, + }); + console.log(` 目录内容:\n${lsResult.output}`); + + // 创建目录结构 + await sandbox.arun('mkdir -p /workspace/project/src', { + mode: RunMode.NORMAL, + }); + + // 写入多个文件 + await sandbox.writeFile({ + content: 'console.log("Hello from Node.js!");', + path: '/workspace/project/src/index.js', + }); + + await sandbox.writeFile({ + content: JSON.stringify({ name: 'test-project', version: '1.0.0' }, null, 2), + path: '/workspace/project/package.json', + }); + + // 查看项目结构 + const treeResult = await sandbox.arun('find /workspace/project -type f', { + mode: RunMode.NORMAL, + }); + console.log(` 项目结构:\n${treeResult.output}`); + + // 5. 文件权限操作 + console.log('5. 文件权限操作...'); + + const sandboxFs = sandbox.getFs(); + + // 修改权限 + await sandboxFs.chmod({ + paths: ['/workspace/project/src/index.js'], + mode: '755', + recursive: false, + }); + console.log(' 已修改文件权限为 755'); + + // 验证权限 + const permResult = await sandbox.arun('ls -la /workspace/project/src/index.js', { + mode: RunMode.NORMAL, + }); + console.log(` 权限: ${permResult.output.trim()}`); + + // 清理本地测试文件 + fs.unlinkSync(localFile); + + } finally { + console.log('\n关闭沙箱...'); + await sandbox.close(); + } +} + +// 运行示例 +fileOperationsExample().catch(console.error); diff --git a/rock/ts-sdk/examples/model-service-usage.ts b/rock/ts-sdk/examples/model-service-usage.ts new file mode 100644 index 000000000..c6eadb0d0 --- /dev/null +++ b/rock/ts-sdk/examples/model-service-usage.ts @@ -0,0 +1,93 @@ +/** + * 模型服务使用示例 + * + * 演示如何在沙箱内安装和管理模型服务 + * 模型服务用于支持 Agent 与 LLM 的交互 + */ + +import { Sandbox, RuntimeEnv, PythonRuntimeEnvConfig, ModelService, ModelServiceConfig } from '../src'; + +async function modelServiceExample() { + console.log('=== ROCK TypeScript SDK 模型服务示例 ===\n'); + + // 创建沙箱 + console.log('1. 创建并启动沙箱...'); + const sandbox = new Sandbox({ + image: 'ubuntu:22.04', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + memory: '8g', + cpus: 4, + }); + + try { + await sandbox.start(); + console.log(` 沙箱已启动: ${sandbox.getSandboxId()}\n`); + + // ==================== 安装 Python 运行时 ==================== + console.log('2. 安装 Python 运行时环境...'); + const pythonConfig: PythonRuntimeEnvConfig = { + type: 'python', + version: '3.11', + pipIndexUrl: 'https://mirrors.aliyun.com/pypi/simple/', + }; + + const pythonEnv = await RuntimeEnv.create(sandbox, pythonConfig); + console.log(` Python 运行时已安装: ${pythonEnv.binDir}\n`); + + // ==================== 创建模型服务 ==================== + console.log('3. 创建模型服务...'); + const modelServiceConfig: ModelServiceConfig = { + enabled: true, + // 安装命令:使用 pip 安装 rl_rock 包(包含模型服务) + installCmd: 'pip install rl_rock[model-service]', + installTimeout: 600, + // 模型服务配置 + configIniCmd: 'mkdir -p ~/.rock && touch ~/.rock/config.ini', + // 日志配置 + loggingPath: '/tmp/rock-model-service/logs', + loggingFileName: 'model-service.log', + }; + + const modelService = new ModelService(sandbox, modelServiceConfig); + console.log(' 模型服务已创建\n'); + + // ==================== 安装模型服务 ==================== + console.log('4. 安装模型服务包...'); + await modelService.install(); + console.log(' 模型服务包已安装\n'); + + // ==================== 启动模型服务 ==================== + console.log('5. 启动模型服务...'); + await modelService.start(); + console.log(' 模型服务已启动'); + console.log(` 运行状态: ${modelService.isStarted ? '运行中' : '已停止'}\n`); + + // ==================== 演示 watchAgent ==================== + console.log('6. 模型服务功能说明...'); + console.log(' - watchAgent(pid): 监控指定进程,保持模型服务活跃'); + console.log(' - antiCallLlm(index, responsePayload): 与 Agent 进行交互'); + console.log(' - start(): 启动模型服务'); + console.log(' - stop(): 停止模型服务\n'); + + // ==================== 停止服务 ==================== + console.log('7. 停止模型服务...'); + await modelService.stop(); + console.log(' 模型服务已停止\n'); + + // ==================== 清理 ==================== + console.log('8. 关闭沙箱...'); + await sandbox.close(); + console.log(' 沙箱已关闭'); + + console.log('\n✅ 模型服务示例完成!'); + + } catch (error) { + console.error('\n❌ 示例失败:', error); + await sandbox.close().catch(() => {}); + process.exit(1); + } +} + +// 运行示例 +modelServiceExample().catch(console.error); diff --git a/rock/ts-sdk/examples/runtime-env-usage.ts b/rock/ts-sdk/examples/runtime-env-usage.ts new file mode 100644 index 000000000..fbdd748ce --- /dev/null +++ b/rock/ts-sdk/examples/runtime-env-usage.ts @@ -0,0 +1,99 @@ +/** + * 运行时环境管理示例 + * + * 演示如何在沙箱内安装和管理 Python/Node.js 运行时环境 + */ + +import { Sandbox, RunMode, RuntimeEnv, PythonRuntimeEnvConfig, NodeRuntimeEnvConfig } from '../src'; + +async function runtimeEnvExample() { + console.log('=== ROCK TypeScript SDK 运行时环境示例 ===\n'); + + // 创建沙箱 + console.log('1. 创建并启动沙箱...'); + const sandbox = new Sandbox({ + image: 'ubuntu:22.04', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + memory: '4g', + cpus: 2, + }); + + try { + await sandbox.start(); + console.log(` 沙箱已启动: ${sandbox.getSandboxId()}\n`); + + // ==================== Python 运行时环境 ==================== + console.log('2. 安装 Python 运行时环境...'); + const pythonConfig: PythonRuntimeEnvConfig = { + type: 'python', + version: '3.11', + pipPackages: ['requests', 'numpy'], + pipIndexUrl: 'https://mirrors.aliyun.com/pypi/simple/', + }; + + const pythonEnv = await RuntimeEnv.create(sandbox, pythonConfig); + console.log(` Python 运行时已安装`); + console.log(` 工作目录: ${pythonEnv.workdir}`); + console.log(` Bin 目录: ${pythonEnv.binDir}\n`); + + // 验证 Python 安装 + console.log('3. 验证 Python 安装...'); + const pythonVersion = await pythonEnv.run('python --version'); + console.log(` ${pythonVersion.output.trim()}`); + + // 验证 pip 包 + const pipList = await pythonEnv.run('pip list | grep -E "requests|numpy"'); + console.log(` 已安装的包:\n${pipList.output.split('\n').map(l => ' ' + l).join('\n')}\n`); + + // 运行 Python 代码 + console.log('4. 运行 Python 代码...'); + const pythonCode = await pythonEnv.run( + 'python -c "import numpy as np; print(f\'NumPy version: {np.__version__}\')"' + ); + console.log(` ${pythonCode.output.trim()}\n`); + + // ==================== Node.js 运行时环境 ==================== + console.log('5. 安装 Node.js 运行时环境...'); + const nodeConfig: NodeRuntimeEnvConfig = { + type: 'node', + version: '22.18.0', + npmRegistry: 'https://registry.npmmirror.com', + }; + + const nodeEnv = await RuntimeEnv.create(sandbox, nodeConfig); + console.log(` Node.js 运行时已安装`); + console.log(` 工作目录: ${nodeEnv.workdir}`); + console.log(` Bin 目录: ${nodeEnv.binDir}\n`); + + // 验证 Node.js 安装 + console.log('6. 验证 Node.js 安装...'); + const nodeVersion = await nodeEnv.run('node --version'); + console.log(` Node.js 版本: ${nodeVersion.output.trim()}`); + + const npmVersion = await nodeEnv.run('npm --version'); + console.log(` npm 版本: ${npmVersion.output.trim()}\n`); + + // ==================== 使用 wrappedCmd ==================== + console.log('7. 使用 wrappedCmd 自动添加 PATH...'); + // wrappedCmd 会自动在命令前添加 PATH 环境变量 + const wrappedCmd = pythonEnv.wrappedCmd('python -c "print(\'Hello from wrapped cmd!\')"'); + const wrappedResult = await sandbox.arun(wrappedCmd, { mode: RunMode.NORMAL }); + console.log(` ${wrappedResult.output.trim()}\n`); + + // ==================== 清理 ==================== + console.log('8. 关闭沙箱...'); + await sandbox.close(); + console.log(' 沙箱已关闭'); + + console.log('\n✅ 运行时环境示例完成!'); + + } catch (error) { + console.error('\n❌ 示例失败:', error); + await sandbox.close().catch(() => {}); + process.exit(1); + } +} + +// 运行示例 +runtimeEnvExample().catch(console.error); diff --git a/rock/ts-sdk/examples/sandbox-group.ts b/rock/ts-sdk/examples/sandbox-group.ts new file mode 100644 index 000000000..d55416d89 --- /dev/null +++ b/rock/ts-sdk/examples/sandbox-group.ts @@ -0,0 +1,142 @@ +/** + * 沙箱组批量操作示例 + * + * 演示如何批量创建和管理多个沙箱 + */ + +import { SandboxGroup, RunMode } from '../src'; + +async function sandboxGroupExample() { + console.log('=== ROCK TypeScript SDK 沙箱组示例 ===\n'); + + // 1. 创建沙箱组 + console.log('1. 创建沙箱组 (5个沙箱)...'); + const group = new SandboxGroup({ + image: 'python:3.11-slim', + baseUrl: process.env.ROCK_BASE_URL || 'http://localhost:8080', + cluster: 'default', + size: 5, // 创建5个沙箱 + startConcurrency: 2, // 同时启动2个 + startRetryTimes: 3, // 失败重试3次 + memory: '2g', + cpus: 1, + autoClearSeconds: 1800, + }); + + try { + // 2. 批量启动 + console.log('2. 批量启动沙箱 (并发数=2)...'); + const startBegin = Date.now(); + await group.start(); + const startElapsed = ((Date.now() - startBegin) / 1000).toFixed(1); + console.log(` 所有沙箱已启动,耗时: ${startElapsed}秒\n`); + + const sandboxes = group.getSandboxList(); + console.log(` 沙箱列表:`); + sandboxes.forEach((s, i) => { + console.log(` ${i + 1}. ${s.getSandboxId()}`); + }); + + // 3. 并行执行相同任务 + console.log('\n3. 并行执行相同任务...'); + const taskResults = await Promise.all( + sandboxes.map(async (sandbox, index) => { + const result = await sandbox.arun( + `echo "Sandbox ${index + 1} reporting" && hostname && date`, + { mode: RunMode.NORMAL } + ); + return { index, id: sandbox.getSandboxId(), output: result.output.trim() }; + }) + ); + + taskResults.forEach(({ index, id, output }) => { + console.log(` 沙箱 ${index + 1} (${id?.substring(0, 8)}...):`); + console.log(` ${output.replace(/\n/g, '\n ')}`); + }); + + // 4. 分配不同任务 + console.log('\n4. 分配不同任务给不同沙箱...'); + const tasks = [ + 'python3 -c "print(sum(range(100)))"', + 'python3 -c "import math; print(math.pi)"', + 'python3 -c "print([x**2 for x in range(10)])"', + 'python3 -c "import json; print(json.dumps({\\"status\\": \\"ok\\"}))"', + 'python3 -c "print(len(\\"Hello, World!\\"))"', + ]; + + const diffResults = await Promise.all( + sandboxes.map((sandbox, i) => + sandbox.arun(tasks[i] || 'echo "No task"', { mode: RunMode.NORMAL }) + ) + ); + + diffResults.forEach((result, i) => { + console.log(` 任务 ${i + 1}: ${result.output.trim()}`); + }); + + // 5. MapReduce 风格处理 + console.log('\n5. MapReduce 风格数据处理...'); + + // 准备数据 + const data = Array.from({ length: 100 }, (_, i) => i + 1); + const chunkSize = Math.ceil(data.length / sandboxes.length); + + // Map: 分配计算任务 + const mapResults = await Promise.all( + sandboxes.map(async (sandbox, i) => { + const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize); + const script = ` +import json +data = json.loads('${JSON.stringify(chunk)}') +result = sum(x * x for x in data) +print(result) +`; + await sandbox.writeFile({ + content: script, + path: `/tmp/map_task_${i}.py`, + }); + + const result = await sandbox.arun(`python3 /tmp/map_task_${i}.py`, { + mode: RunMode.NORMAL, + }); + + return parseInt(result.output.trim(), 10); + }) + ); + + // Reduce: 合并结果 + const totalSum = mapResults.reduce((acc, val) => acc + val, 0); + console.log(` 各分片结果: [${mapResults.join(', ')}]`); + console.log(` 总和 (平方和): ${totalSum}`); + + // 验证 + const expected = data.reduce((acc, x) => acc + x * x, 0); + console.log(` 验证结果: ${expected} (${totalSum === expected ? '正确' : '错误'})`); + + // 6. 健康检查 + console.log('\n6. 健康检查...'); + const healthChecks = await Promise.all( + sandboxes.map(async (sandbox, i) => { + try { + const alive = await sandbox.isAlive(); + return { index: i, alive: alive.isAlive }; + } catch { + return { index: i, alive: false }; + } + }) + ); + + healthChecks.forEach(({ index, alive }) => { + console.log(` 沙箱 ${index + 1}: ${alive ? '健康' : '异常'}`); + }); + + } finally { + // 7. 批量关闭 + console.log('\n7. 批量关闭沙箱...'); + await group.stop(); + console.log(' 所有沙箱已关闭'); + } +} + +// 运行示例 +sandboxGroupExample().catch(console.error); diff --git a/rock/ts-sdk/jest.config.js b/rock/ts-sdk/jest.config.js new file mode 100644 index 000000000..f8c714ecb --- /dev/null +++ b/rock/ts-sdk/jest.config.js @@ -0,0 +1,48 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +const baseConfig = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + module: 'ESNext', + moduleResolution: 'bundler', + experimentalDecorators: true, + }, + }, + ], + }, + extensionsToTreatAsEsm: ['.ts'], +}; + +module.exports = { + projects: [ + // Unit tests - fast, isolated tests in src/ + { + ...baseConfig, + displayName: 'unit', + roots: ['/src'], + testMatch: ['**/*.test.ts'], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/**/index.ts', + ], + coverageDirectory: 'coverage', + }, + // Integration tests - tests that require external services + { + ...baseConfig, + displayName: 'integration', + roots: ['/tests/integration'], + testMatch: ['**/*.test.ts'], + }, + ], +}; \ No newline at end of file diff --git a/rock/ts-sdk/package.json b/rock/ts-sdk/package.json new file mode 100644 index 000000000..8358881bf --- /dev/null +++ b/rock/ts-sdk/package.json @@ -0,0 +1,69 @@ +{ + "name": "rl-rock", + "version": "1.2.7", + "description": "ROCK TypeScript SDK - Sandbox management and agent framework", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "test": "jest", + "test:unit": "jest --selectProjects unit", + "test:integration": "jest --selectProjects integration --forceExit", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src --ext .ts", + "clean": "rm -rf dist", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "sandbox", + "agent", + "llm", + "ai", + "rock" + ], + "author": "", + "license": "Apache-2.0", + "engines": { + "node": ">=20.8.0" + }, + "dependencies": { + "ali-oss": "^6.21.0", + "axios": "^1.7.9", + "ts-case-convert": "^2.1.0", + "winston": "^3.17.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/ali-oss": "^6.16.11", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.5", + "@typescript-eslint/eslint-plugin": "^8.55.0", + "@typescript-eslint/parser": "^8.55.0", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.32.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "tsup": "^8.3.5", + "typescript": "^5.7.3" + }, + "repository": { + "type": "git", + "url": "https://github.com/alibaba/ROCK.git" + }, + "bugs": { + "url": "https://github.com/alibaba/ROCK/issues" + }, + "homepage": "https://github.com/alibaba/ROCK#readme" +} diff --git a/rock/ts-sdk/pnpm-lock.yaml b/rock/ts-sdk/pnpm-lock.yaml new file mode 100644 index 000000000..7493873af --- /dev/null +++ b/rock/ts-sdk/pnpm-lock.yaml @@ -0,0 +1,5932 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + ali-oss: + specifier: ^6.21.0 + version: 6.23.0 + axios: + specifier: ^1.7.9 + version: 1.13.5 + ts-case-convert: + specifier: ^2.1.0 + version: 2.1.0 + winston: + specifier: ^3.17.0 + version: 3.19.0 + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + '@types/ali-oss': + specifier: ^6.16.11 + version: 6.23.2 + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + '@types/node': + specifier: ^22.10.5 + version: 22.19.11 + '@typescript-eslint/eslint-plugin': + specifier: ^8.55.0 + version: 8.55.0(@typescript-eslint/parser@8.55.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.55.0 + version: 8.55.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-plugin-import: + specifier: ^2.32.0 + version: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.11) + ts-jest: + specifier: ^29.2.5 + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.11))(typescript@5.9.3) + tsup: + specifier: ^8.3.5 + version: 8.5.1(typescript@5.9.3) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + + '@types/ali-oss@6.23.2': + resolution: {integrity: sha512-j3n+kskvDpceQjnf4tA2pEagneSOdAS6oLQ9lnhpn4ipTvvN8i8iAF1y5Pn2g0/xREOsPXBhCbS0ox3hwuaOBQ==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.19.11': + resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.55.0': + resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.55.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.55.0': + resolution: {integrity: sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.55.0': + resolution: {integrity: sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.55.0': + resolution: {integrity: sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.55.0': + resolution: {integrity: sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.55.0': + resolution: {integrity: sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.55.0': + resolution: {integrity: sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.55.0': + resolution: {integrity: sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.55.0': + resolution: {integrity: sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.55.0': + resolution: {integrity: sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + agentkeepalive@3.5.3: + resolution: {integrity: sha512-yqXL+k5rr8+ZRpOAntkaaRgWgE5o8ESAj5DyRmVTCSoZxXmqemb9Dd7T4i5UzwuERdLAJUy6XzR9zFVuf0kzkw==} + engines: {node: '>= 4.0.0'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ali-oss@6.23.0: + resolution: {integrity: sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==} + engines: {node: '>=8'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + bowser@1.9.4: + resolution: {integrity: sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-to@2.0.1: + resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dateformat@2.2.0: + resolution: {integrity: sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-user-agent@1.0.0: + resolution: {integrity: sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==} + engines: {node: '>= 0.10.0'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + digest-header@1.1.0: + resolution: {integrity: sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==} + engines: {node: '>= 8.0.0'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + end-or-error@1.0.1: + resolution: {integrity: sha512-OclLMSug+k2A0JKuf494im25ANRBVW8qsjmwbgX7lQ8P82H21PQ1PWkoYwb9y5yMBS69BPlwtzdIFClo3+7kOQ==} + engines: {node: '>= 0.11.14'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.1: + resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formstream@1.5.2: + resolution: {integrity: sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-ready@1.0.0: + resolution: {integrity: sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-class-hotfix@0.0.6: + resolution: {integrity: sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-type-of@1.4.0: + resolution: {integrity: sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-base64@2.6.4: + resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jstoxml@2.2.9: + resolution: {integrity: sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-hex@1.0.1: + resolution: {integrity: sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==} + engines: {node: '>=8.0.0'} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + os-name@1.0.3: + resolution: {integrity: sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==} + engines: {node: '>=0.10.0'} + hasBin: true + + osx-release@1.1.0: + resolution: {integrity: sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==} + engines: {node: '>=0.10.0'} + hasBin: true + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.4: + resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} + engines: {node: '>=11.0.0'} + + sdk-base@2.0.1: + resolution: {integrity: sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + stream-http@2.8.2: + resolution: {integrity: sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==} + + stream-wormhole@1.1.0: + resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} + engines: {node: '>=4.0.0'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-arraybuffer@1.0.1: + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-case-convert@2.1.0: + resolution: {integrity: sha512-Ye79el/pHYXfoew6kqhMwCoxp4NWjKNcm2kBzpmEMIU9dd9aBmHNNFtZ+WTm0rz1ngyDmfqDXDlyUnBXayiD0w==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unescape@1.0.1: + resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==} + engines: {node: '>=0.10.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + urllib@2.44.0: + resolution: {integrity: sha512-zRCJqdfYllRDA9bXUtx+vccyRqtJPKsw85f44zH7zPD28PIvjMqIgw9VwoTLV7xTBWZsbebUFVHU5ghQcWku2A==} + engines: {node: '>= 0.10.0'} + peerDependencies: + proxy-agent: ^5.0.0 + peerDependenciesMeta: + proxy-agent: + optional: true + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utility@1.18.0: + resolution: {integrity: sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==} + engines: {node: '>= 0.12.0'} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + win-release@1.1.1: + resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==} + engines: {node: '>=0.10.0'} + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.19.11) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.19.11 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 22.19.11 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.2.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.19.11 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@sinclair/typebox@0.27.10': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + + '@types/ali-oss@6.23.2': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.19.11 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/json5@0.0.29': {} + + '@types/node@22.19.11': + dependencies: + undici-types: 6.21.0 + + '@types/stack-utils@2.0.3': {} + + '@types/triple-beam@1.3.5': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.55.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/type-utils': 8.55.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.55.0 + eslint: 8.57.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.55.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.55.0 + debug: 4.4.3 + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.55.0': + dependencies: + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 + + '@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.55.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.55.0(eslint@8.57.1)(typescript@5.9.3) + debug: 4.4.3 + eslint: 8.57.1 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.55.0': {} + + '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/visitor-keys': 8.55.0 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.55.0(eslint@8.57.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.55.0 + '@typescript-eslint/types': 8.55.0 + '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + eslint: 8.57.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.55.0': + dependencies: + '@typescript-eslint/types': 8.55.0 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + address@1.2.2: {} + + agentkeepalive@3.5.3: + dependencies: + humanize-ms: 1.2.1 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ali-oss@6.23.0: + dependencies: + address: 1.2.2 + agentkeepalive: 3.5.3 + bowser: 1.9.4 + copy-to: 2.0.1 + dateformat: 2.2.0 + debug: 4.4.3 + destroy: 1.2.0 + end-or-error: 1.0.1 + get-ready: 1.0.0 + humanize-ms: 1.2.1 + is-type-of: 1.4.0 + js-base64: 2.6.4 + jstoxml: 2.2.9 + lodash: 4.17.23 + merge-descriptors: 1.0.3 + mime: 2.6.0 + platform: 1.3.6 + pump: 3.0.3 + qs: 6.14.2 + sdk-base: 2.0.1 + stream-http: 2.8.2 + stream-wormhole: 1.1.0 + urllib: 2.44.0 + utility: 1.18.0 + xml2js: 0.6.2 + transitivePeerDependencies: + - proxy-agent + - supports-color + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axios@1.13.5: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + babel-jest@29.7.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.28.0 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@29.6.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.9.19: {} + + bowser@1.9.4: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + builtin-status-codes@3.0.0: {} + + bundle-require@5.1.0(esbuild@0.27.3): + dependencies: + esbuild: 0.27.3 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001769: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.3: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + + color-name@1.1.4: {} + + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + copy-to@2.0.1: {} + + core-util-is@1.0.3: {} + + create-jest@29.7.0(@types/node@22.19.11): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@22.19.11) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + dateformat@2.2.0: {} + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.1: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-user-agent@1.0.0: + dependencies: + os-name: 1.0.3 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delayed-stream@1.0.0: {} + + destroy@1.2.0: {} + + detect-newline@3.1.0: {} + + diff-sequences@29.6.3: {} + + digest-header@1.1.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.286: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + enabled@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + end-or-error@1.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.1: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.11 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.55.0(eslint@8.57.1)(typescript@5.9.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.55.0(eslint@8.57.1)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fecha@4.2.3: {} + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.57.1 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + fn.name@1.1.0: {} + + follow-redirects@1.15.11: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formstream@1.5.2: + dependencies: + destroy: 1.2.0 + mime: 2.6.0 + node-hex: 1.0.1 + pause-stream: 0.0.11 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-ready@1.0.0: {} + + get-stream@6.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-escaper@2.0.2: {} + + human-signals@2.1.0: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-class-hotfix@0.0.6: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-type-of@1.4.0: + dependencies: + core-util-is: 1.0.3 + is-class-hotfix: 0.0.6 + isstream: 0.1.2 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isstream@0.1.2: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@22.19.11): + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@22.19.11) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@22.19.11) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@22.19.11): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.11 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 22.19.11 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.11 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + chalk: 4.1.2 + cjs-module-lexer: 1.4.3 + collect-v8-coverage: 1.0.3 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.11 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.19.11 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@22.19.11): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@22.19.11) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + joycon@3.1.1: {} + + js-base64@2.6.4: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jstoxml@2.2.9: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kuler@2.0.0: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.23: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-hex@1.0.1: {} + + node-int64@0.4.0: {} + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + os-name@1.0.3: + dependencies: + osx-release: 1.1.0 + win-release: 1.1.1 + + osx-release@1.1.0: + dependencies: + minimist: 1.2.8 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + platform@1.3.6: {} + + possible-typed-array-names@1.1.0: {} + + postcss-load-config@6.0.1: + dependencies: + lilconfig: 3.1.3 + + prelude-ls@1.2.1: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process-nextick-args@2.0.1: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proxy-from-env@1.1.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + qs@6.14.2: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + react-is@18.3.1: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sax@1.4.4: {} + + sdk-base@2.0.1: + dependencies: + get-ready: 1.0.0 + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + sprintf-js@1.0.3: {} + + stack-trace@0.0.10: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + statuses@1.5.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + stream-http@2.8.2: + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 2.3.8 + to-arraybuffer: 1.0.1 + xtend: 4.0.2 + + stream-wormhole@1.1.0: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.1 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + 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.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-hex@1.0.0: {} + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + through@2.3.8: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tmpl@1.0.5: {} + + to-arraybuffer@1.0.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + triple-beam@1.4.1: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-case-convert@2.1.0: {} + + ts-interface-checker@0.1.13: {} + + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.11))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.11) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.4 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + esbuild: 0.27.3 + jest-util: 29.7.0 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsup@8.5.1(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + 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.57.1 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + uglify-js@3.19.3: + optional: true + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unescape@1.0.1: + dependencies: + extend-shallow: 2.0.1 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + urllib@2.44.0: + dependencies: + any-promise: 1.3.0 + content-type: 1.0.5 + default-user-agent: 1.0.0 + digest-header: 1.1.0 + ee-first: 1.1.1 + formstream: 1.5.2 + humanize-ms: 1.2.1 + iconv-lite: 0.6.3 + pump: 3.0.3 + qs: 6.14.2 + statuses: 1.5.0 + utility: 1.18.0 + + util-deprecate@1.0.2: {} + + utility@1.18.0: + dependencies: + copy-to: 2.0.1 + escape-html: 1.0.3 + mkdirp: 0.5.6 + mz: 2.7.0 + unescape: 1.0.1 + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + win-release@1.1.1: + dependencies: + semver: 5.7.2 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + xml2js@0.6.2: + dependencies: + sax: 1.4.4 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + 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 + + yocto-queue@0.1.0: {} + + zod@3.25.76: {} diff --git a/rock/ts-sdk/src/common/constants.test.ts b/rock/ts-sdk/src/common/constants.test.ts new file mode 100644 index 000000000..f7cd69b53 --- /dev/null +++ b/rock/ts-sdk/src/common/constants.test.ts @@ -0,0 +1,28 @@ +/** + * Tests for Constants + */ + +import * as constantsModule from './constants.js'; +import { RunMode, PID_PREFIX, PID_SUFFIX } from './constants.js'; + +describe('Constants class removal', () => { + test('Constants class should NOT be exported (dead code removal)', () => { + // Constants class was deprecated with empty string values and never used + // It should be removed from the codebase + expect('Constants' in constantsModule).toBe(false); + }); +}); + +describe('RunMode', () => { + test('should have correct values', () => { + expect(RunMode.NORMAL).toBe('normal'); + expect(RunMode.NOHUP).toBe('nohup'); + }); +}); + +describe('PID markers', () => { + test('should have correct prefix and suffix', () => { + expect(PID_PREFIX).toBe('__ROCK_PID_START__'); + expect(PID_SUFFIX).toBe('__ROCK_PID_END__'); + }); +}); diff --git a/rock/ts-sdk/src/common/constants.ts b/rock/ts-sdk/src/common/constants.ts new file mode 100644 index 000000000..8a776402c --- /dev/null +++ b/rock/ts-sdk/src/common/constants.ts @@ -0,0 +1,22 @@ +/** + * Common constants + */ + +/** + * Run mode type + */ +export type RunModeType = 'normal' | 'nohup'; + +/** + * Run mode enum + */ +export const RunMode = { + NORMAL: 'normal' as const, + NOHUP: 'nohup' as const, +}; + +/** + * PID prefix and suffix for nohup output parsing + */ +export const PID_PREFIX = '__ROCK_PID_START__'; +export const PID_SUFFIX = '__ROCK_PID_END__'; diff --git a/rock/ts-sdk/src/common/exceptions.test.ts b/rock/ts-sdk/src/common/exceptions.test.ts new file mode 100644 index 000000000..33473aba1 --- /dev/null +++ b/rock/ts-sdk/src/common/exceptions.test.ts @@ -0,0 +1,118 @@ +/** + * Tests for Exceptions + */ + +import { + RockException, + InvalidParameterRockException, + BadRequestRockError, + InternalServerRockError, + CommandRockError, + raiseForCode, + fromRockException, +} from './exceptions.js'; +import { Codes } from '../types/codes.js'; + +describe('RockException', () => { + test('should create exception with message', () => { + const error = new RockException('Test error'); + expect(error.message).toBe('Test error'); + expect(error.name).toBe('RockException'); + expect(error.code).toBeNull(); + }); + + test('should create exception with code', () => { + const error = new RockException('Test error', Codes.BAD_REQUEST); + expect(error.code).toBe(Codes.BAD_REQUEST); + }); +}); + +describe('InvalidParameterRockException', () => { + test('should create deprecated exception', () => { + const error = new InvalidParameterRockException('Invalid param'); + expect(error.message).toBe('Invalid param'); + expect(error.name).toBe('InvalidParameterRockException'); + }); +}); + +describe('BadRequestRockError', () => { + test('should create with default code', () => { + const error = new BadRequestRockError('Bad request'); + expect(error.message).toBe('Bad request'); + expect(error.name).toBe('BadRequestRockError'); + expect(error.code).toBe(Codes.BAD_REQUEST); + }); + + test('should create with custom code', () => { + const error = new BadRequestRockError('Bad request', 4001 as Codes); + expect(error.code).toBe(4001); + }); +}); + +describe('InternalServerRockError', () => { + test('should create with default code', () => { + const error = new InternalServerRockError('Server error'); + expect(error.message).toBe('Server error'); + expect(error.name).toBe('InternalServerRockError'); + expect(error.code).toBe(Codes.INTERNAL_SERVER_ERROR); + }); +}); + +describe('CommandRockError', () => { + test('should create with default code', () => { + const error = new CommandRockError('Command failed'); + expect(error.message).toBe('Command failed'); + expect(error.name).toBe('CommandRockError'); + expect(error.code).toBe(Codes.COMMAND_ERROR); + }); +}); + +describe('raiseForCode', () => { + test('should not throw for null code', () => { + expect(() => raiseForCode(null, 'test')).not.toThrow(); + }); + + test('should not throw for undefined code', () => { + expect(() => raiseForCode(undefined, 'test')).not.toThrow(); + }); + + test('should not throw for success code', () => { + expect(() => raiseForCode(Codes.OK, 'test')).not.toThrow(); + }); + + test('should throw BadRequestRockError for 4xxx code', () => { + expect(() => raiseForCode(Codes.BAD_REQUEST, 'test')).toThrow(BadRequestRockError); + }); + + test('should throw InternalServerRockError for 5xxx code', () => { + expect(() => raiseForCode(Codes.INTERNAL_SERVER_ERROR, 'test')).toThrow(InternalServerRockError); + }); + + test('should throw CommandRockError for 6xxx code', () => { + expect(() => raiseForCode(Codes.COMMAND_ERROR, 'test')).toThrow(CommandRockError); + }); + + test('should throw RockException for unknown error code', () => { + expect(() => raiseForCode(7000 as Codes, 'test')).toThrow(RockException); + }); +}); + +describe('fromRockException', () => { + test('should convert exception to response', () => { + const error = new BadRequestRockError('Test error'); + const response = fromRockException(error); + + expect(response.code).toBe(Codes.BAD_REQUEST); + expect(response.exitCode).toBe(1); + expect(response.failureReason).toBe('Test error'); + }); + + test('should handle exception without code', () => { + const error = new RockException('Test error'); + const response = fromRockException(error); + + expect(response.code).toBeUndefined(); + expect(response.exitCode).toBe(1); + expect(response.failureReason).toBe('Test error'); + }); +}); diff --git a/rock/ts-sdk/src/common/exceptions.ts b/rock/ts-sdk/src/common/exceptions.ts new file mode 100644 index 000000000..8bfd9fa8e --- /dev/null +++ b/rock/ts-sdk/src/common/exceptions.ts @@ -0,0 +1,113 @@ +/** + * ROCK Exception classes + */ + +import { Codes } from '../types/codes.js'; +import { SandboxResponse } from '../types/responses.js'; + +/** + * Base ROCK exception + */ +export class RockException extends Error { + protected _code: Codes | null = null; + + constructor(message: string, code?: Codes) { + super(message); + this.name = 'RockException'; + this._code = code ?? null; + } + + get code(): Codes | null { + return this._code; + } +} + +/** + * Invalid parameter exception (deprecated) + * @deprecated Use BadRequestRockError instead + */ +export class InvalidParameterRockException extends RockException { + constructor(message: string) { + super(message); + this.name = 'InvalidParameterRockException'; + } +} + +/** + * Bad request error (4xxx) + */ +export class BadRequestRockError extends RockException { + constructor(message: string, code: Codes = Codes.BAD_REQUEST) { + super(message, code); + this.name = 'BadRequestRockError'; + } +} + +/** + * Internal server error (5xxx) + */ +export class InternalServerRockError extends RockException { + constructor(message: string, code: Codes = Codes.INTERNAL_SERVER_ERROR) { + super(message, code); + this.name = 'InternalServerRockError'; + } +} + +/** + * Command execution error (6xxx) + */ +export class CommandRockError extends RockException { + constructor(message: string, code: Codes = Codes.COMMAND_ERROR) { + super(message, code); + this.name = 'CommandRockError'; + } +} + +/** + * Raise appropriate exception based on status code + */ +export function raiseForCode(code: Codes | null | undefined, message: string): void { + if (code === null || code === undefined || isSuccessCode(code)) { + return; + } + + if (isClientErrorCode(code)) { + throw new BadRequestRockError(message, code); + } + if (isServerErrorCode(code)) { + throw new InternalServerRockError(message, code); + } + if (isCommandErrorCode(code)) { + throw new CommandRockError(message, code); + } + + throw new RockException(message, code); +} + +/** + * Convert RockException to SandboxResponse + */ +export function fromRockException(e: RockException): SandboxResponse { + return { + code: e.code ?? undefined, + exitCode: 1, + failureReason: e.message, + }; +} + +// Helper functions for code checking +function isSuccessCode(code: Codes): boolean { + return code >= 2000 && code <= 2999; +} + +function isClientErrorCode(code: Codes): boolean { + return code >= 4000 && code <= 4999; +} + +function isServerErrorCode(code: Codes): boolean { + return code >= 5000 && code <= 5999; +} + +function isCommandErrorCode(code: Codes): boolean { + return code >= 6000 && code <= 6999; +} diff --git a/rock/ts-sdk/src/common/index.ts b/rock/ts-sdk/src/common/index.ts new file mode 100644 index 000000000..7d1d50cdf --- /dev/null +++ b/rock/ts-sdk/src/common/index.ts @@ -0,0 +1,6 @@ +/** + * Common module - Constants and Exceptions + */ + +export * from './constants.js'; +export * from './exceptions.js'; diff --git a/rock/ts-sdk/src/env_vars.test.ts b/rock/ts-sdk/src/env_vars.test.ts new file mode 100644 index 000000000..be461b07b --- /dev/null +++ b/rock/ts-sdk/src/env_vars.test.ts @@ -0,0 +1,79 @@ +/** + * Tests for environment variables configuration + */ + +import { envVars } from './env_vars.js'; + +describe('envVars', () => { + describe('PyPI configuration', () => { + test('ROCK_PIP_INDEX_URL should default to public PyPI mirror for open-source SDK', () => { + expect(envVars.ROCK_PIP_INDEX_URL).toBe('https://pypi.org/simple/'); + }); + }); + + describe('Python install command URLs', () => { + test('V31114 install command URL should contain releases/download/ path', () => { + const cmd = envVars.ROCK_RTENV_PYTHON_V31114_INSTALL_CMD; + expect(cmd).toContain('releases/download/'); + }); + + test('V31212 install command URL should contain releases/download/ path', () => { + const cmd = envVars.ROCK_RTENV_PYTHON_V31212_INSTALL_CMD; + expect(cmd).toContain('releases/download/'); + }); + }); + + describe('Sandbox default configuration', () => { + test('ROCK_DEFAULT_IMAGE should have default value', () => { + expect(envVars.ROCK_DEFAULT_IMAGE).toBe('python:3.11'); + }); + + test('ROCK_DEFAULT_MEMORY should have default value', () => { + expect(envVars.ROCK_DEFAULT_MEMORY).toBe('8g'); + }); + + test('ROCK_DEFAULT_CPUS should have default value', () => { + expect(envVars.ROCK_DEFAULT_CPUS).toBe(2); + }); + + test('ROCK_DEFAULT_CLUSTER should have default value', () => { + expect(envVars.ROCK_DEFAULT_CLUSTER).toBe('zb'); + }); + + test('ROCK_DEFAULT_AUTO_CLEAR_SECONDS should have default value', () => { + expect(envVars.ROCK_DEFAULT_AUTO_CLEAR_SECONDS).toBe(300); + }); + }); + + describe('SandboxGroup default configuration', () => { + test('ROCK_DEFAULT_GROUP_SIZE should have default value', () => { + expect(envVars.ROCK_DEFAULT_GROUP_SIZE).toBe(2); + }); + + test('ROCK_DEFAULT_START_CONCURRENCY should have default value', () => { + expect(envVars.ROCK_DEFAULT_START_CONCURRENCY).toBe(2); + }); + + test('ROCK_DEFAULT_START_RETRY_TIMES should have default value', () => { + expect(envVars.ROCK_DEFAULT_START_RETRY_TIMES).toBe(3); + }); + }); + + describe('Client timeout defaults', () => { + test('ROCK_DEFAULT_ARUN_TIMEOUT should have default value', () => { + expect(envVars.ROCK_DEFAULT_ARUN_TIMEOUT).toBe(300); + }); + + test('ROCK_DEFAULT_NOHUP_WAIT_TIMEOUT should have default value', () => { + expect(envVars.ROCK_DEFAULT_NOHUP_WAIT_TIMEOUT).toBe(300); + }); + + test('ROCK_DEFAULT_NOHUP_WAIT_INTERVAL should have default value', () => { + expect(envVars.ROCK_DEFAULT_NOHUP_WAIT_INTERVAL).toBe(10); + }); + + test('ROCK_DEFAULT_STATUS_CHECK_INTERVAL should have default value', () => { + expect(envVars.ROCK_DEFAULT_STATUS_CHECK_INTERVAL).toBe(3); + }); + }); +}); diff --git a/rock/ts-sdk/src/env_vars.ts b/rock/ts-sdk/src/env_vars.ts new file mode 100644 index 000000000..ebfc30e2b --- /dev/null +++ b/rock/ts-sdk/src/env_vars.ts @@ -0,0 +1,277 @@ +/** + * Environment variables configuration + */ + +import { getEnv, isEnvSet } from './utils/system.js'; +import { homedir } from 'os'; +import { join } from 'path'; + +/** + * Environment variable definitions + */ +export const envVars = { + // Logging + get ROCK_LOGGING_PATH(): string | undefined { + return getEnv('ROCK_LOGGING_PATH'); + }, + + get ROCK_LOGGING_FILE_NAME(): string { + return getEnv('ROCK_LOGGING_FILE_NAME', 'rocklet.log')!; + }, + + get ROCK_LOGGING_LEVEL(): string { + return getEnv('ROCK_LOGGING_LEVEL', 'INFO')!; + }, + + // Service + get ROCK_SERVICE_STATUS_DIR(): string { + return getEnv('ROCK_SERVICE_STATUS_DIR', '/data/service_status')!; + }, + + get ROCK_SCHEDULER_STATUS_DIR(): string { + return getEnv('ROCK_SCHEDULER_STATUS_DIR', '/data/scheduler_status')!; + }, + + // Config + get ROCK_CONFIG(): string | undefined { + return getEnv('ROCK_CONFIG'); + }, + + get ROCK_CONFIG_DIR_NAME(): string { + return getEnv('ROCK_CONFIG_DIR_NAME', 'rock-conf')!; + }, + + // Base URLs + get ROCK_BASE_URL(): string { + return getEnv('ROCK_BASE_URL', 'http://localhost:8080')!; + }, + + get ROCK_WORKER_ROCKLET_PORT(): number | undefined { + const val = getEnv('ROCK_WORKER_ROCKLET_PORT'); + return val ? parseInt(val, 10) : undefined; + }, + + get ROCK_SANDBOX_STARTUP_TIMEOUT_SECONDS(): number { + return parseInt(getEnv('ROCK_SANDBOX_STARTUP_TIMEOUT_SECONDS', '180')!, 10); + }, + + get ROCK_CODE_SANDBOX_BASE_URL(): string { + return getEnv('ROCK_CODE_SANDBOX_BASE_URL', '')!; + }, + + // EnvHub + get ROCK_ENVHUB_BASE_URL(): string { + return getEnv('ROCK_ENVHUB_BASE_URL', 'http://localhost:8081')!; + }, + + get ROCK_ENVHUB_DEFAULT_DOCKER_IMAGE(): string { + return getEnv('ROCK_ENVHUB_DEFAULT_DOCKER_IMAGE', 'python:3.11')!; + }, + + get ROCK_ENVHUB_DB_URL(): string { + return getEnv( + 'ROCK_ENVHUB_DB_URL', + `sqlite:///${join(homedir(), '.rock', 'rock_envs.db')}` + )!; + }, + + // Auto clear + get ROCK_DEFAULT_AUTO_CLEAR_TIME_MINUTES(): number { + return parseInt(getEnv('ROCK_DEFAULT_AUTO_CLEAR_TIME_MINUTES', '360')!, 10); + }, + + // Ray + get ROCK_RAY_NAMESPACE(): string { + return getEnv('ROCK_RAY_NAMESPACE', 'xrl-sandbox')!; + }, + + get ROCK_SANDBOX_EXPIRE_TIME_KEY(): string { + return getEnv('ROCK_SANDBOX_EXPIRE_TIME_KEY', 'expire_time')!; + }, + + get ROCK_SANDBOX_AUTO_CLEAR_TIME_KEY(): string { + return getEnv('ROCK_SANDBOX_AUTO_CLEAR_TIME_KEY', 'auto_clear_time')!; + }, + + // Timezone + get ROCK_TIME_ZONE(): string { + return getEnv('ROCK_TIME_ZONE', 'Asia/Shanghai')!; + }, + + // OSS + get ROCK_OSS_ENABLE(): boolean { + return getEnv('ROCK_OSS_ENABLE', 'false')?.toLowerCase() === 'true'; + }, + + get ROCK_OSS_BUCKET_ENDPOINT(): string | undefined { + return getEnv('ROCK_OSS_BUCKET_ENDPOINT'); + }, + + get ROCK_OSS_BUCKET_NAME(): string | undefined { + return getEnv('ROCK_OSS_BUCKET_NAME'); + }, + + get ROCK_OSS_BUCKET_REGION(): string | undefined { + return getEnv('ROCK_OSS_BUCKET_REGION'); + }, + + // Pip + get ROCK_PIP_INDEX_URL(): string { + return getEnv('ROCK_PIP_INDEX_URL', 'https://pypi.org/simple/')!; + }, + + // Monitor + get ROCK_MONITOR_ENABLE(): boolean { + return getEnv('ROCK_MONITOR_ENABLE', 'false')?.toLowerCase() === 'true'; + }, + + // Project + get ROCK_PROJECT_ROOT(): string { + return getEnv('ROCK_PROJECT_ROOT', process.cwd())!; + }, + + get ROCK_WORKER_ENV_TYPE(): string { + return getEnv('ROCK_WORKER_ENV_TYPE', 'local')!; + }, + + get ROCK_PYTHON_ENV_PATH(): string { + return getEnv('ROCK_PYTHON_ENV_PATH', process.cwd())!; + }, + + // Admin + get ROCK_ADMIN_ENV(): string { + return getEnv('ROCK_ADMIN_ENV', 'dev')!; + }, + + get ROCK_ADMIN_ROLE(): string { + return getEnv('ROCK_ADMIN_ROLE', 'write')!; + }, + + // CLI + get ROCK_CLI_LOAD_PATHS(): string { + return getEnv('ROCK_CLI_LOAD_PATHS', '')!; + }, + + get ROCK_CLI_DEFAULT_CONFIG_PATH(): string { + return getEnv('ROCK_CLI_DEFAULT_CONFIG_PATH', join(homedir(), '.rock', 'config.ini'))!; + }, + + // Model Service + get ROCK_MODEL_SERVICE_DATA_DIR(): string { + return getEnv('ROCK_MODEL_SERVICE_DATA_DIR', '/data/logs')!; + }, + + get ROCK_MODEL_SERVICE_TRAJ_APPEND_MODE(): boolean { + return getEnv('ROCK_MODEL_SERVICE_TRAJ_APPEND_MODE', 'false')?.toLowerCase() === 'true'; + }, + + // RuntimeEnv + get ROCK_RTENV_PYTHON_V31114_INSTALL_CMD(): string { + return getEnv( + 'ROCK_RTENV_PYTHON_V31114_INSTALL_CMD', + '[ -f cpython31114.tar.gz ] && rm cpython31114.tar.gz; [ -d python ] && rm -rf python; wget -q -O cpython31114.tar.gz https://github.com/astral-sh/python-build-standalone/releases/download/20251120/cpython-3.11.14+20251120-x86_64-unknown-linux-gnu-install_only.tar.gz && tar -xzf cpython31114.tar.gz && mv python runtime-env' + )!; + }, + + get ROCK_RTENV_PYTHON_V31212_INSTALL_CMD(): string { + return getEnv( + 'ROCK_RTENV_PYTHON_V31212_INSTALL_CMD', + '[ -f cpython-3.12.12.tar.gz ] && rm cpython-3.12.12.tar.gz; [ -d python ] && rm -rf python; wget -q -O cpython-3.12.12.tar.gz https://github.com/astral-sh/python-build-standalone/releases/download/20251217/cpython-3.12.12+20251217-x86_64-unknown-linux-gnu-install_only.tar.gz && tar -xzf cpython-3.12.12.tar.gz && mv python runtime-env' + )!; + }, + + get ROCK_RTENV_NODE_V22180_INSTALL_CMD(): string { + return getEnv( + 'ROCK_RTENV_NODE_V22180_INSTALL_CMD', + '[ -f node.tar.xz ] && rm node.tar.xz; [ -d node ] && rm -rf node; wget -q -O node.tar.xz --tries=10 --waitretry=2 https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.xz && tar -xf node.tar.xz && mv node-v22.18.0-linux-x64 runtime-env' + )!; + }, + + // Agent + get ROCK_AGENT_PRE_INIT_BASH_CMD_LIST(): Array<{ command: string; timeoutSeconds: number }> { + const val = getEnv('ROCK_AGENT_PRE_INIT_BASH_CMD_LIST', '[]'); + try { + return JSON.parse(val!); + } catch { + return []; + } + }, + + get ROCK_AGENT_IFLOW_CLI_INSTALL_CMD(): string { + return getEnv('ROCK_AGENT_IFLOW_CLI_INSTALL_CMD', 'npm i -g @iflow-ai/iflow-cli@latest')!; + }, + + get ROCK_MODEL_SERVICE_INSTALL_CMD(): string { + return getEnv('ROCK_MODEL_SERVICE_INSTALL_CMD', 'pip install rl_rock[model-service]')!; + }, + + // Doccuum + get ROCK_DOCUUM_INSTALL_URL(): string { + return getEnv( + 'ROCK_DOCUUM_INSTALL_URL', + 'https://raw.githubusercontent.com/stepchowfun/docuum/main/install.sh' + )!; + }, + + // ========== Sandbox Defaults ========== + // Sandbox configuration defaults - allow users to override via environment variables + + get ROCK_DEFAULT_IMAGE(): string { + return getEnv('ROCK_DEFAULT_IMAGE', 'python:3.11')!; + }, + + get ROCK_DEFAULT_MEMORY(): string { + return getEnv('ROCK_DEFAULT_MEMORY', '8g')!; + }, + + get ROCK_DEFAULT_CPUS(): number { + return parseFloat(getEnv('ROCK_DEFAULT_CPUS', '2')!); + }, + + get ROCK_DEFAULT_CLUSTER(): string { + return getEnv('ROCK_DEFAULT_CLUSTER', 'zb')!; + }, + + get ROCK_DEFAULT_AUTO_CLEAR_SECONDS(): number { + return parseInt(getEnv('ROCK_DEFAULT_AUTO_CLEAR_SECONDS', '300')!, 10); + }, + + // ========== SandboxGroup Defaults ========== + + get ROCK_DEFAULT_GROUP_SIZE(): number { + return parseInt(getEnv('ROCK_DEFAULT_GROUP_SIZE', '2')!, 10); + }, + + get ROCK_DEFAULT_START_CONCURRENCY(): number { + return parseInt(getEnv('ROCK_DEFAULT_START_CONCURRENCY', '2')!, 10); + }, + + get ROCK_DEFAULT_START_RETRY_TIMES(): number { + return parseInt(getEnv('ROCK_DEFAULT_START_RETRY_TIMES', '3')!, 10); + }, + + // ========== Client Timeouts (in seconds) ========== + + get ROCK_DEFAULT_ARUN_TIMEOUT(): number { + return parseInt(getEnv('ROCK_DEFAULT_ARUN_TIMEOUT', '300')!, 10); + }, + + get ROCK_DEFAULT_NOHUP_WAIT_TIMEOUT(): number { + return parseInt(getEnv('ROCK_DEFAULT_NOHUP_WAIT_TIMEOUT', '300')!, 10); + }, + + get ROCK_DEFAULT_NOHUP_WAIT_INTERVAL(): number { + return parseInt(getEnv('ROCK_DEFAULT_NOHUP_WAIT_INTERVAL', '10')!, 10); + }, + + get ROCK_DEFAULT_STATUS_CHECK_INTERVAL(): number { + return parseInt(getEnv('ROCK_DEFAULT_STATUS_CHECK_INTERVAL', '3')!, 10); + }, +}; + +/** + * Check if an environment variable is explicitly set + */ +export function isSet(name: string): boolean { + return isEnvSet(name); +} diff --git a/rock/ts-sdk/src/envhub/client.ts b/rock/ts-sdk/src/envhub/client.ts new file mode 100644 index 000000000..54bdb264c --- /dev/null +++ b/rock/ts-sdk/src/envhub/client.ts @@ -0,0 +1,152 @@ +/** + * EnvHub client for communicating with EnvHub server + */ + +import { HttpUtils } from '../utils/http.js'; +import { + EnvHubClientConfig, + EnvHubClientConfigSchema, + RockEnvInfo, + createRockEnvInfo, +} from './schema.js'; + +/** + * EnvHub error exception + */ +export class EnvHubError extends Error { + constructor(message: string) { + super(message); + this.name = 'EnvHubError'; + } +} + +/** + * EnvHub client for communicating with EnvHub server + */ +export class EnvHubClient { + private config: EnvHubClientConfig; + private baseUrl: string; + private headers: Record; + + constructor(config?: Partial) { + this.config = EnvHubClientConfigSchema.parse(config ?? {}); + this.baseUrl = this.config.baseUrl; + this.headers = { 'Content-Type': 'application/json' }; + } + + /** + * Register or update an environment + */ + async register(options: { + envName: string; + image: string; + owner?: string; + description?: string; + tags?: string[]; + extraSpec?: Record; + }): Promise { + const url = `${this.baseUrl}/env/register`; + // Use camelCase - HTTP layer will convert to snake_case + const payload = { + envName: options.envName, + image: options.image, + owner: options.owner ?? '', + description: options.description ?? '', + tags: options.tags ?? [], + extraSpec: options.extraSpec, + }; + + try { + const response = await HttpUtils.post>( + url, + this.headers, + payload + ); + // Response is already camelCase (converted by HTTP layer) + return createRockEnvInfo(response.result!); + } catch (e) { + throw new EnvHubError(`Failed to register environment: ${e}`); + } + } + + /** + * Get environment by name + */ + async getEnv(envName: string): Promise { + const url = `${this.baseUrl}/env/get`; + const payload = { envName }; + + try { + const response = await HttpUtils.post>( + url, + this.headers, + payload + ); + return createRockEnvInfo(response.result!); + } catch (e) { + throw new EnvHubError(`Failed to get environment ${envName}: ${e}`); + } + } + + /** + * List environments + */ + async listEnvs(options?: { + owner?: string; + tags?: string[]; + }): Promise { + const url = `${this.baseUrl}/env/list`; + const payload = { + owner: options?.owner, + tags: options?.tags, + }; + + try { + const response = await HttpUtils.post<{ envs: Record[] }>( + url, + this.headers, + payload + ); + const envsData = response.result?.envs ?? []; + return envsData.map((envData) => createRockEnvInfo(envData)); + } catch (e) { + throw new EnvHubError(`Failed to list environments: ${e}`); + } + } + + /** + * Delete environment + */ + async deleteEnv(envName: string): Promise { + const url = `${this.baseUrl}/env/delete`; + const payload = { envName }; + + try { + await HttpUtils.post(url, this.headers, payload); + return true; + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + if (errorMessage.includes('404')) { + return false; + } + throw new EnvHubError(`Failed to delete environment ${envName}: ${e}`); + } + } + + /** + * Health check + */ + async healthCheck(): Promise> { + const url = `${this.baseUrl}/health`; + + try { + const response = await HttpUtils.get>( + url, + this.headers + ); + return response.result ?? {}; + } catch (e) { + throw new EnvHubError(`Failed to health check: ${e}`); + } + } +} \ No newline at end of file diff --git a/rock/ts-sdk/src/envhub/index.ts b/rock/ts-sdk/src/envhub/index.ts new file mode 100644 index 000000000..b3f4ead45 --- /dev/null +++ b/rock/ts-sdk/src/envhub/index.ts @@ -0,0 +1,6 @@ +/** + * EnvHub module - Client and schemas + */ + +export * from './client.js'; +export * from './schema.js'; diff --git a/rock/ts-sdk/src/envhub/schema.test.ts b/rock/ts-sdk/src/envhub/schema.test.ts new file mode 100644 index 000000000..f6cc69efc --- /dev/null +++ b/rock/ts-sdk/src/envhub/schema.test.ts @@ -0,0 +1,73 @@ +/** + * Tests for EnvHub Schema + */ + +import { + EnvHubClientConfigSchema, + RockEnvInfoSchema, + createRockEnvInfo, +} from './schema.js'; + +describe('EnvHubClientConfigSchema', () => { + test('should use default baseUrl', () => { + const config = EnvHubClientConfigSchema.parse({}); + expect(config.baseUrl).toBe('http://localhost:8081'); + }); + + test('should allow custom baseUrl', () => { + const config = EnvHubClientConfigSchema.parse({ + baseUrl: 'http://custom:9000', + }); + expect(config.baseUrl).toBe('http://custom:9000'); + }); +}); + +describe('RockEnvInfoSchema', () => { + test('should parse valid data', () => { + const env = RockEnvInfoSchema.parse({ + envName: 'test-env', + image: 'python:3.11', + }); + + expect(env.envName).toBe('test-env'); + expect(env.image).toBe('python:3.11'); + expect(env.owner).toBe(''); + expect(env.tags).toEqual([]); + }); + + test('should use defaults for optional fields', () => { + const env = RockEnvInfoSchema.parse({ + envName: 'test', + image: 'node:18', + }); + + expect(env.description).toBe(''); + expect(env.createAt).toBe(''); + expect(env.updateAt).toBe(''); + }); +}); + +describe('createRockEnvInfo', () => { + test('should parse camelCase data (after HTTP layer conversion)', () => { + const env = createRockEnvInfo({ + envName: 'test-env', + image: 'python:3.11', + createAt: '2024-01-01', + extraSpec: { key: 'value' }, + }); + + expect(env.envName).toBe('test-env'); + expect(env.createAt).toBe('2024-01-01'); + expect(env.extraSpec).toEqual({ key: 'value' }); + }); + + test('should handle minimal data', () => { + const env = createRockEnvInfo({ + envName: 'test-env', + image: 'python:3.11', + }); + + expect(env.envName).toBe('test-env'); + expect(env.image).toBe('python:3.11'); + }); +}); \ No newline at end of file diff --git a/rock/ts-sdk/src/envhub/schema.ts b/rock/ts-sdk/src/envhub/schema.ts new file mode 100644 index 000000000..a320d975b --- /dev/null +++ b/rock/ts-sdk/src/envhub/schema.ts @@ -0,0 +1,38 @@ +/** + * EnvHub data model definitions + */ + +import { z } from 'zod'; +import { envVars } from '../env_vars.js'; + +/** + * EnvHub client configuration + */ +export const EnvHubClientConfigSchema = z.object({ + baseUrl: z.string().default(envVars.ROCK_ENVHUB_BASE_URL), +}); + +export type EnvHubClientConfig = z.infer; + +/** + * Rock environment info + */ +export const RockEnvInfoSchema = z.object({ + envName: z.string(), + image: z.string(), + owner: z.string().default(''), + createAt: z.string().default(''), + updateAt: z.string().default(''), + description: z.string().default(''), + tags: z.array(z.string()).default([]), + extraSpec: z.record(z.unknown()).optional(), +}); + +export type RockEnvInfo = z.infer; + +/** + * Create RockEnvInfo from API response (already camelCase after HTTP layer conversion) + */ +export function createRockEnvInfo(data: Record): RockEnvInfo { + return RockEnvInfoSchema.parse(data); +} \ No newline at end of file diff --git a/rock/ts-sdk/src/envs/index.ts b/rock/ts-sdk/src/envs/index.ts new file mode 100644 index 000000000..c10579af4 --- /dev/null +++ b/rock/ts-sdk/src/envs/index.ts @@ -0,0 +1,6 @@ +/** + * Envs module - Environment management + */ + +export * from './registration.js'; +export * from './rock_env.js'; diff --git a/rock/ts-sdk/src/envs/registration.ts b/rock/ts-sdk/src/envs/registration.ts new file mode 100644 index 000000000..1b12a9566 --- /dev/null +++ b/rock/ts-sdk/src/envs/registration.ts @@ -0,0 +1,19 @@ +/** + * Environment factory function + */ + +import { RockEnv } from './rock_env.js'; + +/** + * Create a Rock environment instance + * + * @param envId - Environment ID + * @param options - Environment options + * @returns Promise resolving to RockEnv instance + */ +export async function make( + envId: string, + options?: Record +): Promise { + return RockEnv.create({ envId, ...options }); +} \ No newline at end of file diff --git a/rock/ts-sdk/src/envs/rock_env.test.ts b/rock/ts-sdk/src/envs/rock_env.test.ts new file mode 100644 index 000000000..2b1d01325 --- /dev/null +++ b/rock/ts-sdk/src/envs/rock_env.test.ts @@ -0,0 +1,305 @@ +/** + * Tests for RockEnv - TDD implementation + * + * These tests verify the fix for PR #492 reviewer comments: + * 1. sandboxId should be initialized by calling the "make" API + * 2. Should use HttpUtils instead of raw AxiosInstance for consistent camelCase/snake_case conversion + */ + +import { RockEnv } from './rock_env.js'; +import { HttpUtils } from '../utils/http.js'; + +// Mock HttpUtils +jest.mock('../utils/http.js'); +const mockedHttpUtils = HttpUtils as jest.Mocked; + +describe('RockEnv', () => { + const mockBaseUrl = 'http://test-rock-api.example.com'; + + beforeEach(() => { + jest.clearAllMocks(); + process.env.ROCK_BASE_URL = mockBaseUrl; + }); + + afterEach(() => { + delete process.env.ROCK_BASE_URL; + }); + + describe('initializeEnvironment', () => { + test('should call "make" API and set sandboxId from response', async () => { + // Arrange + const expectedSandboxId = 'test-sandbox-123'; + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId: expectedSandboxId }, + headers: {}, + }); + + // Act + const env = await RockEnv.create({ envId: 'test-env' }); + + // Assert + expect(mockedHttpUtils.post).toHaveBeenCalledWith( + `${mockBaseUrl}/apis/v1/envs/gem/make`, + { 'Content-Type': 'application/json' }, + { envId: 'test-env' } + ); + expect(env.getSandboxId()).toBe(expectedSandboxId); + }); + + test('should throw error when make API does not return sandboxId', async () => { + // Arrange + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: {}, + headers: {}, + }); + + // Act & Assert + await expect(RockEnv.create({ envId: 'test-env' })).rejects.toThrow( + 'Failed to get environment instance ID' + ); + }); + + test('should throw error when make API fails', async () => { + // Arrange + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Failed', + error: 'Environment not found', + headers: {}, + }); + + // Act & Assert + await expect(RockEnv.create({ envId: 'invalid-env' })).rejects.toThrow(); + }); + }); + + describe('step', () => { + test('should use HttpUtils.post and send sandboxId', async () => { + // Arrange + const sandboxId = 'test-sandbox-456'; + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId }, + headers: {}, + }); + + const env = await RockEnv.create({ envId: 'test-env' }); + + // Mock step response + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { + observation: 'state-1', + reward: 1.0, + terminated: false, + truncated: false, + info: { steps: 1 }, + }, + headers: {}, + }); + + // Act + const result = await env.step('action-1'); + + // Assert - verify HttpUtils.post was called with correct params + expect(mockedHttpUtils.post).toHaveBeenLastCalledWith( + `${mockBaseUrl}/apis/v1/envs/gem/step`, + { 'Content-Type': 'application/json' }, + { sandboxId, action: 'action-1' } + ); + expect(result).toEqual(['state-1', 1.0, false, false, { steps: 1 }]); + }); + + test('should throw error when environment is closed', async () => { + // Arrange + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId: 'test-sandbox' }, + headers: {}, + }); + + const env = await RockEnv.create({ envId: 'test-env' }); + await env.close(); + + // Act & Assert + await expect(env.step('action')).rejects.toThrow('Environment is closed'); + }); + }); + + describe('reset', () => { + test('should use HttpUtils.post and send sandboxId', async () => { + // Arrange + const sandboxId = 'test-sandbox-789'; + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId }, + headers: {}, + }); + + const env = await RockEnv.create({ envId: 'test-env' }); + + // Mock reset response + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { + observation: 'initial-state', + info: { episode: 1 }, + }, + headers: {}, + }); + + // Act + const result = await env.reset(42); + + // Assert - verify HttpUtils.post was called with correct params + expect(mockedHttpUtils.post).toHaveBeenLastCalledWith( + `${mockBaseUrl}/apis/v1/envs/gem/reset`, + { 'Content-Type': 'application/json' }, + { sandboxId, seed: 42 } + ); + expect(result).toEqual(['initial-state', { episode: 1 }]); + }); + + test('should work without seed parameter', async () => { + // Arrange + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId: 'test-sandbox' }, + headers: {}, + }); + + const env = await RockEnv.create({ envId: 'test-env' }); + + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { + observation: 'initial-state', + info: {}, + }, + headers: {}, + }); + + // Act + await env.reset(); + + // Assert - seed should not be in params + const calls = mockedHttpUtils.post.mock.calls; + const lastCall = calls[calls.length - 1]; + expect(lastCall?.[2]).toEqual({ sandboxId: 'test-sandbox' }); + }); + }); + + describe('close', () => { + test('should use HttpUtils.post and send sandboxId', async () => { + // Arrange + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId: 'test-sandbox' }, + headers: {}, + }); + + const env = await RockEnv.create({ envId: 'test-env' }); + + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: {}, + headers: {}, + }); + + // Act + await env.close(); + + // Assert + expect(mockedHttpUtils.post).toHaveBeenLastCalledWith( + `${mockBaseUrl}/apis/v1/envs/gem/close`, + { 'Content-Type': 'application/json' }, + { sandboxId: 'test-sandbox' } + ); + }); + + test('should be idempotent - calling close multiple times should not error', async () => { + // Arrange + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId: 'test-sandbox' }, + headers: {}, + }); + + const env = await RockEnv.create({ envId: 'test-env' }); + + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: {}, + headers: {}, + }); + + // Act + await env.close(); + await env.close(); // Second call should be a no-op + + // Assert - close API should only be called once + const closeCalls = mockedHttpUtils.post.mock.calls.filter( + (call) => typeof call[0] === 'string' && call[0].includes('/close') + ); + expect(closeCalls).toHaveLength(1); + }); + + test('should not call close API when sandboxId is null', async () => { + // Arrange - create env that failed to get sandboxId + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: {}, + headers: {}, + }); + + // Act - create will fail, but let's test close behavior + try { + await RockEnv.create({ envId: 'test-env' }); + } catch { + // Expected to throw + } + + // Clear mock + mockedHttpUtils.post.mockClear(); + + // This scenario tests the internal guard + }); + }); + + describe('camelCase/snake_case conversion', () => { + test('should send envId (camelCase) as env_id (snake_case) to API', async () => { + // Arrange + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId: 'test-sandbox' }, + headers: {}, + }); + + // Act + await RockEnv.create({ envId: 'my-test-env' }); + + // Assert - HttpUtils should receive camelCase, it handles conversion internally + expect(mockedHttpUtils.post).toHaveBeenCalledWith( + expect.any(String), + expect.any(Object), + { envId: 'my-test-env' } // SDK uses camelCase + ); + }); + + test('should receive sandbox_id (snake_case) from API as sandboxId (camelCase)', async () => { + // Arrange - API returns snake_case, HttpUtils converts to camelCase + mockedHttpUtils.post.mockResolvedValueOnce({ + status: 'Success', + result: { sandboxId: 'converted-to-camelCase' }, + headers: {}, + }); + + // Act + const env = await RockEnv.create({ envId: 'test-env' }); + + // Assert + expect(env.getSandboxId()).toBe('converted-to-camelCase'); + }); + }); +}); diff --git a/rock/ts-sdk/src/envs/rock_env.ts b/rock/ts-sdk/src/envs/rock_env.ts new file mode 100644 index 000000000..8b6ae97c5 --- /dev/null +++ b/rock/ts-sdk/src/envs/rock_env.ts @@ -0,0 +1,204 @@ +/** + * RockEnv - Gym-style environment interface + */ + +import { envVars } from '../env_vars.js'; +import { HttpUtils } from '../utils/http.js'; +import { initLogger } from '../logger.js'; + +const logger = initLogger('rock.envs'); + +/** + * Step result tuple type + */ +export type StepResult = [ + observation: unknown, + reward: number, + terminated: boolean, + truncated: boolean, + info: Record +]; + +/** + * Reset result tuple type + */ +export type ResetResult = [observation: unknown, info: Record]; + +/** + * RockEnv configuration + */ +export interface RockEnvConfig { + envId: string; +} + +/** + * RockEnv - Gym-style environment for ROCK + */ +export class RockEnv { + private readonly envId: string; + private sandboxId: string | null = null; + private isClosed = false; + + private constructor(config: RockEnvConfig) { + this.envId = config.envId; + } + + /** + * Create and initialize a RockEnv instance + * + * @param config - Environment configuration + * @returns Initialized RockEnv instance + */ + static async create(config: RockEnvConfig): Promise { + const env = new RockEnv(config); + try { + await env.initializeEnvironment(); + } catch (e) { + throw new Error(`Failed to initialize environment: ${e}`); + } + return env; + } + + /** + * Get the sandbox ID + */ + getSandboxId(): string | null { + return this.sandboxId; + } + + /** + * Initialize environment instance + */ + private async initializeEnvironment(): Promise { + logger.debug(`Initializing environment: ${this.envId}`); + const response = await HttpUtils.post<{ sandboxId: string }>( + `${envVars.ROCK_BASE_URL}/apis/v1/envs/gem/make`, + { 'Content-Type': 'application/json' }, + { envId: this.envId } + ); + + this.sandboxId = response.result?.sandboxId ?? null; + if (!this.sandboxId) { + throw new Error('Failed to get environment instance ID'); + } + } + + /** + * Execute an action step + * + * @param action - Action ID to execute + * @returns Tuple containing observation, reward, terminated, truncated, info + */ + async step(action: string | number): Promise { + this.ensureNotClosed(); + + const response = await HttpUtils.post<{ + observation: unknown; + reward: number; + terminated: boolean; + truncated: boolean; + info: Record; + }>( + `${envVars.ROCK_BASE_URL}/apis/v1/envs/gem/step`, + { 'Content-Type': 'application/json' }, + { sandboxId: this.sandboxId, action } + ); + + return this.parseStepResult(response.result); + } + + /** + * Reset environment to initial state + * + * @param seed - Optional random seed + * @returns Tuple containing initial observation and info + */ + async reset(seed?: number): Promise { + this.ensureNotClosed(); + + const params: Record = { sandboxId: this.sandboxId }; + if (seed !== undefined) { + params.seed = seed; + } + + const response = await HttpUtils.post<{ + observation: unknown; + info: Record; + }>( + `${envVars.ROCK_BASE_URL}/apis/v1/envs/gem/reset`, + { 'Content-Type': 'application/json' }, + params + ); + + return this.parseResetResult(response.result); + } + + /** + * Close environment and clean up resources + */ + async close(): Promise { + if (this.isClosed || !this.sandboxId) { + return; + } + + try { + await HttpUtils.post( + `${envVars.ROCK_BASE_URL}/apis/v1/envs/gem/close`, + { 'Content-Type': 'application/json' }, + { sandboxId: this.sandboxId } + ); + } catch (e) { + throw new Error(`Failed to close environment: ${e}`); + } finally { + this.isClosed = true; + this.sandboxId = null; + } + } + + /** + * Parse step result from API response + */ + private parseStepResult( + data: + | { + observation: unknown; + reward: number; + terminated: boolean; + truncated: boolean; + info: Record; + } + | undefined + ): StepResult { + if (!data) { + throw new Error('Invalid step result: no data'); + } + return [ + data.observation, + data.reward, + data.terminated, + data.truncated, + data.info, + ]; + } + + /** + * Parse reset result from API response + */ + private parseResetResult( + data: { observation: unknown; info: Record } | undefined + ): ResetResult { + if (!data) { + throw new Error('Invalid reset result: no data'); + } + return [data.observation, data.info]; + } + + /** + * Ensure environment is not closed + */ + private ensureNotClosed(): void { + if (this.isClosed) { + throw new Error('Environment is closed'); + } + } +} \ No newline at end of file diff --git a/rock/ts-sdk/src/index.test.ts b/rock/ts-sdk/src/index.test.ts new file mode 100644 index 000000000..66cbd0726 --- /dev/null +++ b/rock/ts-sdk/src/index.test.ts @@ -0,0 +1,12 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { VERSION } from './index.js'; + +describe('VERSION', () => { + it('should match package.json version', () => { + const packageJsonPath = join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + + expect(VERSION).toBe(packageJson.version); + }); +}); diff --git a/rock/ts-sdk/src/index.ts b/rock/ts-sdk/src/index.ts new file mode 100644 index 000000000..7f2f85df7 --- /dev/null +++ b/rock/ts-sdk/src/index.ts @@ -0,0 +1,72 @@ +/** + * ROCK TypeScript SDK + * Main entry point + */ + +import { readFileSync } from 'fs'; +import { join } from 'path'; + +// Version - read from package.json to ensure consistency +function getVersion(): string { + const packageJsonPath = join(__dirname, '..', 'package.json'); + const content = readFileSync(packageJsonPath, 'utf-8'); + return JSON.parse(content).version; +} + +export const VERSION: string = getVersion(); + +// Types +export * from './types/index.js'; + +// Common - explicit exports to avoid conflicts +export { + RockException, + InvalidParameterRockException, + BadRequestRockError, + InternalServerRockError, + CommandRockError, + raiseForCode, + fromRockException, +} from './common/index.js'; +export { RunMode, RunModeType, PID_PREFIX, PID_SUFFIX } from './common/constants.js'; + +// Utils - explicit exports to avoid conflicts +export { HttpUtils } from './utils/http.js'; +export { retryAsync, sleep, withRetry } from './utils/retry.js'; +export { deprecated, deprecatedClass } from './utils/deprecated.js'; +export { isNode, getEnv, getRequiredEnv, isEnvSet } from './utils/system.js'; + +// EnvHub +export * from './envhub/index.js'; + +// Envs +export * from './envs/index.js'; + +// Sandbox - selective exports +export { Sandbox, SandboxGroup } from './sandbox/client.js'; +export type { RunModeType as SandboxRunModeType } from './common/constants.js'; +export { + SandboxConfigSchema, + SandboxGroupConfigSchema, + createSandboxConfig, + createSandboxGroupConfig, +} from './sandbox/config.js'; +export type { SandboxConfig, SandboxGroupConfig, BaseConfig } from './sandbox/config.js'; +export { Deploy } from './sandbox/deploy.js'; +export { LinuxFileSystem } from './sandbox/file_system.js'; +export { Network, SpeedupType } from './sandbox/network.js'; +export { Process } from './sandbox/process.js'; +export { LinuxRemoteUser } from './sandbox/remote_user.js'; +export { withTimeLogging, arunWithRetry, extractNohupPid as extractNohupPidFromSandbox } from './sandbox/utils.js'; + +// Model +export * from './model/index.js'; + +// RuntimeEnv +export * from './sandbox/runtime_env/index.js'; + +// ModelService (sandbox) +export * from './sandbox/model_service/index.js'; + +// Agent +export * from './sandbox/agent/index.js'; diff --git a/rock/ts-sdk/src/logger.test.ts b/rock/ts-sdk/src/logger.test.ts new file mode 100644 index 000000000..d1c95f9f5 --- /dev/null +++ b/rock/ts-sdk/src/logger.test.ts @@ -0,0 +1,72 @@ +/** + * Logger module tests + */ + +import { initLogger, clearLoggerCache, getLoggerCacheSize } from './logger.js'; + +describe('Logger', () => { + beforeEach(() => { + // Clear cache before each test + clearLoggerCache(); + }); + + describe('initLogger caching', () => { + test('returns same logger instance for same name', () => { + const logger1 = initLogger('test-logger'); + const logger2 = initLogger('test-logger'); + + expect(logger1).toBe(logger2); + }); + + test('returns different logger instances for different names', () => { + const logger1 = initLogger('logger-one'); + const logger2 = initLogger('logger-two'); + + expect(logger1).not.toBe(logger2); + }); + + test('cache size increases when new logger names are used', () => { + expect(getLoggerCacheSize()).toBe(0); + + initLogger('logger-a'); + expect(getLoggerCacheSize()).toBe(1); + + initLogger('logger-b'); + expect(getLoggerCacheSize()).toBe(2); + }); + + test('cache size does not increase when same name is used', () => { + initLogger('same-logger'); + expect(getLoggerCacheSize()).toBe(1); + + initLogger('same-logger'); + expect(getLoggerCacheSize()).toBe(1); + + initLogger('same-logger'); + expect(getLoggerCacheSize()).toBe(1); + }); + }); + + describe('clearLoggerCache', () => { + test('clears all cached loggers', () => { + initLogger('logger-1'); + initLogger('logger-2'); + initLogger('logger-3'); + + expect(getLoggerCacheSize()).toBe(3); + + clearLoggerCache(); + + expect(getLoggerCacheSize()).toBe(0); + }); + + test('allows creating fresh logger after clear', () => { + const logger1 = initLogger('fresh-logger'); + clearLoggerCache(); + const logger2 = initLogger('fresh-logger'); + + // After clear, should be a new instance + expect(logger1).not.toBe(logger2); + }); + }); +}); diff --git a/rock/ts-sdk/src/logger.ts b/rock/ts-sdk/src/logger.ts new file mode 100644 index 000000000..9bc78f31f --- /dev/null +++ b/rock/ts-sdk/src/logger.ts @@ -0,0 +1,160 @@ +/** + * Winston-based logger module + */ + +import winston from 'winston'; +import { envVars } from './env_vars.js'; +import { join } from 'path'; +import { existsSync, mkdirSync } from 'fs'; + +/** + * Log levels + */ +const levels: winston.config.AbstractConfigSetLevels = { + error: 0, + warn: 1, + info: 2, + http: 3, + debug: 4, +}; + +/** + * Log level colors + */ +const colors: winston.config.AbstractConfigSetColors = { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'cyan', +}; + +winston.addColors(colors); + +/** + * Custom format for console output + */ +const consoleFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), + winston.format.colorize({ all: true }), + winston.format.printf((info) => { + const { timestamp, level, message, ...meta } = info; + const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : ''; + return `${timestamp} ${level}: ${message} ${metaStr}`; + }) +); + +/** + * Custom format for file output + */ +const fileFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), + winston.format.json() +); + +/** + * Get log level from environment + */ +function getLogLevel(): string { + const level = envVars.ROCK_LOGGING_LEVEL.toLowerCase(); + if (level in levels) { + return level; + } + return 'info'; +} + +/** + * Logger cache using winston Container for proper lifecycle management + */ +const loggerContainer = new winston.Container(); + +/** + * Get the number of cached loggers + */ +export function getLoggerCacheSize(): number { + return loggerContainer.loggers.size; +} + +/** + * Clear all cached loggers + */ +export function clearLoggerCache(): void { + // Close all loggers and clear the cache + for (const name of loggerContainer.loggers.keys()) { + loggerContainer.close(name); + } +} + +/** + * Initialize and return a logger instance + */ +export function initLogger(name: string = 'rock', fileName?: string): winston.Logger { + // Check if logger already exists in container + if (loggerContainer.has(name)) { + return loggerContainer.get(name); + } + + const transports: winston.transport[] = []; + const logLevel = getLogLevel(); + + // File transport + const logPath = envVars.ROCK_LOGGING_PATH; + const logFileName = fileName ?? envVars.ROCK_LOGGING_FILE_NAME; + + if (logPath) { + // Ensure directory exists + if (!existsSync(logPath)) { + mkdirSync(logPath, { recursive: true }); + } + + transports.push( + new winston.transports.File({ + filename: join(logPath, logFileName), + format: fileFormat, + level: logLevel, + }) + ); + } else { + // Console transport + transports.push( + new winston.transports.Console({ + format: consoleFormat, + level: logLevel, + }) + ); + } + + // Use winston.Container to create and cache the logger + const logger = loggerContainer.add(name, { + levels, + defaultMeta: { service: name }, + transports, + }); + + return logger; +} + +/** + * Get or create a child logger + */ +export function getChildLogger(parentName: string, childName: string): winston.Logger { + const fullName = `${parentName}:${childName}`; + return initLogger(fullName); +} + +/** + * Create a logger with context + */ +export function createContextLogger(context: string): winston.Logger { + const logger = initLogger(context); + return logger; +} + +/** + * Default logger instance + */ +export const defaultLogger = initLogger('rock'); + +// Re-export winston types +export type Logger = winston.Logger; +export type LogEntry = winston.LogEntry; diff --git a/rock/ts-sdk/src/model/client.test.ts b/rock/ts-sdk/src/model/client.test.ts new file mode 100644 index 000000000..07eb17768 --- /dev/null +++ b/rock/ts-sdk/src/model/client.test.ts @@ -0,0 +1,234 @@ +/** + * Tests for ModelClient timeout and cancellation support + * + * Following TDD: These tests are written BEFORE the implementation. + * They should FAIL initially, then we implement the feature to make them pass. + */ + +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { ModelClient, PollOptions } from './client.js'; +import * as retryUtils from '../utils/retry.js'; + +// Test that ModelClient uses the shared sleep function +describe('sleep function', () => { + test('ModelClient should use shared sleep from utils/retry.ts', async () => { + // This test verifies that ModelClient uses the shared sleep function + // by spying on the retry module's sleep function + + const testDir = join(tmpdir(), `model-client-sleep-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + const logFile = join(testDir, 'model.log'); + + try { + // Create log file with request that has index 0 + const REQUEST_START_MARKER = '__REQUEST_START__'; + const REQUEST_END_MARKER = '__REQUEST_END__'; + writeFileSync(logFile, `${REQUEST_START_MARKER}test${REQUEST_END_MARKER}{"index":0}\n`); + + const client = new ModelClient({ logFileName: logFile }); + + // Spy on the sleep function from retry module + const sleepSpy = jest.spyOn(retryUtils, 'sleep').mockResolvedValue(); + + // popRequest for index 1 should poll and call sleep + // before timing out (we set a very short timeout) + try { + await client.popRequest(1, { timeout: 0.1 }); + } catch { + // Expected to timeout + } + + // If ModelClient uses shared sleep, the spy should have been called + expect(sleepSpy).toHaveBeenCalled(); + + sleepSpy.mockRestore(); + } finally { + rmSync(testDir, { recursive: true, force: true }); + } + }); +}); + +// Test markers (must match client.ts) +const REQUEST_START_MARKER = '__REQUEST_START__'; +const REQUEST_END_MARKER = '__REQUEST_END__'; +const SESSION_END_MARKER = '__SESSION_END__'; + +describe('ModelClient timeout and cancellation', () => { + let testDir: string; + let logFile: string; + let client: ModelClient; + + beforeEach(() => { + testDir = join(tmpdir(), `model-client-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + logFile = join(testDir, 'model.log'); + client = new ModelClient({ logFileName: logFile }); + }); + + afterEach(() => { + rmSync(testDir, { recursive: true, force: true }); + }); + + describe('popRequest timeout', () => { + it('should throw TimeoutError when timeout expires', async () => { + // Create log file with a request that has different index + const content = `${REQUEST_START_MARKER}test-request${REQUEST_END_MARKER}{"index":0}\n`; + writeFileSync(logFile, content); + + // Request index 1, but only index 0 exists - should timeout + await expect(client.popRequest(1, { timeout: 0.1 })).rejects.toThrow('popRequest timed out'); + }); + + it('should return request when found before timeout', async () => { + // Create log file with the request we want + const content = `${REQUEST_START_MARKER}test-request${REQUEST_END_MARKER}{"index":1}\n`; + writeFileSync(logFile, content); + + const result = await client.popRequest(1, { timeout: 5 }); + expect(result).toBe('test-request'); + }); + + it('should return SESSION_END_MARKER when session ends', async () => { + const content = `${SESSION_END_MARKER}\n`; + writeFileSync(logFile, content); + + const result = await client.popRequest(1, { timeout: 1 }); + expect(result).toBe(SESSION_END_MARKER); + }); + }); + + describe('waitForFirstRequest timeout', () => { + it('should throw TimeoutError when timeout expires and no request arrives', async () => { + // Don't create log file - it doesn't exist + await expect(client.waitForFirstRequest({ timeout: 0.1 })).rejects.toThrow('waitForFirstRequest timed out'); + }); + + it('should return when log file has content before timeout', async () => { + // Create log file with content + writeFileSync(logFile, 'some content\n'); + + await expect(client.waitForFirstRequest({ timeout: 5 })).resolves.toBeUndefined(); + }); + }); + + describe('popRequest cancellation via AbortSignal', () => { + it('should throw AbortError when signal is aborted', async () => { + // Create log file with a request that has different index + const content = `${REQUEST_START_MARKER}test-request${REQUEST_END_MARKER}{"index":0}\n`; + writeFileSync(logFile, content); + + const controller = new AbortController(); + + // Abort after a short delay + setTimeout(() => controller.abort(), 100); + + await expect(client.popRequest(1, { timeout: 10, signal: controller.signal })).rejects.toThrow(); + }); + + it('should return request when found before abort', async () => { + // Create log file with the request we want + const content = `${REQUEST_START_MARKER}test-request${REQUEST_END_MARKER}{"index":1}\n`; + writeFileSync(logFile, content); + + const controller = new AbortController(); + + const result = await client.popRequest(1, { timeout: 5, signal: controller.signal }); + expect(result).toBe('test-request'); + }); + }); + + describe('waitForFirstRequest cancellation via AbortSignal', () => { + it('should throw AbortError when signal is aborted', async () => { + const controller = new AbortController(); + + // Abort after a short delay + setTimeout(() => controller.abort(), 100); + + await expect(client.waitForFirstRequest({ timeout: 10, signal: controller.signal })).rejects.toThrow(); + }); + + it('should return when log file has content before abort', async () => { + // Create log file with content + writeFileSync(logFile, 'some content\n'); + + const controller = new AbortController(); + + await expect(client.waitForFirstRequest({ timeout: 5, signal: controller.signal })).resolves.toBeUndefined(); + }); + }); + + describe('async file I/O', () => { + it('should import readFile from fs/promises (not readFileSync from fs)', async () => { + // Read the client.ts source to verify imports + const { readFileSync: readSource } = await import('fs'); + const sourceCode = readSource(require.resolve('./client.js'), 'utf-8'); + + // Should import from fs/promises + expect(sourceCode).toMatch(/from ['"]fs\/promises['"]/); + + // Should import readFile + expect(sourceCode).toMatch(/import\s+\{[^}]*readFile[^}]*\}\s+from\s+['"]fs\/promises['"]/); + + // Should NOT import readFileSync + expect(sourceCode).not.toMatch(/readFileSync/); + }); + + it('should import appendFile from fs/promises (not appendFileSync from fs)', async () => { + // Read the client.ts source to verify imports + const { readFileSync: readSource } = await import('fs'); + const sourceCode = readSource(require.resolve('./client.js'), 'utf-8'); + + // Should import appendFile from fs/promises + expect(sourceCode).toMatch(/import\s+\{[^}]*appendFile[^}]*\}\s+from\s+['"]fs\/promises['"]/); + + // Should NOT import appendFileSync + expect(sourceCode).not.toMatch(/appendFileSync/); + }); + }); + + describe('JSON parse error handling', () => { + it('should throw meaningful error when request line has corrupted JSON', async () => { + // Create log file with corrupted JSON in meta section + const content = `${REQUEST_START_MARKER}test-request${REQUEST_END_MARKER}{invalid json}\n`; + writeFileSync(logFile, content); + + // Should throw "Invalid request line format" immediately + // NOT retry until timeout, NOT throw raw SyntaxError + await expect(client.popRequest(1, { timeout: 5 })).rejects.toThrow('Invalid request line format'); + }); + + it('should throw meaningful error when response line has corrupted JSON', async () => { + // Create log file with request and corrupted response + const RESPONSE_START_MARKER = '__RESPONSE_START__'; + const RESPONSE_END_MARKER = '__RESPONSE_END__'; + const content = `${REQUEST_START_MARKER}test-request${REQUEST_END_MARKER}{"index":0}\n${RESPONSE_START_MARKER}response${RESPONSE_END_MARKER}{bad}\n`; + writeFileSync(logFile, content); + + // pushResponse internally calls parseResponseLine + // Should throw "Invalid response line format" + await expect(client.pushResponse(1, 'new-response')).rejects.toThrow('Invalid response line format'); + }); + }); + + describe('backward compatibility', () => { + it('should use default timeout when not specified', async () => { + // This test verifies backward compatibility + // Default timeout should be applied (not infinite wait) + // We'll test that the method signature accepts no options + const content = `${REQUEST_START_MARKER}test-request${REQUEST_END_MARKER}{"index":1}\n`; + writeFileSync(logFile, content); + + // Should work without timeout option + const result = await client.popRequest(1); + expect(result).toBe('test-request'); + }); + + it('waitForFirstRequest should work without options', async () => { + writeFileSync(logFile, 'some content\n'); + + await expect(client.waitForFirstRequest()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/rock/ts-sdk/src/model/client.ts b/rock/ts-sdk/src/model/client.ts new file mode 100644 index 000000000..cbb27456a --- /dev/null +++ b/rock/ts-sdk/src/model/client.ts @@ -0,0 +1,272 @@ +/** + * Model client for LLM interaction + */ + +import { existsSync } from 'fs'; +import { readFile, appendFile } from 'fs/promises'; +import { initLogger } from '../logger.js'; +import { envVars } from '../env_vars.js'; +import { sleep } from '../utils/retry.js'; + +const logger = initLogger('rock.model.client'); + +/** + * Default timeout for polling operations (in seconds) + */ +const DEFAULT_POLL_TIMEOUT = 60.0; + +/** + * Request/Response markers + */ +const REQUEST_START_MARKER = '__REQUEST_START__'; +const REQUEST_END_MARKER = '__REQUEST_END__'; +const RESPONSE_START_MARKER = '__RESPONSE_START__'; +const RESPONSE_END_MARKER = '__RESPONSE_END__'; +const SESSION_END_MARKER = '__SESSION_END__'; + +/** + * Model client configuration + */ +export interface ModelClientConfig { + logFileName?: string; +} + +/** + * Options for polling operations (popRequest, waitForFirstRequest) + */ +export interface PollOptions { + /** Maximum time to wait in seconds. Defaults to DEFAULT_POLL_TIMEOUT */ + timeout?: number; + /** AbortSignal for cancellation support */ + signal?: AbortSignal; +} + +/** + * Model client for LLM interaction + */ +export class ModelClient { + private logFile: string; + + constructor(config?: ModelClientConfig) { + this.logFile = config?.logFileName ?? envVars.ROCK_MODEL_SERVICE_DATA_DIR + '/model.log'; + } + + /** + * Anti-call LLM - input is response, output is next request + */ + async antiCallLlm(index: number, lastResponse?: string): Promise { + if (index < 0) { + throw new Error('index must be greater than 0'); + } + + if (index === 0) { + if (lastResponse !== undefined) { + throw new Error('lastResponse must be undefined when index is 0'); + } + await this.waitForFirstRequest(); + return this.popRequest(index + 1); + } + + if (lastResponse === undefined) { + throw new Error('lastResponse must not be undefined when index is greater than 0'); + } + + await this.pushResponse(index, lastResponse); + return this.popRequest(index + 1); + } + + /** + * Push response to log file + */ + async pushResponse(index: number, lastResponse: string): Promise { + const content = this.constructResponse(lastResponse, index); + const lastResponseLine = await this.readLastResponseLine(); + + if (lastResponseLine === null) { + await this.appendResponse(content); + return; + } + + const { meta } = this.parseResponseLine(lastResponseLine); + const lastResponseIndex = meta.index as number; + + if (index < lastResponseIndex) { + throw new Error(`index ${index} must not be smaller than last_response_index ${lastResponseIndex}`); + } + + if (index === lastResponseIndex) { + logger.debug(`response index ${index} already exists, skip`); + return; + } + + await this.appendResponse(content); + } + + /** + * Pop request from log file + * + * @param index - The index of the request to pop + * @param options - Optional configuration for timeout and cancellation + * @returns The request JSON string or SESSION_END_MARKER + * @throws Error if timeout expires or operation is aborted + */ + async popRequest(index: number, options?: PollOptions): Promise { + const timeout = options?.timeout ?? DEFAULT_POLL_TIMEOUT; + const startTime = Date.now(); + + while (true) { + // Check for abort signal + if (options?.signal?.aborted) { + throw new Error(`popRequest(index=${index}) aborted`); + } + + // Check for timeout + if ((Date.now() - startTime) / 1000 > timeout) { + throw new Error(`popRequest timed out after ${timeout} seconds`); + } + + try { + const lastRequestLine = await this.readLastRequestLine(); + const { requestJson, meta } = this.parseRequestLine(lastRequestLine); + + if (requestJson === SESSION_END_MARKER) { + return SESSION_END_MARKER; + } + + if (meta.index === index) { + return requestJson; + } + + logger.debug(`Last request is not the index ${index} we want, waiting...`); + await sleep(1000); + } catch (e) { + // Re-throw abort errors and parse errors immediately + if (e instanceof Error) { + if (e.message.includes('aborted')) { + throw e; + } + // Re-throw parse errors (invalid format) immediately - don't retry + if (e.message.includes('Invalid request line format')) { + throw e; + } + } + // For other errors (like file not found), wait and retry + logger.debug(`Error reading request: ${e}, waiting...`); + await sleep(1000); + } + } + } + + /** + * Wait for first request + * + * @param options - Optional configuration for timeout and cancellation + * @throws Error if timeout expires or operation is aborted + */ + async waitForFirstRequest(options?: PollOptions): Promise { + const timeout = options?.timeout ?? DEFAULT_POLL_TIMEOUT; + const startTime = Date.now(); + + while (true) { + // Check for abort signal + if (options?.signal?.aborted) { + throw new Error('waitForFirstRequest aborted'); + } + + // Check for timeout + if ((Date.now() - startTime) / 1000 > timeout) { + throw new Error(`waitForFirstRequest timed out after ${timeout} seconds`); + } + + if (!existsSync(this.logFile)) { + logger.debug(`Log file ${this.logFile} not found, waiting...`); + await sleep(1000); + continue; + } + + const content = await readFile(this.logFile, 'utf-8'); + const lines = content.split('\n').filter((l) => l.trim()); + + if (lines.length === 0) { + logger.debug(`Log file ${this.logFile} is empty, waiting for the first request...`); + await sleep(1000); + continue; + } + + return; + } + } + + private parseRequestLine(lineContent: string): { requestJson: string; meta: Record } { + if (lineContent.includes(SESSION_END_MARKER)) { + return { requestJson: SESSION_END_MARKER, meta: {} }; + } + + try { + const parts = lineContent.split(REQUEST_END_MARKER); + const metaJson = parts[1] ?? ''; + const requestJson = parts[0]?.split(REQUEST_START_MARKER)[1] ?? ''; + const meta = JSON.parse(metaJson); + + return { requestJson, meta }; + } catch (e) { + logger.error(`Failed to parse request line: ${lineContent}, error: ${e}`); + throw new Error(`Invalid request line format: ${e}`); + } + } + + private parseResponseLine(lineContent: string): { responseJson: string; meta: Record } { + try { + const parts = lineContent.split(RESPONSE_END_MARKER); + const metaJson = parts[1] ?? ''; + const responseJson = parts[0]?.split(RESPONSE_START_MARKER)[1] ?? ''; + const meta = JSON.parse(metaJson); + + return { responseJson, meta }; + } catch (e) { + logger.error(`Failed to parse response line: ${lineContent}, error: ${e}`); + throw new Error(`Invalid response line format: ${e}`); + } + } + + private async readLastRequestLine(): Promise { + const content = await readFile(this.logFile, 'utf-8'); + const lines = content.split('\n').filter((l) => l.trim()); + + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i]; + if (line && (line.includes(REQUEST_START_MARKER) || line.includes(SESSION_END_MARKER))) { + return line; + } + } + + throw new Error(`No request found in log file ${this.logFile}`); + } + + private async readLastResponseLine(): Promise { + const content = await readFile(this.logFile, 'utf-8'); + const lines = content.split('\n').filter((l) => l.trim()); + + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i]; + if (line && line.includes(RESPONSE_START_MARKER)) { + return line; + } + } + + return null; + } + + private async appendResponse(content: string): Promise { + await appendFile(this.logFile, content); + } + + private constructResponse(lastResponse: string, index: number): string { + const meta = { + timestamp: Date.now(), + index, + }; + const metaJson = JSON.stringify(meta); + return `${RESPONSE_START_MARKER}${lastResponse}${RESPONSE_END_MARKER}${metaJson}\n`; + } +} diff --git a/rock/ts-sdk/src/model/index.ts b/rock/ts-sdk/src/model/index.ts new file mode 100644 index 000000000..ada3ae791 --- /dev/null +++ b/rock/ts-sdk/src/model/index.ts @@ -0,0 +1,5 @@ +/** + * Model module - Model client + */ + +export * from './client.js'; diff --git a/rock/ts-sdk/src/sandbox/agent/base.test.ts b/rock/ts-sdk/src/sandbox/agent/base.test.ts new file mode 100644 index 000000000..8b8925963 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/agent/base.test.ts @@ -0,0 +1,120 @@ +import { Agent, DefaultAgent } from './base.js'; +import { RockAgentConfigSchema } from './config.js'; +import type { SandboxLike, RuntimeEnv } from '../runtime_env/base.js'; +import type { Observation } from '../../types/responses.js'; + +// Mock sandbox for testing +const createMockSandbox = (): SandboxLike => ({ + sandboxId: 'test-sandbox-id', + runtimeEnvs: {} as Record, + arun: jest.fn().mockResolvedValue({ exitCode: 0, output: 'success', failureReason: '', expectString: '' } as Observation), + createSession: jest.fn().mockResolvedValue(undefined), + startNohupProcess: jest.fn().mockResolvedValue({ pid: 12345, errorResponse: null }), + waitForProcessCompletion: jest.fn().mockResolvedValue({ success: true, message: 'completed' }), + handleNohupOutput: jest.fn().mockResolvedValue({ + output: 'test output', + exitCode: 0, + failureReason: '', + expectString: '', + } as Observation), +}); + +describe('Agent', () => { + describe('Agent base class', () => { + it('should be abstract and require sandbox', () => { + const mockSandbox = createMockSandbox(); + const agent = new DefaultAgent(mockSandbox); + expect(agent.sandbox).toBe(mockSandbox); + }); + }); +}); + +describe('DefaultAgent', () => { + describe('constructor', () => { + it('should create agent with sandbox', () => { + const mockSandbox = createMockSandbox(); + const agent = new DefaultAgent(mockSandbox); + expect(agent.sandbox).toBe(mockSandbox); + expect(agent.modelService).toBeNull(); + expect(agent.config).toBeNull(); + expect(agent.agentSession).toBeNull(); + }); + }); + + describe('install', () => { + it('should initialize agent with config', async () => { + const mockSandbox = createMockSandbox(); + const agent = new DefaultAgent(mockSandbox); + + const configResult = RockAgentConfigSchema.safeParse({ + agentSession: 'test-session', + env: { TEST_VAR: 'test_value' }, + }); + expect(configResult.success).toBe(true); + + if (configResult.success) { + await agent.install(configResult.data); + + expect(agent.config).toBe(configResult.data); + expect(agent.agentSession).toBe('test-session'); + expect(mockSandbox.createSession).toHaveBeenCalled(); + } + }); + + it('should execute pre-init commands', async () => { + const mockSandbox = createMockSandbox(); + const agent = new DefaultAgent(mockSandbox); + + const configResult = RockAgentConfigSchema.safeParse({ + agentSession: 'test-session', + preInitCmds: [{ command: 'echo pre-init', timeoutSeconds: 60 }], + }); + expect(configResult.success).toBe(true); + + if (configResult.success) { + await agent.install(configResult.data); + expect(mockSandbox.arun).toHaveBeenCalled(); + } + }); + + it('should execute post-init commands', async () => { + const mockSandbox = createMockSandbox(); + const agent = new DefaultAgent(mockSandbox); + + const configResult = RockAgentConfigSchema.safeParse({ + agentSession: 'test-session', + postInitCmds: [{ command: 'echo post-init', timeoutSeconds: 60 }], + }); + expect(configResult.success).toBe(true); + + if (configResult.success) { + await agent.install(configResult.data); + expect(mockSandbox.arun).toHaveBeenCalled(); + } + }); + }); + + describe('run', () => { + it('should throw error if not installed', async () => { + const mockSandbox = createMockSandbox(); + const agent = new DefaultAgent(mockSandbox); + + await expect(agent.run('test prompt')).rejects.toThrow('Agent is not installed'); + }); + + it('should throw error if runCmd is not set', async () => { + const mockSandbox = createMockSandbox(); + const agent = new DefaultAgent(mockSandbox); + + const configResult = RockAgentConfigSchema.safeParse({ + agentSession: 'test-session', + }); + expect(configResult.success).toBe(true); + + if (configResult.success) { + await agent.install(configResult.data); + await expect(agent.run('test prompt')).rejects.toThrow('runCmd is not configured'); + } + }); + }); +}); diff --git a/rock/ts-sdk/src/sandbox/agent/base.ts b/rock/ts-sdk/src/sandbox/agent/base.ts new file mode 100644 index 000000000..4c58a6a06 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/agent/base.ts @@ -0,0 +1,219 @@ +/** + * Agent base classes + */ + +import { initLogger } from '../../logger.js'; +import type { RockAgentConfig } from './config.js'; +import type { SandboxLike } from '../runtime_env/base.js'; +import type { ModelService } from '../model_service/base.js'; +import type { Observation } from '../../types/responses.js'; + +const logger = initLogger('rock.agent'); + +/** + * Abstract Agent base class + */ +export abstract class Agent { + protected _sandbox: SandboxLike; + protected _modelService: ModelService | null = null; + + constructor(sandbox: SandboxLike) { + this._sandbox = sandbox; + } + + get sandbox(): SandboxLike { + return this._sandbox; + } + + get modelService(): ModelService | null { + return this._modelService; + } + + abstract install(config: RockAgentConfig): Promise; + abstract run(prompt: string): Promise; +} + +/** + * DefaultAgent with common initialization and execution logic + */ +export class DefaultAgent extends Agent { + protected _config: RockAgentConfig | null = null; + protected _agentSession: string | null = null; + + get config(): RockAgentConfig | null { + return this._config; + } + + get agentSession(): string | null { + return this._agentSession; + } + + override get modelService(): ModelService | null { + return this._modelService; + } + + async install(config: RockAgentConfig): Promise { + this._config = config; + this._agentSession = config.agentSession; + + const sandboxId = this._sandbox.sandboxId; + logger.info(`[${sandboxId}] Starting agent initialization`); + + try { + // Setup bash session + await this._setupSession(); + + // Execute pre-init commands + await this._executeInitCommands(config.preInitCmds, 'pre-init'); + + // Execute post-init commands + await this._executeInitCommands(config.postInitCmds, 'post-init'); + + logger.info(`[${sandboxId}] Agent initialization completed`); + } catch (e) { + const error = e as Error; + logger.error(`[${sandboxId}] Agent initialization failed: ${error.message}`); + throw error; + } + } + + async run(prompt: string): Promise { + if (!this._config) { + throw new Error('Agent is not installed. Please call install() first.'); + } + + if (!this._config.runCmd) { + throw new Error('runCmd is not configured'); + } + + const cmd = await this._createAgentRunCmd(prompt); + return this._agentRun(cmd); + } + + protected async _setupSession(): Promise { + if (!this._config) return; + + const sandboxId = this._sandbox.sandboxId; + logger.info(`[${sandboxId}] Creating bash session: ${this._agentSession}`); + + await this._sandbox.createSession({ + session: this._agentSession!, + envEnable: true, + env: this._config.env, + }); + + logger.info(`[${sandboxId}] Bash session '${this._agentSession}' created successfully`); + } + + protected async _executeInitCommands( + cmdList: Array<{ command: string; timeoutSeconds: number }>, + stepName: string + ): Promise { + if (!cmdList || cmdList.length === 0) return; + + const sandboxId = this._sandbox.sandboxId; + logger.info(`[${sandboxId}] ${stepName} started: Executing ${cmdList.length} commands`); + + for (let idx = 0; idx < cmdList.length; idx++) { + const cmdConfig = cmdList[idx]; + if (!cmdConfig) continue; + + const command = cmdConfig.command; + const timeout = cmdConfig.timeoutSeconds; + + logger.debug( + `[${sandboxId}] Executing ${stepName} command ${idx + 1}/${cmdList.length}: ${command.substring(0, 100)}...` + ); + + const result = await this._sandbox.arun(`bash -c ${JSON.stringify(command)}`, { + waitTimeout: timeout, + mode: 'NOHUP', + }); + + if (result.exitCode !== 0) { + throw new Error( + `[${sandboxId}] ${stepName} command ${idx + 1} failed with exit code ${result.exitCode}: ${result.output.substring(0, 200)}` + ); + } + + logger.debug(`[${sandboxId}] ${stepName} command ${idx + 1} completed successfully`); + } + + logger.info(`[${sandboxId}] ${stepName} completed: Completed ${cmdList.length} commands`); + } + + protected async _createAgentRunCmd(prompt: string): Promise { + if (!this._config || !this._config.runCmd) { + throw new Error('runCmd is not configured'); + } + + // Replace {prompt} placeholder + let runCmd = this._config.runCmd.replace(/{prompt}/g, JSON.stringify(prompt)); + + // Skip wrap if configured + if (this._config.skipWrapRunCmd) { + return `bash -c ${JSON.stringify(runCmd)}`; + } + + return `bash -c ${JSON.stringify(runCmd)}`; + } + + protected async _agentRun(cmd: string): Promise { + const sandboxId = this._sandbox.sandboxId; + + try { + const timestamp = Date.now().toString(); + const tmpFile = `/tmp/tmp_${timestamp}.out`; + + // Check if startNohupProcess is available + if (!this._sandbox.startNohupProcess) { + const msg = 'startNohupProcess method is not available on sandbox'; + return { output: msg, exitCode: 1, failureReason: msg, expectString: '' }; + } + + // Start nohup process and get PID + const { pid, errorResponse } = await this._sandbox.startNohupProcess(cmd, tmpFile, this._agentSession!); + + if (errorResponse) { + return errorResponse; + } + + if (!pid) { + const msg = 'Failed to submit command, nohup failed to extract PID'; + return { output: msg, exitCode: 1, failureReason: msg, expectString: '' }; + } + + logger.info(`[${sandboxId}] Agent process started with PID: ${pid}`); + + // Check if waitForProcessCompletion is available + if (!this._sandbox.waitForProcessCompletion) { + const msg = 'waitForProcessCompletion method is not available on sandbox'; + return { output: msg, exitCode: 1, failureReason: msg, expectString: '' }; + } + + // Wait for agent process to complete + const { success, message } = await this._sandbox.waitForProcessCompletion( + pid, + this._agentSession!, + this._config!.agentRunTimeout, + this._config!.agentRunCheckInterval + ); + + // Check if handleNohupOutput is available + if (!this._sandbox.handleNohupOutput) { + const msg = 'handleNohupOutput method is not available on sandbox'; + return { output: msg, exitCode: 1, failureReason: msg, expectString: '' }; + } + + // Handle nohup output and return result + const result = await this._sandbox.handleNohupOutput(tmpFile, this._agentSession!, success, message, false, null); + + return result; + } catch (e) { + const error = e as Error; + const errorMsg = `Failed to execute nohup command: ${error.message}`; + logger.error(`[${sandboxId}] ${errorMsg}`); + return { output: errorMsg, exitCode: 1, failureReason: errorMsg, expectString: '' }; + } + } +} diff --git a/rock/ts-sdk/src/sandbox/agent/config.test.ts b/rock/ts-sdk/src/sandbox/agent/config.test.ts new file mode 100644 index 000000000..a007422dc --- /dev/null +++ b/rock/ts-sdk/src/sandbox/agent/config.test.ts @@ -0,0 +1,222 @@ +import { + AgentConfigSchema, + AgentBashCommandSchema, + DefaultAgentConfigSchema, + RockAgentConfigSchema, +} from './config.js'; + +describe('AgentConfig', () => { + describe('AgentConfigSchema', () => { + it('should parse valid config with required fields', () => { + const result = AgentConfigSchema.safeParse({ + agentType: 'default', + version: '1.0.0', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.agentType).toBe('default'); + expect(result.data.version).toBe('1.0.0'); + } + }); + + it('should use default version when not specified', () => { + const result = AgentConfigSchema.safeParse({ + agentType: 'default', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.version).toBe('default'); + } + }); + + it('should reject config without agentType', () => { + const result = AgentConfigSchema.safeParse({ + version: '1.0.0', + }); + expect(result.success).toBe(false); + }); + }); +}); + +describe('AgentBashCommand', () => { + describe('AgentBashCommandSchema', () => { + it('should parse valid command', () => { + const result = AgentBashCommandSchema.safeParse({ + command: 'echo hello', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.command).toBe('echo hello'); + expect(result.data.timeoutSeconds).toBe(300); + } + }); + + it('should parse command with custom timeout', () => { + const result = AgentBashCommandSchema.safeParse({ + command: 'long-running-command', + timeoutSeconds: 600, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.timeoutSeconds).toBe(600); + } + }); + + it('should reject config without command', () => { + const result = AgentBashCommandSchema.safeParse({ + timeoutSeconds: 300, + }); + expect(result.success).toBe(false); + }); + + it('should reject non-positive timeout', () => { + const result = AgentBashCommandSchema.safeParse({ + command: 'echo hello', + timeoutSeconds: 0, + }); + expect(result.success).toBe(false); + }); + }); +}); + +describe('DefaultAgentConfig', () => { + describe('DefaultAgentConfigSchema', () => { + it('should parse valid config with defaults', () => { + const result = DefaultAgentConfigSchema.safeParse({ + agentType: 'default', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.agentType).toBe('default'); + expect(result.data.agentSession).toBe('default-agent-session'); + expect(result.data.sessionEnvs).toEqual({}); + expect(result.data.preInitBashCmdList).toBeInstanceOf(Array); + expect(result.data.postInitBashCmdList).toEqual([]); + expect(result.data.modelServiceConfig).toBeNull(); + } + }); + + it('should parse config with sessionEnvs', () => { + const result = DefaultAgentConfigSchema.safeParse({ + agentType: 'default', + sessionEnvs: { NODE_ENV: 'development' }, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.sessionEnvs).toEqual({ NODE_ENV: 'development' }); + } + }); + }); +}); + +describe('RockAgentConfig', () => { + describe('RockAgentConfigSchema', () => { + it('should parse valid config with defaults', () => { + const result = RockAgentConfigSchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.agentType).toBe('default'); + expect(result.data.version).toBe('default'); + expect(result.data.agentInstalledDir).toBe('/tmp/installed_agent'); + expect(result.data.projectPath).toBeNull(); + expect(result.data.useDeployWorkingDirAsFallback).toBe(true); + expect(result.data.env).toEqual({}); + expect(result.data.agentInstallTimeout).toBe(600); + expect(result.data.agentRunTimeout).toBe(1800); + expect(result.data.agentRunCheckInterval).toBe(30); + expect(result.data.workingDir).toBeNull(); + expect(result.data.runCmd).toBeNull(); + expect(result.data.skipWrapRunCmd).toBe(false); + } + }); + + it('should parse config with custom values', () => { + const result = RockAgentConfigSchema.safeParse({ + agentType: 'custom-agent', + version: '2.0.0', + agentInstalledDir: '/custom/agent/dir', + projectPath: '/path/to/project', + env: { API_KEY: 'secret' }, + agentInstallTimeout: 1200, + agentRunTimeout: 3600, + agentRunCheckInterval: 60, + workingDir: '/local/workdir', + runCmd: 'node agent.js --prompt {prompt}', + skipWrapRunCmd: true, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.agentType).toBe('custom-agent'); + expect(result.data.version).toBe('2.0.0'); + expect(result.data.agentInstalledDir).toBe('/custom/agent/dir'); + expect(result.data.projectPath).toBe('/path/to/project'); + expect(result.data.env).toEqual({ API_KEY: 'secret' }); + expect(result.data.agentInstallTimeout).toBe(1200); + expect(result.data.agentRunTimeout).toBe(3600); + expect(result.data.agentRunCheckInterval).toBe(60); + expect(result.data.workingDir).toBe('/local/workdir'); + expect(result.data.runCmd).toBe('node agent.js --prompt {prompt}'); + expect(result.data.skipWrapRunCmd).toBe(true); + } + }); + + it('should reject when agentRunCheckInterval >= agentRunTimeout', () => { + const result = RockAgentConfigSchema.safeParse({ + agentRunTimeout: 30, + agentRunCheckInterval: 60, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.some((issue) => issue.message.includes('must be less than'))).toBe(true); + } + }); + + it('should reject non-positive agentInstallTimeout', () => { + const result = RockAgentConfigSchema.safeParse({ + agentInstallTimeout: 0, + }); + expect(result.success).toBe(false); + }); + + it('should reject non-positive agentRunTimeout', () => { + const result = RockAgentConfigSchema.safeParse({ + agentRunTimeout: -1, + }); + expect(result.success).toBe(false); + }); + + it('should generate unique agentSession if not specified', () => { + const result1 = RockAgentConfigSchema.safeParse({}); + const result2 = RockAgentConfigSchema.safeParse({}); + expect(result1.success).toBe(true); + expect(result2.success).toBe(true); + if (result1.success && result2.success) { + expect(result1.data.agentSession).toMatch(/^agent-session-/); + expect(result2.data.agentSession).toMatch(/^agent-session-/); + expect(result1.data.agentSession).not.toBe(result2.data.agentSession); + } + }); + + it('should generate unique instanceId if not specified', () => { + const result1 = RockAgentConfigSchema.safeParse({}); + const result2 = RockAgentConfigSchema.safeParse({}); + expect(result1.success).toBe(true); + expect(result2.success).toBe(true); + if (result1.success && result2.success) { + expect(result1.data.instanceId).toMatch(/^instance-id-/); + expect(result2.data.instanceId).toMatch(/^instance-id-/); + expect(result1.data.instanceId).not.toBe(result2.data.instanceId); + } + }); + + it('should generate unique agentName if not specified', () => { + const result1 = RockAgentConfigSchema.safeParse({}); + const result2 = RockAgentConfigSchema.safeParse({}); + expect(result1.success).toBe(true); + expect(result2.success).toBe(true); + if (result1.success && result2.success) { + expect(result1.data.agentName).not.toBe(result2.data.agentName); + } + }); + }); +}); diff --git a/rock/ts-sdk/src/sandbox/agent/config.ts b/rock/ts-sdk/src/sandbox/agent/config.ts new file mode 100644 index 000000000..3b72e76fd --- /dev/null +++ b/rock/ts-sdk/src/sandbox/agent/config.ts @@ -0,0 +1,100 @@ +/** + * Agent configuration schemas + */ + +import { z } from 'zod'; +import { randomUUID } from 'crypto'; +import { envVars } from '../../env_vars.js'; +import type { ModelServiceConfig } from '../model_service/base.js'; + +/** + * Base agent configuration schema + */ +export const AgentConfigSchema = z.object({ + agentType: z.string(), + version: z.string().default('default'), +}); + +export type AgentConfig = z.infer; + +/** + * Configuration for a command execution with timeout control + */ +export const AgentBashCommandSchema = z.object({ + command: z.string(), + timeoutSeconds: z.number().int().positive().default(300), +}); + +export type AgentBashCommand = z.infer; + +/** + * Default agent configuration schema + */ +export const DefaultAgentConfigSchema = z.object({ + agentType: z.string(), + version: z.string().default('default'), + + // Session management + agentSession: z.string().default('default-agent-session'), + + // Startup/shutdown commands + preInitBashCmdList: z.array(AgentBashCommandSchema).default( + envVars.ROCK_AGENT_PRE_INIT_BASH_CMD_LIST.map((cmd) => ({ + command: cmd.command, + timeoutSeconds: cmd.timeoutSeconds || 300, + })) + ), + postInitBashCmdList: z.array(AgentBashCommandSchema).default([]), + + // Environment variables for the session + sessionEnvs: z.record(z.string()).default({}), + + // Optional ModelService configuration + modelServiceConfig: z.custom().nullable().default(null), +}); + +export type DefaultAgentConfig = z.infer; + +/** + * RockAgent configuration schema with validation + */ +export const RockAgentConfigSchema = z + .object({ + agentType: z.string().default('default'), + agentName: z.string().default(() => randomUUID().replace(/-/g, '')), + version: z.string().default('default'), + + agentInstalledDir: z.string().default('/tmp/installed_agent'), + instanceId: z.string().default(() => `instance-id-${randomUUID().replace(/-/g, '')}`), + + projectPath: z.string().nullable().default(null), + useDeployWorkingDirAsFallback: z.boolean().default(true), + + agentSession: z.string().default(() => `agent-session-${randomUUID().replace(/-/g, '')}`), + + env: z.record(z.string()).default({}), + + preInitCmds: z.array(AgentBashCommandSchema).default( + envVars.ROCK_AGENT_PRE_INIT_BASH_CMD_LIST.map((cmd) => ({ + command: cmd.command, + timeoutSeconds: cmd.timeoutSeconds || 300, + })) + ), + postInitCmds: z.array(AgentBashCommandSchema).default([]), + + agentInstallTimeout: z.number().int().positive().default(600), + agentRunTimeout: z.number().int().positive().default(1800), + agentRunCheckInterval: z.number().int().positive().default(30), + + workingDir: z.string().nullable().default(null), + runCmd: z.string().nullable().default(null), + skipWrapRunCmd: z.boolean().default(false), + + runtimeEnvConfig: z.any().nullable().default(null), + modelServiceConfig: z.custom().nullable().default(null), + }) + .refine((data) => data.agentRunCheckInterval < data.agentRunTimeout, { + message: 'agentRunCheckInterval must be less than agentRunTimeout', + }); + +export type RockAgentConfig = z.infer; diff --git a/rock/ts-sdk/src/sandbox/agent/index.ts b/rock/ts-sdk/src/sandbox/agent/index.ts new file mode 100644 index 000000000..c2c94d280 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/agent/index.ts @@ -0,0 +1,15 @@ +/** + * Agent module exports + */ + +export { Agent, DefaultAgent } from './base.js'; +export { + AgentConfigSchema, + type AgentConfig, + AgentBashCommandSchema, + type AgentBashCommand, + DefaultAgentConfigSchema, + type DefaultAgentConfig, + RockAgentConfigSchema, + type RockAgentConfig, +} from './config.js'; diff --git a/rock/ts-sdk/src/sandbox/client.test.ts b/rock/ts-sdk/src/sandbox/client.test.ts new file mode 100644 index 000000000..b6aec5408 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/client.test.ts @@ -0,0 +1,880 @@ +/** + * Tests for Sandbox Client - Exception handling + */ + +import axios from 'axios'; +import { Sandbox } from './client.js'; +import { RunMode } from '../common/constants.js'; +import { + BadRequestRockError, + InternalServerRockError, + CommandRockError, + RockException, +} from '../common/exceptions.js'; +import { Codes } from '../types/codes.js'; + +// Mock axios +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +// Helper to create mock axios response +function createMockPost(data: unknown, headers: Record = {}) { + return jest.fn().mockResolvedValue({ + data, + headers, + }); +} + +// Helper to create mock axios get +function createMockGet(data: unknown, headers: Record = {}) { + return jest.fn().mockResolvedValue({ + data, + headers, + }); +} + +describe('Sandbox Exception Handling', () => { + let sandbox: Sandbox; + let mockPost: jest.Mock; + let mockGet: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockPost = jest.fn(); + mockGet = jest.fn(); + mockedAxios.create = jest.fn().mockReturnValue({ + post: mockPost, + get: mockGet, + }); + + sandbox = new Sandbox({ + image: 'test:latest', + startupTimeout: 2, // Short timeout for tests + }); + }); + + describe('start() - error code handling', () => { + test('should throw BadRequestRockError when API returns 4xxx code', async () => { + // Mock the start_async API to return an error response with code + mockPost.mockResolvedValueOnce({ + data: { + status: 'Failed', + result: { + sandbox_id: 'test-id', + code: Codes.BAD_REQUEST, + }, + }, + headers: {}, + }); + + await expect(sandbox.start()).rejects.toThrow(BadRequestRockError); + }); + + test('should throw InternalServerRockError when API returns 5xxx code', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Failed', + result: { + sandbox_id: 'test-id', + code: Codes.INTERNAL_SERVER_ERROR, + }, + }, + headers: {}, + }); + + await expect(sandbox.start()).rejects.toThrow(InternalServerRockError); + }); + + test('should throw CommandRockError when API returns 6xxx code', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Failed', + result: { + sandbox_id: 'test-id', + code: Codes.COMMAND_ERROR, + }, + }, + headers: {}, + }); + + await expect(sandbox.start()).rejects.toThrow(CommandRockError); + }); + + test('should throw RockException for unknown error codes', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Failed', + result: { + sandbox_id: 'test-id', + code: 7000, + }, + }, + headers: {}, + }); + + await expect(sandbox.start()).rejects.toThrow(RockException); + }); + + test('should throw InternalServerRockError on startup timeout', async () => { + // Mock successful start_async but sandbox never becomes alive + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + + // Mock getStatus to return not alive + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { + is_alive: false, + }, + }, + headers: {}, + }); + + await expect(sandbox.start()).rejects.toThrow(InternalServerRockError); + }, 10000); // 10s timeout for this test + }); + + describe('execute() - error code handling', () => { + test('should throw BadRequestRockError when API returns 4xxx code', async () => { + // First start the sandbox + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + + // Mock execute to return error + mockPost.mockResolvedValueOnce({ + data: { + status: 'Failed', + result: { + code: Codes.BAD_REQUEST, + }, + }, + headers: {}, + }); + + await expect(sandbox.execute({ command: 'test', timeout: 60 })).rejects.toThrow(BadRequestRockError); + }); + }); + + describe('createSession() - error code handling', () => { + test('should throw BadRequestRockError when API returns 4xxx code', async () => { + // First start the sandbox + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + + // Mock createSession to return error + mockPost.mockResolvedValueOnce({ + data: { + status: 'Failed', + result: { + code: Codes.BAD_REQUEST, + }, + }, + headers: {}, + }); + + await expect(sandbox.createSession({ + session: 'test', + startupSource: [], + envEnable: false + })).rejects.toThrow(BadRequestRockError); + }); + }); +}); + +/** + * Zod Validation Tests + * + * These tests verify that API responses are validated against Zod schemas. + * Similar to Python SDK's Pydantic validation: CommandResponse(**result) + */ +import { ZodError } from 'zod'; + +describe('Zod Validation', () => { + let sandbox: Sandbox; + let mockPost: jest.Mock; + let mockGet: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockPost = jest.fn(); + mockGet = jest.fn(); + mockedAxios.create = jest.fn().mockReturnValue({ + post: mockPost, + get: mockGet, + }); + + sandbox = new Sandbox({ + image: 'test:latest', + startupTimeout: 2, + }); + }); + + describe('execute() - Zod validation', () => { + beforeEach(async () => { + // Start the sandbox first + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + }); + + test('should throw ZodError when stdout is not a string', async () => { + // Return invalid data: stdout as number instead of string + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + stdout: 12345, // Invalid: should be string + stderr: '', + exit_code: 0, + }, + }, + headers: {}, + }); + + await expect(sandbox.execute({ command: 'test', timeout: 60 })).rejects.toThrow(ZodError); + }); + + test('should throw ZodError when exitCode is not a number', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + stdout: 'output', + stderr: '', + exit_code: '0', // Invalid: should be number + }, + }, + headers: {}, + }); + + await expect(sandbox.execute({ command: 'test', timeout: 60 })).rejects.toThrow(ZodError); + }); + + test('should pass validation with valid CommandResponse', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + stdout: 'valid output', + stderr: '', + exit_code: 0, + }, + }, + headers: {}, + }); + + const result = await sandbox.execute({ command: 'test', timeout: 60 }); + + expect(result.stdout).toBe('valid output'); + expect(result.exitCode).toBe(0); + }); + }); + + describe('getStatus() - Zod validation', () => { + beforeEach(async () => { + // Start the sandbox first + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + }); + + test('should throw ZodError when isAlive is not a boolean', async () => { + mockGet.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + is_alive: 'true', // Invalid: should be boolean + }, + }, + headers: {}, + }); + + await expect(sandbox.getStatus()).rejects.toThrow(ZodError); + }); + + test('should pass validation with valid SandboxStatusResponse', async () => { + mockGet.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + is_alive: true, + host_name: 'test-host', + }, + }, + headers: { + 'x-rock-gateway-target-cluster': 'test-cluster', + }, + }); + + const result = await sandbox.getStatus(); + + expect(result.sandboxId).toBe('test-id'); + expect(result.isAlive).toBe(true); + }); + }); + + describe('createSession() - Zod validation', () => { + beforeEach(async () => { + // Start the sandbox first + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + }); + + test('should throw ZodError when sessionType is invalid', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: '', + session_type: 'invalid', // Invalid: should be 'bash' + }, + }, + headers: {}, + }); + + await expect(sandbox.createSession({ + session: 'test', + startupSource: [], + envEnable: false + })).rejects.toThrow(ZodError); + }); + + test('should pass validation with valid CreateSessionResponse', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: 'session created', + session_type: 'bash', + }, + }, + headers: {}, + }); + + const result = await sandbox.createSession({ + session: 'test', + startupSource: [], + envEnable: false + }); + + expect(result.output).toBe('session created'); + expect(result.sessionType).toBe('bash'); + }); + }); + + describe('readFile() - Zod validation', () => { + beforeEach(async () => { + // Start the sandbox first + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + }); + + test('should throw ZodError when content is not a string', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + content: { invalid: 'object' }, // Invalid: should be string + }, + }, + headers: {}, + }); + + await expect(sandbox.readFile({ path: '/test.txt' })).rejects.toThrow(ZodError); + }); + + test('should pass validation with valid ReadFileResponse', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + content: 'file content', + }, + }, + headers: {}, + }); + + const result = await sandbox.readFile({ path: '/test.txt' }); + + expect(result.content).toBe('file content'); + }); + }); +}); + +/** + * arun() session creation behavior tests + * + * These tests verify that arun() matches Python SDK behavior: + * - NORMAL mode: do NOT pre-create session, run command directly + * - NOHUP mode: only create session when session is NOT provided + */ +describe('arun() - session creation behavior (matching Python SDK)', () => { + let sandbox: Sandbox; + let mockPost: jest.Mock; + let mockGet: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockPost = jest.fn(); + mockGet = jest.fn(); + mockedAxios.create = jest.fn().mockReturnValue({ + post: mockPost, + get: mockGet, + }); + + sandbox = new Sandbox({ + image: 'test:latest', + startupTimeout: 2, + }); + }); + + describe('NORMAL mode', () => { + beforeEach(async () => { + // Start the sandbox + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + }); + + test('should NOT call createSession before running command in NORMAL mode', async () => { + // Mock run_in_session response + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: 'command output', + exit_code: 0, + failure_reason: '', + expect_string: '', + }, + }, + headers: {}, + }); + + await sandbox.arun('echo hello', { mode: 'normal', session: 'existing-session' }); + + // Should only have called run_in_session, NOT create_session + const postCalls = mockPost.mock.calls; + const calledEndpoints = postCalls.map((call: unknown[]) => call[0] as string); + + // Should NOT have called create_session + expect(calledEndpoints.some((url: string) => url.includes('create_session'))).toBe(false); + // Should have called run_in_session + expect(calledEndpoints.some((url: string) => url.includes('run_in_session'))).toBe(true); + }); + + test('should directly run command without session pre-creation in NORMAL mode', async () => { + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: 'test output', + exit_code: 0, + failure_reason: '', + expect_string: '', + }, + }, + headers: {}, + }); + + const result = await sandbox.arun('ls -la', { mode: 'normal', session: 'my-session' }); + + expect(result.output).toBe('test output'); + expect(result.exitCode).toBe(0); + }); + }); + + describe('NOHUP mode', () => { + beforeEach(async () => { + // Start the sandbox + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + }); + + test('should NOT create session when session is provided in NOHUP mode', async () => { + // Mock nohup command response (for PID extraction) + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: '__ROCK_PID_START__12345__ROCK_PID_END__', + exit_code: 0, + failure_reason: '', + expect_string: '', + }, + }, + headers: {}, + }); + + // Mock kill -0 check (process completed) + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: '', + exit_code: 1, // Process doesn't exist = completed + failure_reason: '', + expect_string: '', + }, + }, + headers: {}, + }); + + // Mock output file read + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: 'nohup output', + exit_code: 0, + failure_reason: '', + expect_string: '', + }, + }, + headers: {}, + }); + + await sandbox.arun('long-running-command', { + mode: 'nohup', + session: 'existing-nohup-session', + waitTimeout: 1, + waitInterval: 1, + }); + + // Should NOT have called create_session since session was provided + const postCalls = mockPost.mock.calls; + const calledEndpoints = postCalls.map((call: unknown[]) => call[0] as string); + + expect(calledEndpoints.some((url: string) => url.includes('create_session'))).toBe(false); + }); + + test('should create session when session is NOT provided in NOHUP mode', async () => { + // Mock create_session response + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: '', + session_type: 'bash', + }, + }, + headers: {}, + }); + + // Mock nohup command response + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: '__ROCK_PID_START__12345__ROCK_PID_END__', + exit_code: 0, + failure_reason: '', + expect_string: '', + }, + }, + headers: {}, + }); + + // Mock kill -0 check (process completed) + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: '', + exit_code: 1, + failure_reason: '', + expect_string: '', + }, + }, + headers: {}, + }); + + // Mock output file read + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + output: 'nohup output', + exit_code: 0, + failure_reason: '', + expect_string: '', + }, + }, + headers: {}, + }); + + await sandbox.arun('long-running-command', { + mode: 'nohup', + // session NOT provided + waitTimeout: 1, + waitInterval: 1, + }); + + // Should have called create_session since session was NOT provided + const postCalls = mockPost.mock.calls; + const calledEndpoints = postCalls.map((call: unknown[]) => call[0] as string); + + expect(calledEndpoints.some((url: string) => url.includes('create_session'))).toBe(true); + }); + }); +}); + +/** + * uploadByPath() async file I/O tests + * + * These tests verify that uploadByPath uses async file operations (fs/promises) + * instead of sync operations that block the event loop. + */ + +// Mock fs/promises module +const mockAccess = jest.fn(); +const mockReadFile = jest.fn(); + +jest.mock('fs/promises', () => ({ + access: mockAccess, + readFile: mockReadFile, +})); + +describe('uploadByPath() - async file I/O', () => { + let sandbox: Sandbox; + let mockPost: jest.Mock; + let mockGet: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + mockPost = jest.fn(); + mockGet = jest.fn(); + mockedAxios.create = jest.fn().mockReturnValue({ + post: mockPost, + get: mockGet, + }); + + sandbox = new Sandbox({ + image: 'test:latest', + startupTimeout: 2, + }); + }); + + describe('async file operations', () => { + beforeEach(async () => { + // Start the sandbox + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: { + sandbox_id: 'test-id', + host_name: 'test-host', + host_ip: '127.0.0.1', + }, + }, + headers: {}, + }); + mockGet.mockResolvedValue({ + data: { + status: 'Success', + result: { is_alive: true }, + }, + headers: {}, + }); + await sandbox.start(); + }); + + test('should use fs/promises.readFile (async) instead of fs.readFileSync', async () => { + // Mock upload response + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: {}, + }, + headers: {}, + }); + + // Mock fs/promises methods + mockAccess.mockResolvedValueOnce(undefined); + mockReadFile.mockResolvedValueOnce(Buffer.from('test content')); + + const tempFile = '/tmp/test-upload-file.txt'; + const result = await sandbox.uploadByPath(tempFile, '/remote/path.txt'); + + // Verify async readFile was called instead of sync readFileSync + expect(mockReadFile).toHaveBeenCalledWith(tempFile); + expect(result.success).toBe(true); + }); + + test('should use fs/promises.access (async) for file existence check', async () => { + // Mock upload response + mockPost.mockResolvedValueOnce({ + data: { + status: 'Success', + result: {}, + }, + headers: {}, + }); + + // Mock fs/promises methods + mockAccess.mockResolvedValueOnce(undefined); + mockReadFile.mockResolvedValueOnce(Buffer.from('test content')); + + const tempFile = '/tmp/test-upload-file.txt'; + await sandbox.uploadByPath(tempFile, '/remote/path.txt'); + + // Verify async access was called instead of sync existsSync + expect(mockAccess).toHaveBeenCalledWith(tempFile); + }); + + test('should return failure when file does not exist (async access throws)', async () => { + // Mock access to throw (file not found) + mockAccess.mockRejectedValueOnce(new Error('ENOENT')); + + const result = await sandbox.uploadByPath('/nonexistent/file.txt', '/remote/path.txt'); + + expect(result.success).toBe(false); + expect(result.message).toContain('File not found'); + }); + }); +}); \ No newline at end of file diff --git a/rock/ts-sdk/src/sandbox/client.ts b/rock/ts-sdk/src/sandbox/client.ts new file mode 100644 index 000000000..09986f172 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/client.ts @@ -0,0 +1,723 @@ +/** + * Sandbox client - Core sandbox management + */ + +import { randomUUID } from 'crypto'; +import { initLogger } from '../logger.js'; +import { raiseForCode, InternalServerRockError } from '../common/exceptions.js'; +import { HttpUtils } from '../utils/http.js'; +import { sleep } from '../utils/retry.js'; +import { + SandboxConfig, + SandboxGroupConfig, + createSandboxConfig, + createSandboxGroupConfig, +} from './config.js'; +import { Deploy } from './deploy.js'; +import { LinuxFileSystem } from './file_system.js'; +import { Network } from './network.js'; +import { Process } from './process.js'; +import { LinuxRemoteUser } from './remote_user.js'; +import { extractNohupPid } from './utils.js'; +import { RunModeType, RunMode as RunModeEnum } from '../common/constants.js'; +export type { RunModeType }; +export { RunModeEnum as RunMode }; +import { + ObservationSchema, + CommandResponseSchema, + IsAliveResponseSchema, + SandboxStatusResponseSchema, + CreateSessionResponseSchema, + WriteFileResponseSchema, + ReadFileResponseSchema, + UploadResponseSchema, + CloseSessionResponseSchema, +} from '../types/responses.js'; +import type { + Observation, + CommandResponse, + IsAliveResponse, + SandboxStatusResponse, + CreateSessionResponse, + WriteFileResponse, + ReadFileResponse, + UploadResponse, + CloseSessionResponse, +} from '../types/responses.js'; +import type { + Command, + CreateBashSessionRequest, + WriteFileRequest, + ReadFileRequest, + UploadRequest, + CloseSessionRequest, +} from '../types/requests.js'; + +const logger = initLogger('rock.sandbox'); + +/** + * Abstract sandbox interface + */ +export abstract class AbstractSandbox { + abstract isAlive(): Promise; + abstract createSession(request: CreateBashSessionRequest): Promise; + abstract execute(command: Command): Promise; + abstract readFile(request: ReadFileRequest): Promise; + abstract writeFile(request: WriteFileRequest): Promise; + abstract upload(request: UploadRequest): Promise; + abstract closeSession(request: CloseSessionRequest): Promise; + abstract arun( + cmd: string, + options?: { + session?: string; + mode?: RunModeType; + timeout?: number; + waitTimeout?: number; + waitInterval?: number; + responseLimitedBytesInNohup?: number; + ignoreOutput?: boolean; + outputFile?: string; + } + ): Promise; + abstract close(): Promise; +} + +/** + * Sandbox - Main sandbox client + */ +export class Sandbox extends AbstractSandbox { + private config: SandboxConfig; + private url: string; + private routeKey: string; + private sandboxId: string | null = null; + private hostName: string | null = null; + private hostIp: string | null = null; + private cluster: string; + + // Sub-components + private deploy: Deploy; + private fs: LinuxFileSystem; + private network: Network; + private process: Process; + private remoteUser: LinuxRemoteUser; + + constructor(config: Partial = {}) { + super(); + this.config = createSandboxConfig(config); + this.url = `${this.config.baseUrl}/apis/envs/sandbox/v1`; + this.routeKey = this.config.routeKey ?? randomUUID().replace(/-/g, ''); + this.cluster = this.config.cluster; + + this.deploy = new Deploy(this); + this.fs = new LinuxFileSystem(this); + this.network = new Network(this); + this.process = new Process(this); + this.remoteUser = new LinuxRemoteUser(this); + } + + // Getters + getSandboxId(): string { + if (!this.sandboxId) { + throw new Error('Sandbox not started'); + } + return this.sandboxId; + } + + getHostName(): string | null { + return this.hostName; + } + + getHostIp(): string | null { + return this.hostIp; + } + + getCluster(): string { + return this.cluster; + } + + getUrl(): string { + return this.url; + } + + getFs(): LinuxFileSystem { + return this.fs; + } + + getNetwork(): Network { + return this.network; + } + + getProcess(): Process { + return this.process; + } + + getRemoteUser(): LinuxRemoteUser { + return this.remoteUser; + } + + getDeploy(): Deploy { + return this.deploy; + } + + getConfig(): SandboxConfig { + return this.config; + } + + // Build headers + private buildHeaders(): Record { + const headers: Record = { + 'ROUTE-KEY': this.routeKey, + 'X-Cluster': this.cluster, + }; + + if (this.config.extraHeaders) { + Object.assign(headers, this.config.extraHeaders); + } + + this.addUserDefinedTags(headers); + + return headers; + } + + private addUserDefinedTags(headers: Record): void { + if (this.config.userId) { + headers['X-User-Id'] = this.config.userId; + } + if (this.config.experimentId) { + headers['X-Experiment-Id'] = this.config.experimentId; + } + if (this.config.namespace) { + headers['X-Namespace'] = this.config.namespace; + } + } + + // Lifecycle methods + async start(): Promise { + logger.info('Starting sandbox...'); + const url = `${this.url}/start_async`; + const headers = this.buildHeaders(); + // Use camelCase - HTTP layer will convert to snake_case + const data = { + image: this.config.image, + autoClearTime: this.config.autoClearSeconds / 60, + autoClearTimeMinutes: this.config.autoClearSeconds / 60, + startupTimeout: this.config.startupTimeout, + memory: this.config.memory, + cpus: this.config.cpus, + }; + + logger.debug(`Calling start_async API: ${url}`); + logger.debug(`Request data: ${JSON.stringify(data)}`); + + const response = await HttpUtils.post<{ sandboxId?: string; hostName?: string; hostIp?: string; code?: number }>( + url, + headers, + data + ); + + logger.debug(`Start sandbox response: ${JSON.stringify(response)}`); + + if (response.status !== 'Success') { + // Check for error code and throw appropriate exception + const code = response.result?.code; + raiseForCode(code, `Failed to start sandbox: ${JSON.stringify(response)}`); + // If no error code, throw generic error + throw new Error(`Failed to start sandbox: ${JSON.stringify(response)}`); + } + + // Response is already camelCase (converted by HTTP layer) + this.sandboxId = response.result?.sandboxId ?? null; + this.hostName = response.result?.hostName ?? null; + this.hostIp = response.result?.hostIp ?? null; + + logger.info(`Sandbox ID: ${this.sandboxId}`); + + // Wait for sandbox to be alive + // First, wait a bit for the backend to process the start request + await sleep(2000); + + const startTime = Date.now(); + const checkTimeout = 10000; // 10s timeout for each status check + const checkInterval = 3000; // 3s between checks + + while (Date.now() - startTime < this.config.startupTimeout * 1000) { + try { + logger.info(`Checking status... (elapsed: ${Math.round((Date.now() - startTime) / 1000)}s)`); + // Use Promise.race to implement timeout for status check + const statusPromise = this.getStatus(); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Status check timeout')), checkTimeout) + ); + + const status = await Promise.race([statusPromise, timeoutPromise]); + if (status && status.isAlive) { + logger.info('Sandbox is alive'); + return; + } + } catch (e) { + // Status check may fail temporarily during startup, continue waiting + logger.debug(`Status check failed (will retry): ${e}`); + } + await sleep(checkInterval); + } + + throw new InternalServerRockError( + `Failed to start sandbox within ${this.config.startupTimeout}s, sandbox: ${this.toString()}` + ); + } + + async stop(): Promise { + if (!this.sandboxId) return; + + try { + const url = `${this.url}/stop`; + const headers = this.buildHeaders(); + await HttpUtils.post(url, headers, { sandboxId: this.sandboxId }); + } catch (e) { + logger.warn(`Failed to stop sandbox, IGNORE: ${e}`); + } + } + + async isAlive(): Promise { + try { + const status = await this.getStatus(); + // Validate response with Zod schema + return IsAliveResponseSchema.parse({ + isAlive: status.isAlive, + message: status.hostName ?? '', + }); + } catch (e) { + throw new Error(`Failed to get is alive: ${e}`); + } + } + + async getStatus(): Promise { + const url = `${this.url}/get_status?sandbox_id=${this.sandboxId}`; + const headers = this.buildHeaders(); + const response = await HttpUtils.get(url, headers); + + if (response.status !== 'Success') { + const errorDetail = response.error ? `, error=${response.error}` : ''; + const code = response.result?.code; + raiseForCode(code, `Failed to get status: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + throw new Error(`Failed to get status: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + } + + // Validate response with Zod schema (similar to Python: SandboxStatusResponse(**result)) + const result = SandboxStatusResponseSchema.parse(response.result); + + // Extract additional info from response headers (backward compatible) + result.cluster = response.headers['x-rock-gateway-target-cluster'] || this.config.cluster || 'N/A'; + result.requestId = response.headers['x-request-id'] || response.headers['request-id'] || 'N/A'; + result.eagleeyeTraceid = response.headers['eagleeye-traceid'] || 'N/A'; + + return result; + } + + // Command execution + async execute(command: Command): Promise { + const url = `${this.url}/execute`; + const headers = this.buildHeaders(); + const data = { + command: command.command, + sandboxId: this.sandboxId, + timeout: command.timeout, + cwd: command.cwd, + env: command.env, + }; + + const response = await HttpUtils.post( + url, + headers, + data + ); + + if (response.status !== 'Success') { + const errorDetail = response.error ? `, error=${response.error}` : ''; + const code = response.result?.code; + raiseForCode(code, `Failed to execute command: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + throw new Error(`Failed to execute command: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + } + + // Validate response with Zod schema (similar to Python: CommandResponse(**result)) + return CommandResponseSchema.parse(response.result); + } + + // Session management + async createSession(request: CreateBashSessionRequest): Promise { + const url = `${this.url}/create_session`; + const headers = this.buildHeaders(); + const data = { + sandboxId: this.sandboxId, + ...request, + }; + + const response = await HttpUtils.post( + url, + headers, + data + ); + + if (response.status !== 'Success') { + const errorDetail = response.error ? `, error=${response.error}` : ''; + const code = response.result?.code; + raiseForCode(code, `Failed to create session: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + throw new Error(`Failed to create session: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + } + + // Validate response with Zod schema (similar to Python: CreateBashSessionResponse(**result)) + return CreateSessionResponseSchema.parse(response.result); + } + + async closeSession(request: CloseSessionRequest): Promise { + const url = `${this.url}/close_session`; + const headers = this.buildHeaders(); + const data = { + sandboxId: this.sandboxId, + ...request, + }; + + const response = await HttpUtils.post( + url, + headers, + data + ); + + if (response.status !== 'Success') { + const errorDetail = response.error ? `, error=${response.error}` : ''; + const code = response.result?.code; + raiseForCode(code, `Failed to close session: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + throw new Error(`Failed to close session: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + } + + // Validate response with Zod schema (similar to Python: CloseSessionResponse(**result)) + return CloseSessionResponseSchema.parse(response.result ?? {}); + } + + // Run command in session + async arun( + cmd: string, + options: { + session?: string; + mode?: RunModeType; + timeout?: number; + waitTimeout?: number; + waitInterval?: number; + responseLimitedBytesInNohup?: number; + ignoreOutput?: boolean; + outputFile?: string; + } = {} + ): Promise { + const { + session, + mode = 'normal', + timeout = 300, + } = options; + + const sessionName = session ?? 'default'; + + if (mode === 'normal') { + // Run command directly without pre-creating session (matches Python SDK behavior) + return this.runInSession({ command: cmd, session: sessionName, timeout }); + } + + return this.arunWithNohup(cmd, options); + } + + private async runInSession(action: { command: string; session: string; timeout?: number }): Promise { + const url = `${this.url}/run_in_session`; + const headers = this.buildHeaders(); + const data = { + actionType: 'bash', + session: action.session, + command: action.command, + sandboxId: this.sandboxId, + timeout: action.timeout, + }; + + // Convert timeout from seconds to milliseconds for axios + const timeoutMs = action.timeout ? action.timeout * 1000 : undefined; + const response = await HttpUtils.post( + url, + headers, + data, + timeoutMs + ); + + if (response.status !== 'Success') { + const errorDetail = response.error ? `, error=${response.error}` : ''; + const code = response.result?.code; + raiseForCode(code, `Failed to run in session: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + throw new Error(`Failed to run in session: status=${response.status}${errorDetail}, result=${JSON.stringify(response.result)}`); + } + + // Validate response with Zod schema (similar to Python: Observation(**result)) + return ObservationSchema.parse(response.result); + } + + private async arunWithNohup( + cmd: string, + options: { + session?: string; + waitTimeout?: number; + waitInterval?: number; + responseLimitedBytesInNohup?: number; + ignoreOutput?: boolean; + outputFile?: string; + } + ): Promise { + const { + session, + waitTimeout = 300, + waitInterval = 10, + responseLimitedBytesInNohup, + ignoreOutput = false, + outputFile, + } = options; + + const timestamp = Date.now(); + + // Only create session if not provided (matches Python SDK behavior) + let tmpSession: string; + if (session === undefined || session === null) { + tmpSession = `bash-${timestamp}`; + await this.createSession({ session: tmpSession, startupSource: [], envEnable: false }); + } else { + tmpSession = session; + } + + const tmpFile = outputFile ?? `/tmp/tmp_${timestamp}.out`; + + // Start nohup process + const nohupCommand = `nohup ${cmd} < /dev/null > ${tmpFile} 2>&1 & echo __ROCK_PID_START__$!__ROCK_PID_END__;disown`; + const response = await this.runInSession({ + command: nohupCommand, + session: tmpSession, + timeout: 30, + }); + + // Check if nohup command failed (non-zero exit code and not undefined) + if (response.exitCode !== undefined && response.exitCode !== 0) { + return response; + } + + // Extract PID + const pid = extractNohupPid(response.output); + if (!pid) { + return { + output: 'Failed to submit command, nohup failed to extract PID', + exitCode: 1, + failureReason: 'PID extraction failed', + expectString: '', + }; + } + + // Wait for process completion + const success = await this.waitForProcessCompletion(pid, tmpSession, waitTimeout, waitInterval); + + // Read output + if (ignoreOutput) { + return { + output: `Command executed in nohup mode. Output file: ${tmpFile}`, + exitCode: success ? 0 : 1, + failureReason: success ? '' : 'Process did not complete successfully', + expectString: '', + }; + } + + const readCmd = responseLimitedBytesInNohup + ? `head -c ${responseLimitedBytesInNohup} ${tmpFile}` + : `cat ${tmpFile}`; + + const outputResult = await this.runInSession({ + command: readCmd, + session: tmpSession, + }); + + return { + output: outputResult.output, + exitCode: success ? 0 : 1, + failureReason: success ? '' : 'Process did not complete successfully', + expectString: '', + }; + } + + private async waitForProcessCompletion( + pid: number, + session: string, + waitTimeout: number, + waitInterval: number + ): Promise { + const startTime = Date.now(); + const checkInterval = Math.max(1, waitInterval); + const effectiveTimeout = Math.min(checkInterval * 2, waitTimeout); + + while (Date.now() - startTime < waitTimeout * 1000) { + try { + const result = await this.runInSession({ + command: `kill -0 ${pid}`, + session, + timeout: effectiveTimeout, + }); + // If exitCode is 0, process is still running + if (result.exitCode === 0) { + await sleep(checkInterval * 1000); + } else { + // Process does not exist - completed + return true; + } + } catch { + // Process does not exist - completed + return true; + } + } + + return false; // Timeout + } + + // File operations + async writeFile(request: WriteFileRequest): Promise { + const url = `${this.url}/write_file`; + const headers = this.buildHeaders(); + const data = { + content: request.content, + path: request.path, + sandboxId: this.sandboxId, + }; + + const response = await HttpUtils.post(url, headers, data); + + if (response.status !== 'Success') { + return { success: false, message: `Failed to write file ${request.path}` }; + } + + return { success: true, message: `Successfully write content to file ${request.path}` }; + } + + async readFile(request: ReadFileRequest): Promise { + const url = `${this.url}/read_file`; + const headers = this.buildHeaders(); + const data = { + path: request.path, + encoding: request.encoding, + errors: request.errors, + sandboxId: this.sandboxId, + }; + + const response = await HttpUtils.post<{ content: string }>( + url, + headers, + data + ); + + // Validate response with Zod schema (similar to Python: ReadFileResponse(**result)) + return ReadFileResponseSchema.parse(response.result ?? {}); + } + + // Upload + async upload(request: UploadRequest): Promise { + return this.uploadByPath(request.sourcePath, request.targetPath); + } + + async uploadByPath(sourcePath: string, targetPath: string): Promise { + const url = `${this.url}/upload`; + const headers = this.buildHeaders(); + + try { + const fs = await import('fs/promises'); + + // Use async access instead of sync existsSync + try { + await fs.access(sourcePath); + } catch { + return { success: false, message: `File not found: ${sourcePath}` }; + } + + // Use async readFile instead of sync readFileSync + const fileBuffer = await fs.readFile(sourcePath); + const fileName = sourcePath.split('/').pop() ?? 'file'; + + const response = await HttpUtils.postMultipart( + url, + headers, + { targetPath: targetPath, sandboxId: this.sandboxId ?? '' }, + { file: [fileName, fileBuffer, 'application/octet-stream'] } + ); + + if (response.status !== 'Success') { + return { success: false, message: 'Upload failed' }; + } + + return { success: true, message: `Successfully uploaded file ${fileName} to ${targetPath}` }; + } catch (e) { + return { success: false, message: `Upload failed: ${e}` }; + } + } + + // Close + override async close(): Promise { + await this.stop(); + } + + override toString(): string { + return `Sandbox(sandboxId=${this.sandboxId}, hostName=${this.hostName}, image=${this.config.image}, cluster=${this.cluster})`; + } +} + +/** + * SandboxGroup - Group of sandboxes with concurrent operations + */ +export class SandboxGroup { + private config: SandboxGroupConfig; + private sandboxList: Sandbox[]; + + constructor(config: Partial = {}) { + this.config = createSandboxGroupConfig(config); + this.sandboxList = Array.from( + { length: this.config.size }, + () => new Sandbox(this.config) + ); + } + + getSandboxList(): Sandbox[] { + return this.sandboxList; + } + + async start(): Promise { + const concurrency = this.config.startConcurrency; + const retryTimes = this.config.startRetryTimes; + + const startSandbox = async (index: number, sandbox: Sandbox): Promise => { + logger.info(`Starting sandbox ${index} with ${sandbox.getConfig().image}...`); + + for (let attempt = 0; attempt < retryTimes; attempt++) { + try { + await sandbox.start(); + return; + } catch (e) { + if (attempt === retryTimes - 1) { + logger.error(`Failed to start sandbox after ${retryTimes} attempts: ${e}`); + throw e; + } + logger.warn(`Failed to start sandbox (attempt ${attempt + 1}/${retryTimes}): ${e}, retrying...`); + await sleep(1000); + } + } + }; + + // Start with concurrency limit + for (let i = 0; i < this.sandboxList.length; i += concurrency) { + const batch = this.sandboxList.slice(i, i + concurrency); + const promises = batch.map((sandbox, idx) => startSandbox(i + idx, sandbox)); + await Promise.all(promises); + } + + logger.info(`Successfully started ${this.sandboxList.length} sandboxes`); + } + + async stop(): Promise { + const promises = this.sandboxList.map((sandbox) => sandbox.stop()); + await Promise.allSettled(promises); + logger.info(`Stopped ${this.sandboxList.length} sandboxes`); + } +} \ No newline at end of file diff --git a/rock/ts-sdk/src/sandbox/config.test.ts b/rock/ts-sdk/src/sandbox/config.test.ts new file mode 100644 index 000000000..6c531f095 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/config.test.ts @@ -0,0 +1,155 @@ +/** + * Tests for Sandbox Config + */ + +import { + SandboxConfigSchema, + SandboxGroupConfigSchema, + createSandboxConfig, + createSandboxGroupConfig, +} from './config.js'; +import { envVars } from '../env_vars.js'; + +describe('SandboxConfigSchema', () => { + test('should use default values', () => { + const config = SandboxConfigSchema.parse({}); + expect(config.image).toBe('python:3.11'); + expect(config.autoClearSeconds).toBe(300); + expect(config.memory).toBe('8g'); + expect(config.cpus).toBe(2); + expect(config.cluster).toBe('zb'); + }); + + test('should allow custom values', () => { + const config = SandboxConfigSchema.parse({ + image: 'node:18', + memory: '16g', + cpus: 4, + cluster: 'custom', + }); + expect(config.image).toBe('node:18'); + expect(config.memory).toBe('16g'); + expect(config.cpus).toBe(4); + expect(config.cluster).toBe('custom'); + }); + + test('should allow extra headers', () => { + const config = SandboxConfigSchema.parse({ + extraHeaders: { 'X-Custom': 'value' }, + }); + expect(config.extraHeaders).toEqual({ 'X-Custom': 'value' }); + }); +}); + +describe('SandboxGroupConfigSchema', () => { + test('should use default values', () => { + const config = SandboxGroupConfigSchema.parse({}); + expect(config.size).toBe(2); + expect(config.startConcurrency).toBe(2); + expect(config.startRetryTimes).toBe(3); + }); + + test('should extend SandboxConfig', () => { + const config = SandboxGroupConfigSchema.parse({ + image: 'python:3.12', + size: 5, + }); + expect(config.image).toBe('python:3.12'); + expect(config.size).toBe(5); + }); +}); + +describe('createSandboxConfig', () => { + test('should create config with defaults', () => { + const config = createSandboxConfig(); + expect(config.image).toBe('python:3.11'); + expect(config.cluster).toBe('zb'); + }); + + test('should merge partial config', () => { + const config = createSandboxConfig({ image: 'custom:latest' }); + expect(config.image).toBe('custom:latest'); + expect(config.cluster).toBe('zb'); + }); +}); + +describe('createSandboxGroupConfig', () => { + test('should create group config with defaults', () => { + const config = createSandboxGroupConfig(); + expect(config.size).toBe(2); + expect(config.startConcurrency).toBe(2); + }); + + test('should merge partial config', () => { + const config = createSandboxGroupConfig({ size: 10, startConcurrency: 5 }); + expect(config.size).toBe(10); + expect(config.startConcurrency).toBe(5); + }); +}); + +describe('Config uses envVars (not hardcoded)', () => { + // These tests verify that config schemas read defaults from envVars + // rather than using hardcoded values. + // This enables users to override defaults via environment variables. + + test('SandboxConfigSchema should use envVars for all default values', () => { + const config = SandboxConfigSchema.parse({}); + // These assertions prove that config reads from envVars, not hardcoded strings + expect(config.image).toBe(envVars.ROCK_DEFAULT_IMAGE); + expect(config.memory).toBe(envVars.ROCK_DEFAULT_MEMORY); + expect(config.cpus).toBe(envVars.ROCK_DEFAULT_CPUS); + expect(config.cluster).toBe(envVars.ROCK_DEFAULT_CLUSTER); + expect(config.autoClearSeconds).toBe(envVars.ROCK_DEFAULT_AUTO_CLEAR_SECONDS); + }); + + test('SandboxGroupConfigSchema should use envVars for group defaults', () => { + const config = SandboxGroupConfigSchema.parse({}); + expect(config.size).toBe(envVars.ROCK_DEFAULT_GROUP_SIZE); + expect(config.startConcurrency).toBe(envVars.ROCK_DEFAULT_START_CONCURRENCY); + expect(config.startRetryTimes).toBe(envVars.ROCK_DEFAULT_START_RETRY_TIMES); + }); +}); + +describe('Config lazy evaluation of env vars', () => { + // This test verifies that env var defaults are evaluated lazily (at parse time) + // not eagerly (at module load time). This allows env vars to be changed + // dynamically and have the new values reflected in schema defaults. + + test('should use current env var value at parse time, not captured at import', () => { + // Save original values + const originalBaseUrl = process.env.ROCK_BASE_URL; + const originalImage = process.env.ROCK_DEFAULT_IMAGE; + + try { + // Set initial values + process.env.ROCK_BASE_URL = 'http://original:8080'; + process.env.ROCK_DEFAULT_IMAGE = 'original:latest'; + + // Parse config - should use current env values + const config1 = createSandboxConfig({}); + expect(config1.baseUrl).toBe('http://original:8080'); + expect(config1.image).toBe('original:latest'); + + // Change env vars AFTER module is loaded + process.env.ROCK_BASE_URL = 'http://changed:9090'; + process.env.ROCK_DEFAULT_IMAGE = 'changed:latest'; + + // Parse again - should use NEW env values, not stale captured values + const config2 = createSandboxConfig({}); + expect(config2.baseUrl).toBe('http://changed:9090'); + expect(config2.image).toBe('changed:latest'); + } finally { + // Restore original values + if (originalBaseUrl === undefined) { + delete process.env.ROCK_BASE_URL; + } else { + process.env.ROCK_BASE_URL = originalBaseUrl; + } + if (originalImage === undefined) { + delete process.env.ROCK_DEFAULT_IMAGE; + } else { + process.env.ROCK_DEFAULT_IMAGE = originalImage; + } + } + }); +}); diff --git a/rock/ts-sdk/src/sandbox/config.ts b/rock/ts-sdk/src/sandbox/config.ts new file mode 100644 index 000000000..486af15fe --- /dev/null +++ b/rock/ts-sdk/src/sandbox/config.ts @@ -0,0 +1,64 @@ +/** + * Sandbox configuration + */ + +import { z } from 'zod'; +import { envVars } from '../env_vars.js'; + +/** + * Base configuration schema + */ +export const BaseConfigSchema = z.object({ + baseUrl: z.string().default(() => envVars.ROCK_BASE_URL), + xrlAuthorization: z.string().optional(), + extraHeaders: z.record(z.string()).default({}), +}); + +export type BaseConfig = z.infer; + +/** + * Sandbox configuration schema + */ +export const SandboxConfigSchema = BaseConfigSchema.extend({ + image: z.string().default(() => envVars.ROCK_DEFAULT_IMAGE), + autoClearSeconds: z.number().default(() => envVars.ROCK_DEFAULT_AUTO_CLEAR_SECONDS), + routeKey: z.string().optional(), + startupTimeout: z.number().default(() => envVars.ROCK_SANDBOX_STARTUP_TIMEOUT_SECONDS), + memory: z.string().default(() => envVars.ROCK_DEFAULT_MEMORY), + cpus: z.number().default(() => envVars.ROCK_DEFAULT_CPUS), + userId: z.string().optional(), + experimentId: z.string().optional(), + cluster: z.string().default(() => envVars.ROCK_DEFAULT_CLUSTER), + namespace: z.string().optional(), +}); + +export type SandboxConfig = z.infer; + +/** + * Sandbox group configuration schema + */ +export const SandboxGroupConfigSchema = SandboxConfigSchema.extend({ + size: z.number().default(() => envVars.ROCK_DEFAULT_GROUP_SIZE), + startConcurrency: z.number().default(() => envVars.ROCK_DEFAULT_START_CONCURRENCY), + startRetryTimes: z.number().default(() => envVars.ROCK_DEFAULT_START_RETRY_TIMES), +}); + +export type SandboxGroupConfig = z.infer; + +/** + * Create sandbox config with defaults + */ +export function createSandboxConfig( + config?: Partial +): SandboxConfig { + return SandboxConfigSchema.parse(config ?? {}); +} + +/** + * Create sandbox group config with defaults + */ +export function createSandboxGroupConfig( + config?: Partial +): SandboxGroupConfig { + return SandboxGroupConfigSchema.parse(config ?? {}); +} diff --git a/rock/ts-sdk/src/sandbox/deploy.ts b/rock/ts-sdk/src/sandbox/deploy.ts new file mode 100644 index 000000000..b79b66b43 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/deploy.ts @@ -0,0 +1,101 @@ +/** + * Deploy - Sandbox resource deployment manager + */ + +import { existsSync, statSync } from 'fs'; +import { resolve } from 'path'; +import { randomUUID } from 'crypto'; +import { initLogger } from '../logger.js'; +import type { Sandbox } from './client.js'; + +const logger = initLogger('rock.sandbox.deploy'); + +/** + * Deploy - Manages deployment of local directories to sandbox + */ +export class Deploy { + private sandbox: Sandbox; + private workingDir: string | null = null; + + constructor(sandbox: Sandbox) { + this.sandbox = sandbox; + } + + /** + * Get the current working directory + */ + getWorkingDir(): string | null { + return this.workingDir; + } + + /** + * Deploy local directory to sandbox + * + * @param localPath - Local directory path + * @param targetPath - Target path in sandbox (optional) + * @returns The target path in sandbox + */ + async deployWorkingDir( + localPath: string, + targetPath?: string + ): Promise { + const localAbs = resolve(localPath); + + // Validate local path + if (!existsSync(localAbs)) { + throw new Error(`local_path not found: ${localAbs}`); + } + const stats = statSync(localAbs); + if (!stats.isDirectory()) { + throw new Error(`local_path must be a directory: ${localAbs}`); + } + + // Determine target path + const target = targetPath ?? `/tmp/rock_workdir_${randomUUID().replace(/-/g, '')}`; + + const sandboxId = this.sandbox.getSandboxId(); + logger.info(`[${sandboxId}] Deploying working_dir: ${localAbs} -> ${target}`); + + // Upload directory + const uploadResult = await this.sandbox.getFs().uploadDir(localAbs, target); + if (uploadResult.exitCode !== 0) { + throw new Error(`Failed to upload directory: ${uploadResult.failureReason}`); + } + + // Update working directory + this.workingDir = target; + logger.info(`[${sandboxId}] working_dir deployed: ${target}`); + + return target; + } + + /** + * Format command template supporting ${} and <<>> syntax + * + * @param template - Template string with placeholders + * @param kwargs - Additional substitution variables + * @returns Formatted string + */ + format(template: string, kwargs: Record = {}): string { + // Build substitution map + const subs: Record = { + ...kwargs, + ...(this.workingDir ? { working_dir: this.workingDir } : {}), + }; + + // Replace <> with ${key} for template substitution + let result = template; + for (const key of Object.keys(subs)) { + result = result.replace(new RegExp(`<<${key}>>`, 'g'), `\${${key}}`); + } + + // Perform substitution + for (const [key, value] of Object.entries(subs)) { + if (value !== undefined) { + result = result.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value); + } + } + + return result; + } +} diff --git a/rock/ts-sdk/src/sandbox/file_system.test.ts b/rock/ts-sdk/src/sandbox/file_system.test.ts new file mode 100644 index 000000000..965828497 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/file_system.test.ts @@ -0,0 +1,208 @@ +/** + * Tests for FileSystem - Injection Protection + */ + +import { LinuxFileSystem } from './file_system.js'; +import type { AbstractSandbox } from './client.js'; +import type { Observation, CommandResponse } from '../types/responses.js'; +import { existsSync, statSync } from 'fs'; + +// Mock fs module +jest.mock('fs', () => ({ + existsSync: jest.fn(), + statSync: jest.fn(), + mkdtempSync: jest.fn().mockReturnValue('/tmp/rock-upload-test'), + rmSync: jest.fn(), +})); + +// Mock child_process +jest.mock('child_process', () => ({ + spawn: jest.fn().mockImplementation(() => ({ + stderr: { on: jest.fn() }, + on: jest.fn((event, callback) => { + if (event === 'close') callback(0); + }), + })), +})); + +// Mock sandbox factory +function createMockSandbox(): jest.Mocked { + return { + getSandboxId: jest.fn().mockReturnValue('test-sandbox'), + execute: jest.fn().mockResolvedValue({ + stdout: '', + stderr: '', + exitCode: 0, + } as CommandResponse), + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + } as Observation), + createSession: jest.fn().mockResolvedValue(undefined), + upload: jest.fn().mockResolvedValue({ success: true, message: '' }), + } as unknown as jest.Mocked; +} + +describe('FileSystem', () => { + describe('uploadDir injection protection', () => { + let fs: LinuxFileSystem; + let mockSandbox: jest.Mocked; + + beforeEach(() => { + mockSandbox = createMockSandbox(); + fs = new LinuxFileSystem(mockSandbox); + // Mock source directory exists + (existsSync as jest.Mock).mockReturnValue(true); + (statSync as jest.Mock).mockReturnValue({ isDirectory: () => true }); + }); + + test('should reject path traversal attempts in targetDir', async () => { + const result = await fs.uploadDir('/tmp/source', '/tmp/../../../etc/passwd'); + // Should either reject or sanitize the path + expect(result.exitCode).toBe(1); + expect(result.failureReason).toMatch(/invalid|traversal|not allowed|\.\./i); + }); + + test('should reject paths containing shell metacharacters', async () => { + const result = await fs.uploadDir('/tmp/source', "/tmp/dir$(rm -rf /)"); + expect(result.exitCode).toBe(1); + expect(result.failureReason).toMatch(/invalid|character|not allowed|\$|forbidden/i); + }); + + test('should reject paths containing backticks', async () => { + const result = await fs.uploadDir('/tmp/source', '/tmp/`whoami`'); + expect(result.exitCode).toBe(1); + expect(result.failureReason).toMatch(/invalid|character|not allowed|`|forbidden/i); + }); + + test('should reject paths containing semicolons', async () => { + const result = await fs.uploadDir('/tmp/source', '/tmp/dir; rm -rf /'); + expect(result.exitCode).toBe(1); + expect(result.failureReason).toMatch(/invalid|character|not allowed|forbidden/i); + }); + + test('should reject paths containing pipe characters', async () => { + const result = await fs.uploadDir('/tmp/source', '/tmp/dir | cat /etc/passwd'); + expect(result.exitCode).toBe(1); + expect(result.failureReason).toMatch(/invalid|character|not allowed|forbidden/i); + }); + + test('should reject paths containing && or ||', async () => { + const result = await fs.uploadDir('/tmp/source', '/tmp/dir && rm -rf /'); + expect(result.exitCode).toBe(1); + expect(result.failureReason).toMatch(/invalid|character|not allowed|forbidden/i); + }); + + test('should accept valid absolute paths', async () => { + const result = await fs.uploadDir('/tmp/source', '/tmp/valid/path'); + // Should succeed (mocked source exists) + expect(result.exitCode).toBe(0); + }); + }); + + describe('chown injection protection', () => { + let fs: LinuxFileSystem; + let mockSandbox: jest.Mocked; + + beforeEach(() => { + mockSandbox = createMockSandbox(); + fs = new LinuxFileSystem(mockSandbox); + }); + + test('should reject remoteUser with shell metacharacters', async () => { + await expect(fs.chown({ + paths: ['/tmp/test'], + recursive: false, + remoteUser: 'root; rm -rf /', + })).rejects.toThrow(/invalid|character|not allowed/i); + }); + + test('should reject remoteUser starting with dash', async () => { + await expect(fs.chown({ + paths: ['/tmp/test'], + recursive: false, + remoteUser: '-rf', + })).rejects.toThrow(/invalid|character|not allowed/i); + }); + + test('should reject paths containing shell metacharacters', async () => { + await expect(fs.chown({ + paths: ['/tmp/test$(whoami)'], + recursive: false, + remoteUser: 'root', + })).rejects.toThrow(/invalid|character|not allowed/i); + }); + + test('should accept valid usernames', async () => { + await fs.chown({ + paths: ['/tmp/test'], + recursive: false, + remoteUser: 'validuser', + }); + expect(mockSandbox.execute).toHaveBeenCalled(); + }); + + test('should accept valid usernames with underscore', async () => { + await fs.chown({ + paths: ['/tmp/test'], + recursive: false, + remoteUser: 'valid_user', + }); + expect(mockSandbox.execute).toHaveBeenCalled(); + }); + }); + + describe('chmod injection protection', () => { + let fs: LinuxFileSystem; + let mockSandbox: jest.Mocked; + + beforeEach(() => { + mockSandbox = createMockSandbox(); + fs = new LinuxFileSystem(mockSandbox); + }); + + test('should reject invalid mode format', async () => { + await expect(fs.chmod({ + paths: ['/tmp/test'], + recursive: false, + mode: '755; rm -rf /', + })).rejects.toThrow(/invalid|mode/i); + }); + + test('should reject mode with non-octal characters', async () => { + await expect(fs.chmod({ + paths: ['/tmp/test'], + recursive: false, + mode: 'abc', + })).rejects.toThrow(/invalid|mode/i); + }); + + test('should reject paths containing shell metacharacters', async () => { + await expect(fs.chmod({ + paths: ['/tmp/test$(id)'], + recursive: false, + mode: '755', + })).rejects.toThrow(/invalid|character|not allowed/i); + }); + + test('should accept valid octal mode', async () => { + await fs.chmod({ + paths: ['/tmp/test'], + recursive: false, + mode: '755', + }); + expect(mockSandbox.execute).toHaveBeenCalled(); + }); + + test('should accept valid symbolic mode', async () => { + await fs.chmod({ + paths: ['/tmp/test'], + recursive: false, + mode: 'u+x', + }); + expect(mockSandbox.execute).toHaveBeenCalled(); + }); + }); +}); diff --git a/rock/ts-sdk/src/sandbox/file_system.ts b/rock/ts-sdk/src/sandbox/file_system.ts new file mode 100644 index 000000000..83918ded6 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/file_system.ts @@ -0,0 +1,272 @@ +/** + * FileSystem - File system operations for sandbox + */ + +import { existsSync, statSync, mkdtempSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join, resolve, basename } from 'path'; +import { initLogger } from '../logger.js'; +import type { Observation, CommandResponse } from '../types/responses.js'; +import type { ChownRequest, ChmodRequest } from '../types/requests.js'; +import type { AbstractSandbox } from './client.js'; +import { RunMode } from '../common/constants.js'; +import { validatePath, validateUsername, validateChmodMode, shellQuote } from '../utils/shell.js'; + +const logger = initLogger('rock.sandbox.fs'); + +/** + * Create a tar.gz archive of a directory (simplified implementation) + * Uses shell command `tar` for reliability + */ +async function createTarGz(sourceDir: string, outputPath: string): Promise { + const { spawn } = await import('child_process'); + + return new Promise((resolve, reject) => { + const tar = spawn('tar', ['-czf', outputPath, '-C', sourceDir, '.']); + + let stderr = ''; + tar.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + tar.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`tar command failed with code ${code}: ${stderr}`)); + } + }); + + tar.on('error', (err) => { + reject(err); + }); + }); +} + +/** + * Abstract file system interface + */ +export abstract class FileSystem { + protected sandbox: AbstractSandbox; + + constructor(sandbox: AbstractSandbox) { + this.sandbox = sandbox; + } + + abstract chown(request: ChownRequest): Promise<{ success: boolean; message: string }>; + abstract chmod(request: ChmodRequest): Promise<{ success: boolean; message: string }>; + abstract uploadDir( + sourceDir: string, + targetDir: string, + extractTimeout?: number + ): Promise; +} + +/** + * Linux file system implementation + */ +export class LinuxFileSystem extends FileSystem { + constructor(sandbox: AbstractSandbox) { + super(sandbox); + } + + async chown(request: ChownRequest): Promise<{ success: boolean; message: string }> { + const { paths, recursive, remoteUser } = request; + + if (!paths || paths.length === 0) { + throw new Error('paths is empty'); + } + + // Validate username to prevent injection + validateUsername(remoteUser); + + // Validate all paths + for (const p of paths) { + validatePath(p); + } + + const command = ['chown']; + if (recursive) { + command.push('-R'); + } + command.push(`${remoteUser}:${remoteUser}`, ...paths); + + logger.info(`chown command: ${command.join(' ')}`); + + const response: CommandResponse = await this.sandbox.execute({ command, timeout: 300 }); + if (response.exitCode !== 0) { + return { success: false, message: JSON.stringify(response) }; + } + return { success: true, message: JSON.stringify(response) }; + } + + async chmod(request: ChmodRequest): Promise<{ success: boolean; message: string }> { + const { paths, recursive, mode } = request; + + if (!paths || paths.length === 0) { + throw new Error('paths is empty'); + } + + // Validate mode to prevent injection + validateChmodMode(mode); + + // Validate all paths + for (const p of paths) { + validatePath(p); + } + + const command = ['chmod']; + if (recursive) { + command.push('-R'); + } + command.push(mode, ...paths); + + logger.info(`chmod command: ${command.join(' ')}`); + const response: CommandResponse = await this.sandbox.execute({ command, timeout: 300 }); + if (response.exitCode !== 0) { + return { success: false, message: JSON.stringify(response) }; + } + return { success: true, message: JSON.stringify(response) }; + } + + async uploadDir( + sourceDir: string, + targetDir: string, + extractTimeout: number = 600 + ): Promise { + let localTarPath: string | null = null; + let remoteTarPath: string | null = null; + let session: string | null = null; + + try { + // Validate source directory + const src = resolve(sourceDir); + if (!existsSync(src)) { + return { + output: '', + exitCode: 1, + failureReason: `source_dir not found: ${src}`, + expectString: '', + }; + } + const stats = statSync(src); + if (!stats.isDirectory()) { + return { + output: '', + exitCode: 1, + failureReason: `source_dir must be a directory: ${src}`, + expectString: '', + }; + } + + // Validate target directory for security + try { + validatePath(targetDir); + } catch (e) { + return { + output: '', + exitCode: 1, + failureReason: e instanceof Error ? e.message : 'Invalid target directory', + expectString: '', + }; + } + + // Generate unique names using timestamp + const ts = Date.now().toString(); + const tmpDir = mkdtempSync(join(tmpdir(), 'rock-upload-')); + localTarPath = join(tmpDir, `rock_upload_${ts}.tar.gz`); + remoteTarPath = `/tmp/rock_upload_${ts}.tar.gz`; + session = `bash-${ts}`; + + logger.info(`uploadDir: ${src} -> ${targetDir}`); + + // Create bash session + await this.sandbox.createSession({ session, startupSource: [], envEnable: false }); + + // Check tar exists in sandbox + const checkResult = await this.sandbox.arun('command -v tar >/dev/null 2>&1', { + session, + mode: RunMode.NORMAL, + }); + if (checkResult.exitCode !== 0) { + return { + output: '', + exitCode: 1, + failureReason: 'sandbox has no tar command; cannot extract tarball', + expectString: '', + }; + } + + // Pack locally + try { + await createTarGz(src, localTarPath); + } catch (e) { + throw new Error(`tar pack failed: ${e}`); + } + + // Upload tarball + const uploadResponse = await this.sandbox.upload({ + sourcePath: localTarPath, + targetPath: remoteTarPath, + }); + if (!uploadResponse.success) { + return { + output: '', + exitCode: 1, + failureReason: `tar upload failed: ${uploadResponse.message}`, + expectString: '', + }; + } + + // Extract in sandbox using properly escaped paths + const escapedTargetDir = shellQuote(targetDir); + const escapedRemoteTar = shellQuote(remoteTarPath); + const extractCmd = `rm -rf ${escapedTargetDir} && mkdir -p ${escapedTargetDir} && tar -xzf ${escapedRemoteTar} -C ${escapedTargetDir}`; + const extractResult = await this.sandbox.arun(`bash -c ${shellQuote(extractCmd)}`, { + session, + mode: RunMode.NOHUP, + waitTimeout: extractTimeout, + }); + + if (extractResult.exitCode !== 0) { + return { + output: '', + exitCode: 1, + failureReason: `tar extract failed: ${extractResult.output}`, + expectString: '', + }; + } + + // Cleanup remote tarball + try { + await this.sandbox.execute({ command: ['rm', '-f', remoteTarPath], timeout: 30 }); + } catch { + // Ignore cleanup errors + } + + return { + output: `uploaded ${src} -> ${targetDir} via tar`, + exitCode: 0, + failureReason: '', + expectString: '', + }; + } catch (e) { + return { + output: '', + exitCode: 1, + failureReason: `upload_dir unexpected error: ${e}`, + expectString: '', + }; + } finally { + // Cleanup local tarball and temp directory + try { + if (localTarPath) { + const tmpDir = basename(localTarPath).replace(/\.tar\.gz$/, ''); + rmSync(join(tmpdir(), `rock-upload-${tmpDir.split('_').pop()}`), { recursive: true, force: true }); + } + } catch { + // Ignore cleanup errors + } + } + } +} diff --git a/rock/ts-sdk/src/sandbox/index.ts b/rock/ts-sdk/src/sandbox/index.ts new file mode 100644 index 000000000..a2280e0ac --- /dev/null +++ b/rock/ts-sdk/src/sandbox/index.ts @@ -0,0 +1,17 @@ +/** + * Sandbox module - Core sandbox management + */ + +export * from './client.js'; +export * from './config.js'; +export * from './deploy.js'; +export * from './file_system.js'; +export * from './network.js'; +export * from './process.js'; +export * from './remote_user.js'; +export * from './utils.js'; + +// Re-export types from their new locations +export { SpeedupType } from './network.js'; +export type { RunModeType } from '../common/constants.js'; +export { RunMode } from '../common/constants.js'; diff --git a/rock/ts-sdk/src/sandbox/model_service/base.test.ts b/rock/ts-sdk/src/sandbox/model_service/base.test.ts new file mode 100644 index 000000000..bdc4fae27 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/model_service/base.test.ts @@ -0,0 +1,178 @@ +/** + * ModelService tests + */ + +import { + ModelServiceConfig, + ModelServiceConfigSchema, + ModelService, +} from './base.js'; + +describe('ModelServiceConfig', () => { + describe('ModelServiceConfigSchema', () => { + it('should parse valid config with default values', () => { + const result = ModelServiceConfigSchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.enabled).toBe(false); + expect(result.data.type).toBe('local'); + expect(result.data.installTimeout).toBe(300); + expect(result.data.loggingPath).toBe('/data/logs'); + expect(result.data.loggingFileName).toBe('model_service.log'); + } + }); + + it('should parse config with enabled=true', () => { + const result = ModelServiceConfigSchema.safeParse({ enabled: true }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.enabled).toBe(true); + } + }); + + it('should parse config with custom type', () => { + const result = ModelServiceConfigSchema.safeParse({ type: 'proxy' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('proxy'); + } + }); + + it('should parse config with custom commands', () => { + const result = ModelServiceConfigSchema.safeParse({ + startCmd: 'custom start command', + stopCmd: 'custom stop command', + watchAgentCmd: 'custom watch command', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.startCmd).toBe('custom start command'); + expect(result.data.stopCmd).toBe('custom stop command'); + expect(result.data.watchAgentCmd).toBe('custom watch command'); + } + }); + + it('should reject invalid installTimeout (non-positive)', () => { + const result = ModelServiceConfigSchema.safeParse({ installTimeout: 0 }); + expect(result.success).toBe(false); + }); + + it('should reject invalid installTimeout (negative)', () => { + const result = ModelServiceConfigSchema.safeParse({ installTimeout: -1 }); + expect(result.success).toBe(false); + }); + }); + + describe('ModelServiceConfig type', () => { + it('should be inferred from schema', () => { + const config = ModelServiceConfigSchema.parse({}); + expect(config.type).toBe('local'); + }); + }); +}); + +describe('ModelService', () => { + describe('constructor', () => { + it('should create instance with default config', () => { + const config = ModelServiceConfigSchema.parse({ enabled: true }); + + // Create a minimal mock sandbox + const mockSandbox = { + sandboxId: 'test-sandbox-id', + runtimeEnvs: {}, + createSession: jest.fn().mockResolvedValue({}), + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + }), + }; + + const service = new ModelService( + mockSandbox as unknown as ConstructorParameters[0], + config + ); + + expect(service.isInstalled).toBe(false); + expect(service.isStarted).toBe(false); + }); + }); + + describe('start', () => { + it('should throw error if not installed', async () => { + const config = ModelServiceConfigSchema.parse({ enabled: true }); + + const mockSandbox = { + sandboxId: 'test-sandbox-id', + runtimeEnvs: {}, + createSession: jest.fn().mockResolvedValue({}), + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + }), + }; + + const service = new ModelService( + mockSandbox as unknown as ConstructorParameters[0], + config + ); + + await expect(service.start()).rejects.toThrow('not been installed'); + }); + }); + + describe('watchAgent', () => { + it('should throw error if not started', async () => { + const config = ModelServiceConfigSchema.parse({ enabled: true }); + + const mockSandbox = { + sandboxId: 'test-sandbox-id', + runtimeEnvs: {}, + createSession: jest.fn().mockResolvedValue({}), + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + }), + }; + + const service = new ModelService( + mockSandbox as unknown as ConstructorParameters[0], + config + ); + + await expect(service.watchAgent('12345')).rejects.toThrow('not started'); + }); + }); + + describe('stop', () => { + it('should skip stop if not started', async () => { + const config = ModelServiceConfigSchema.parse({ enabled: true }); + + const mockSandbox = { + sandboxId: 'test-sandbox-id', + runtimeEnvs: {}, + createSession: jest.fn().mockResolvedValue({}), + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + }), + }; + + const service = new ModelService( + mockSandbox as unknown as ConstructorParameters[0], + config + ); + + // Should not throw, just skip + await service.stop(); + expect(service.isStarted).toBe(false); + }); + }); +}); diff --git a/rock/ts-sdk/src/sandbox/model_service/base.ts b/rock/ts-sdk/src/sandbox/model_service/base.ts new file mode 100644 index 000000000..ffebc1992 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/model_service/base.ts @@ -0,0 +1,289 @@ +/** + * ModelService - manages model service installation and lifecycle in sandbox + * + * This module provides functionality to install, start, stop, and manage + * the model service within a sandboxed environment. + */ + +import { z } from 'zod'; +import { PythonRuntimeEnv, PythonRuntimeEnvConfigSchema } from '../runtime_env/python_runtime_env.js'; +import { type RuntimeEnvId, type SandboxLike } from '../runtime_env/base.js'; +import { envVars } from '../../env_vars.js'; +import { initLogger } from '../../logger.js'; + +const logger = initLogger('rock.model_service'); + +/** + * ModelService configuration schema + */ +export const ModelServiceConfigSchema = z.object({ + /** Whether to enable model service */ + enabled: z.boolean().default(false), + + /** Type of model service to start */ + type: z.string().default('local'), + + /** Command to install model service package */ + installCmd: z.string().default(envVars.ROCK_MODEL_SERVICE_INSTALL_CMD), + + /** Timeout for model service installation in seconds */ + installTimeout: z.number().positive().default(300), + + /** Runtime environment configuration for the model service */ + runtimeEnvConfig: PythonRuntimeEnvConfigSchema.default({ type: 'python', version: 'default' }), + + /** Command to start model service with type placeholder */ + startCmd: z.string().default('rock model-service start --type ${type}'), + + /** Command to stop model service */ + stopCmd: z.string().default('rock model-service stop'), + + /** Command to create Rock config file */ + configIniCmd: z.string().default('mkdir -p ~/.rock && touch ~/.rock/config.ini'), + + /** Command to watch agent with pid placeholder */ + watchAgentCmd: z.string().default('rock model-service watch-agent --pid ${pid}'), + + /** Command to anti-call LLM with index and response_payload placeholders */ + antiCallLlmCmd: z.string().default( + 'rock model-service anti-call-llm --index ${index} --response ${response_payload}' + ), + + /** Command to anti-call LLM with only index placeholder */ + antiCallLlmCmdNoResponse: z.string().default('rock model-service anti-call-llm --index ${index}'), + + /** Path for logging directory */ + loggingPath: z.string().default('/data/logs'), + + /** Name of the log file */ + loggingFileName: z.string().default('model_service.log'), +}); + +/** + * ModelService configuration type + */ +export type ModelServiceConfig = z.infer; + +/** + * ModelService - manages model service installation and lifecycle in sandbox + * + * This class handles model service installation, startup, and agent management + * within a sandboxed environment. + * + * Note: + * Caller is responsible for ensuring proper sequencing of install/start/stop operations. + */ +export class ModelService { + private _sandbox: SandboxLike; + private _config: ModelServiceConfig; + private _runtimeEnv: PythonRuntimeEnv | null = null; + + private _isInstalled = false; + private _isStarted = false; + + constructor( + sandbox: SandboxLike & { runtimeEnvs: Record }, + config: ModelServiceConfig + ) { + this._sandbox = sandbox; + this._config = config; + } + + /** Whether the model service has been installed */ + get isInstalled(): boolean { + return this._isInstalled; + } + + /** Whether the model service has been started */ + get isStarted(): boolean { + return this._isStarted; + } + + /** + * Install model service in the sandbox. + * + * Performs the following installation steps: + * 1. Create and initialize Python runtime environment (via RuntimeEnv). + * 2. Install model service package. + */ + async install(): Promise { + // Validate runtime config is Python + if (this._config.runtimeEnvConfig.type !== 'python') { + throw new Error('ModelService requires a Python runtime environment'); + } + + // Parse and validate the runtime config + const runtimeConfigResult = PythonRuntimeEnvConfigSchema.safeParse(this._config.runtimeEnvConfig); + if (!runtimeConfigResult.success) { + throw new Error(`Invalid runtime config: ${runtimeConfigResult.error.message}`); + } + + // Create Python runtime environment + this._runtimeEnv = new PythonRuntimeEnv( + this._sandbox as SandboxLike & { runtimeEnvs: Record }, + runtimeConfigResult.data + ); + + // Initialize the runtime (installs Python) + await this._runtimeEnv.init(); + + // Create rock config + await this._createRockConfig(); + + // Install model service package + await this._installModelService(); + + this._isInstalled = true; + } + + private async _createRockConfig(): Promise { + if (!this._runtimeEnv) { + throw new Error('Runtime environment not initialized'); + } + await this._runtimeEnv.run(this._config.configIniCmd); + } + + private async _installModelService(): Promise { + if (!this._runtimeEnv) { + throw new Error('Runtime environment not initialized'); + } + + const installCmd = `cd ${this._runtimeEnv.workdir} && ${this._config.installCmd}`; + await this._runtimeEnv.run(installCmd, { + waitTimeout: this._config.installTimeout, + errorMsg: 'Model service installation failed', + }); + } + + /** + * Start the model service in the sandbox. + * + * Starts the service with configured logging settings. + */ + async start(): Promise { + if (!this._isInstalled) { + throw new Error( + `[${this._sandbox.sandboxId}] Cannot start model service: ModelService has not been installed yet. ` + + 'Please call install() first.' + ); + } + + if (!this._runtimeEnv) { + throw new Error('Runtime environment not initialized'); + } + + const startCmd = this._config.startCmd.replace(/\$\{type\}/g, this._config.type); + const bashStartCmd = + `export ROCK_LOGGING_PATH=${this._config.loggingPath} && ` + + `export ROCK_LOGGING_FILE_NAME=${this._config.loggingFileName} && ` + + `${this._config.stopCmd} && ` + + startCmd; + + logger.debug(`[${this._sandbox.sandboxId}] Model service Start command: ${bashStartCmd}`); + + await this._runtimeEnv.run(bashStartCmd); + + this._isStarted = true; + } + + /** + * Stop the model service. + */ + async stop(): Promise { + if (!this._isStarted) { + logger.warn( + `[${this._sandbox.sandboxId}] Model service is not running, skipping stop operation. is_started=${this._isStarted}` + ); + return; + } + + if (!this._runtimeEnv) { + throw new Error('Runtime environment not initialized'); + } + + await this._runtimeEnv.run(this._config.stopCmd); + + this._isStarted = false; + } + + /** + * Watch agent process with the specified PID. + */ + async watchAgent(pid: string): Promise { + if (!this._isStarted) { + throw new Error( + `[${this._sandbox.sandboxId}] Cannot watch agent: ModelService is not started. Please call start() first.` + ); + } + + if (!this._runtimeEnv) { + throw new Error('Runtime environment not initialized'); + } + + const watchCmd = this._config.watchAgentCmd.replace(/\$\{pid\}/g, pid); + logger.debug( + `[${this._sandbox.sandboxId}] Model service watch agent with pid=${pid}, cmd: ${watchCmd}` + ); + + await this._runtimeEnv.run(watchCmd); + } + + /** + * Execute anti-call LLM command. + */ + async antiCallLlm( + index: number, + responsePayload?: string, + callTimeout = 600 + ): Promise { + if (!this._isStarted) { + throw new Error( + `[${this._sandbox.sandboxId}] Cannot execute anti-call LLM: ModelService is not started. Please call start() first.` + ); + } + + if (!this._runtimeEnv) { + throw new Error('Runtime environment not initialized'); + } + + logger.info( + `[${this._sandbox.sandboxId}] Executing anti-call LLM: index=${index}, ` + + `has_response=${responsePayload !== undefined}, timeout=${callTimeout}s` + ); + + let cmd: string; + if (responsePayload !== undefined) { + const escapedPayload = `'${responsePayload.replace(/'/g, "'\\''")}'`; + cmd = this._config.antiCallLlmCmd + .replace(/\$\{index\}/g, String(index)) + .replace(/\$\{response_payload\}/g, escapedPayload); + } else { + cmd = this._config.antiCallLlmCmdNoResponse.replace(/\$\{index\}/g, String(index)); + } + + const bashCmd = this._runtimeEnv.wrappedCmd(cmd); + logger.debug(`[${this._sandbox.sandboxId}] Executing command: ${bashCmd}`); + + const result = await this._sandbox.arun(bashCmd, { + mode: 'nohup', + waitTimeout: callTimeout, + waitInterval: 3, + }); + + if (result.exitCode !== 0) { + throw new Error(`Anti-call LLM command failed: ${result.output}`); + } + + return result.output; + } +} + +/** + * Minimal RuntimeEnv interface for type checking + */ +interface RuntimeEnvLike { + init(): Promise; + run(cmd: string, mode?: string, waitTimeout?: number, errorMsg?: string): Promise<{ output: string; exitCode?: number }>; + workdir: string; + wrappedCmd(cmd: string): string; +} diff --git a/rock/ts-sdk/src/sandbox/model_service/index.ts b/rock/ts-sdk/src/sandbox/model_service/index.ts new file mode 100644 index 000000000..af222eaec --- /dev/null +++ b/rock/ts-sdk/src/sandbox/model_service/index.ts @@ -0,0 +1,5 @@ +/** + * ModelService module - manages model service installation and lifecycle in sandbox + */ + +export { ModelServiceConfigSchema, ModelService, type ModelServiceConfig } from './base.js'; diff --git a/rock/ts-sdk/src/sandbox/network.test.ts b/rock/ts-sdk/src/sandbox/network.test.ts new file mode 100644 index 000000000..935447b74 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/network.test.ts @@ -0,0 +1,168 @@ +/** + * Tests for Network - Network management for sandbox + */ + +import { Network, SpeedupType } from './network.js'; +import type { Observation } from '../types/responses.js'; + +/** + * Mock types for testing + */ +interface MockProcess { + executeScript: jest.Mock; +} + +interface MockSandbox { + getSandboxId: () => string; + arun: jest.Mock; + getProcess: () => MockProcess; +} + +/** + * Create a mock sandbox with all required methods + */ +function createMockSandbox(): MockSandbox { + const mockProcess: MockProcess = { + executeScript: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + } as Observation), + }; + + return { + getSandboxId: () => 'test-sandbox', + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + } as Observation), + getProcess: () => mockProcess, + }; +} + +describe('Network', () => { + describe('buildAptSpeedupScript', () => { + // Test the script generation for APT speedup + test('should generate valid bash script with heredoc', async () => { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + const mirrorUrl = 'http://mirrors.aliyun.com/ubuntu'; + + // Call speedup which internally uses buildAptSpeedupScript + await network.speedup(SpeedupType.APT, mirrorUrl); + + // Get the script content that was passed to executeScript + const executeScriptMock = mockSandbox.getProcess().executeScript; + const callArgs = executeScriptMock.mock.calls[0]; + const options = callArgs[0] as { scriptContent: string }; + const scriptContent = options.scriptContent; + + // The script should be a valid bash script + expect(scriptContent).toContain('#!/bin/bash'); + // Should use heredoc for writing sources.list + expect(scriptContent).toContain('< { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + const mirrorUrl = 'http://mirrors.aliyun.com/ubuntu'; + + await network.speedup(SpeedupType.APT, mirrorUrl); + + const executeScriptMock = mockSandbox.getProcess().executeScript; + const callArgs = executeScriptMock.mock.calls[0]; + const options = callArgs[0] as { scriptContent: string }; + const scriptContent = options.scriptContent; + + // Verify the script includes system detection function + expect(scriptContent).toContain('detect_system_and_version'); + expect(scriptContent).toContain('VERSION_CODENAME'); + }); + + test('should include validated mirror URL in script', async () => { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + const mirrorUrl = 'http://mirrors.aliyun.com/ubuntu'; + + await network.speedup(SpeedupType.APT, mirrorUrl); + + const executeScriptMock = mockSandbox.getProcess().executeScript; + const callArgs = executeScriptMock.mock.calls[0]; + const options = callArgs[0] as { scriptContent: string }; + const scriptContent = options.scriptContent; + + // Verify the validated URL is in the script + expect(scriptContent).toContain(mirrorUrl); + }); + }); + + describe('command injection protection', () => { + describe('GitHub speedup', () => { + it('should reject invalid IP address format', async () => { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + + // IP with injection attempt + await expect(network.speedup(SpeedupType.GITHUB, '1.1.1.1; rm -rf /')).rejects.toThrow(); + await expect(network.speedup(SpeedupType.GITHUB, 'not-an-ip')).rejects.toThrow(); + await expect(network.speedup(SpeedupType.GITHUB, '256.1.1.1')).rejects.toThrow(); + }); + + it('should accept valid IP address', async () => { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + + // Should not throw for valid IP + await expect(network.speedup(SpeedupType.GITHUB, '192.168.1.1')).resolves.toBeDefined(); + + // Verify the script doesn't have unquoted injection + const executeScriptMock = mockSandbox.getProcess().executeScript; + const callArgs = executeScriptMock.mock.calls[0]; + const options = callArgs[0] as { scriptContent: string }; + expect(options.scriptContent).not.toMatch(/; rm -rf/); + }); + }); + + describe('APT speedup', () => { + it('should reject invalid URL format', async () => { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + + // Invalid URLs should be rejected + await expect(network.speedup(SpeedupType.APT, 'not-a-url')).rejects.toThrow(); + await expect(network.speedup(SpeedupType.APT, 'ftp://evil.com')).rejects.toThrow(); + }); + + it('should accept valid HTTP/HTTPS URLs', async () => { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + + await expect(network.speedup(SpeedupType.APT, 'http://mirrors.aliyun.com')).resolves.toBeDefined(); + await expect(network.speedup(SpeedupType.APT, 'https://mirrors.aliyun.com')).resolves.toBeDefined(); + }); + }); + + describe('PIP speedup', () => { + it('should reject invalid URL format', async () => { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + + await expect(network.speedup(SpeedupType.PIP, 'javascript:alert(1)')).rejects.toThrow(); + await expect(network.speedup(SpeedupType.PIP, 'not-a-url')).rejects.toThrow(); + }); + + it('should accept valid HTTP/HTTPS URLs', async () => { + const mockSandbox = createMockSandbox(); + const network = new Network(mockSandbox as unknown as import('./client.js').Sandbox); + + await expect(network.speedup(SpeedupType.PIP, 'http://mirrors.aliyun.com/pypi/simple/')).resolves.toBeDefined(); + }); + }); + }); +}); \ No newline at end of file diff --git a/rock/ts-sdk/src/sandbox/network.ts b/rock/ts-sdk/src/sandbox/network.ts new file mode 100644 index 000000000..a930d7042 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/network.ts @@ -0,0 +1,234 @@ +/** + * Network - Network management for sandbox + */ + +import { initLogger } from '../logger.js'; +import type { Observation } from '../types/responses.js'; +import type { Sandbox } from './client.js'; +import { validateUrl, validateIpAddress, shellQuote } from '../utils/shell.js'; + +const logger = initLogger('rock.sandbox.network'); + +/** + * Speedup type enum + */ +export enum SpeedupType { + APT = 'apt', + PIP = 'pip', + GITHUB = 'github', +} + +/** + * Network management for sandbox + */ +export class Network { + private sandbox: Sandbox; + + constructor(sandbox: Sandbox) { + this.sandbox = sandbox; + } + + /** + * Configure acceleration for package managers or network resources + * + * @param speedupType - Type of speedup configuration + * @param speedupValue - Speedup value (mirror URL or IP address) + * @param timeout - Execution timeout in seconds + * @returns Observation with execution result + */ + async speedup( + speedupType: SpeedupType, + speedupValue: string, + timeout: number = 300 + ): Promise { + const sandboxId = this.sandbox.getSandboxId(); + logger.info( + `[${sandboxId}] Configuring ${speedupType} speedup: ${speedupValue}` + ); + + // Validate input based on type + let validatedValue: string; + switch (speedupType) { + case SpeedupType.APT: + case SpeedupType.PIP: + validatedValue = validateUrl(speedupValue); + break; + case SpeedupType.GITHUB: + validatedValue = validateIpAddress(speedupValue); + break; + default: + throw new Error(`Unsupported speedup type: ${speedupType}`); + } + + // Generate script content + const scriptContent = this.generateSpeedupScript(speedupType, validatedValue); + + // Execute script using the process module (uploads script file and executes) + const result = await this.sandbox.getProcess().executeScript({ + scriptContent, + waitTimeout: timeout, + }); + + return result; + } + + /** + * Generate speedup script content based on type + */ + private generateSpeedupScript(speedupType: SpeedupType, value: string): string { + switch (speedupType) { + case SpeedupType.APT: + return this.buildAptSpeedupScript(value); + case SpeedupType.PIP: + return this.buildPipSpeedupScript(value); + case SpeedupType.GITHUB: + return this.buildGithubSpeedupScript(value); + default: + throw new Error(`Unsupported speedup type: ${speedupType}`); + } + } + + /** + * Build APT speedup script + * Uses script file approach for safety + */ + private buildAptSpeedupScript(mirrorUrl: string): string { + return `#!/bin/bash +detect_system_and_version() { + if [ -f /etc/debian_version ]; then + . /etc/os-release + if [ "$ID" = "ubuntu" ]; then + echo "ubuntu:$VERSION_CODENAME" + elif [ "$ID" = "debian" ]; then + echo "debian:$VERSION_CODENAME" + else + echo "unknown:" + fi + else + echo "unknown:" + fi +} + +SYSTEM_INFO=$(detect_system_and_version) +SYSTEM=$(echo "$SYSTEM_INFO" | cut -d: -f1) +CODENAME=$(echo "$SYSTEM_INFO" | cut -d: -f2) +echo "System type: $SYSTEM, Version codename: $CODENAME" + +# Backup original sources file +if [ ! -f /etc/apt/sources.list.backup ]; then + cp /etc/apt/sources.list /etc/apt/sources.list.backup +fi + +if [ "$SYSTEM" = "debian" ]; then + if [ -z "$CODENAME" ]; then + CODENAME="bookworm" + fi + cat > /etc/apt/sources.list < /etc/apt/sources.list <>> APT source configuration completed" +`; + } + + /** + * Build PIP speedup script + * Uses script file approach for safety + */ + private buildPipSpeedupScript(mirrorUrl: string): string { + const parsed = new URL(mirrorUrl); + const trustedHost = parsed.host; + const indexUrl = `${mirrorUrl}/pypi/simple/`; + + return `#!/bin/bash +echo ">>> Configuring pip source..." + +# Configure for root user +mkdir -p /root/.pip +cat > /root/.pip/pip.conf < "$home_dir/.pip/pip.conf" </dev/null || true + fi +done + +echo ">>> pip source configuration completed" +`; + } + + /** + * Build GitHub speedup script + * Uses script file approach for safety + */ + private buildGithubSpeedupScript(ipAddress: string): string { + return `#!/bin/bash +echo ">>> Configuring GitHub hosts for github.com acceleration..." + +# Backup original hosts file if not already backed up +if [ ! -f /etc/hosts.backup ]; then + cp /etc/hosts /etc/hosts.backup + echo "Hosts file backed up to /etc/hosts.backup" +fi + +# Remove existing github.com entry if any +sed -i '/github\.com$/d' /etc/hosts + +# Add new github.com hosts entry +echo "${ipAddress} github.com" | tee -a /etc/hosts + +echo ">>> GitHub hosts configuration completed" +echo "Current github.com entry in /etc/hosts:" +grep 'github\.com$' /etc/hosts || echo "No github.com entry found" +`; + } +} \ No newline at end of file diff --git a/rock/ts-sdk/src/sandbox/process.ts b/rock/ts-sdk/src/sandbox/process.ts new file mode 100644 index 000000000..f9a15c234 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/process.ts @@ -0,0 +1,88 @@ +/** + * Process - Process management for sandbox execution + */ + +import { initLogger } from '../logger.js'; +import type { Observation } from '../types/responses.js'; +import type { Sandbox } from './client.js'; + +const logger = initLogger('rock.sandbox.process'); + +/** + * Process management for sandbox execution + */ +export class Process { + private sandbox: Sandbox; + + constructor(sandbox: Sandbox) { + this.sandbox = sandbox; + } + + /** + * Execute a script in the sandbox + * + * @param scriptContent - The script content to execute + * @param scriptName - Optional custom script name + * @param waitTimeout - Maximum time to wait for script completion + * @param waitInterval - Interval between process checks + * @param cleanup - Whether to delete the script file after execution + * @returns Observation with execution result + */ + async executeScript(options: { + scriptContent: string; + scriptName?: string; + waitTimeout?: number; + waitInterval?: number; + cleanup?: boolean; + }): Promise { + const { + scriptContent, + scriptName, + waitTimeout = 300, + cleanup = true, + } = options; + + const sandboxId = this.sandbox.getSandboxId(); + const timestamp = Date.now(); + const name = scriptName ?? `script_${timestamp}.sh`; + const scriptPath = `/tmp/${name}`; + + try { + // Upload script + logger.info(`[${sandboxId}] Uploading script to ${scriptPath}`); + const writeResult = await this.sandbox.writeFile({ + content: scriptContent, + path: scriptPath, + }); + + if (!writeResult.success) { + const errorMsg = `Failed to upload script: ${writeResult.message}`; + logger.error(errorMsg); + return { output: errorMsg, exitCode: 1, failureReason: 'Script upload failed', expectString: '' }; + } + + // Execute script + logger.info(`[${sandboxId}] Executing script: ${scriptPath} (timeout=${waitTimeout}s)`); + const result = await this.sandbox.arun(`bash ${scriptPath}`, { + mode: 'nohup', + waitTimeout, + }); + + return result; + } catch (e) { + const errorMsg = `Script execution failed: ${e}`; + logger.error(errorMsg); + return { output: errorMsg, exitCode: 1, failureReason: errorMsg, expectString: '' }; + } finally { + // Cleanup script if requested + if (cleanup) { + try { + logger.info(`[${sandboxId}] Cleaning up script: ${scriptPath}`); + await this.sandbox.execute({ command: ['rm', '-f', scriptPath], timeout: 30 }); + } catch (e) { + logger.warn(`Failed to cleanup script ${scriptPath}: ${e}`); + } + } + } + } +} diff --git a/rock/ts-sdk/src/sandbox/remote_user.ts b/rock/ts-sdk/src/sandbox/remote_user.ts new file mode 100644 index 000000000..c975983aa --- /dev/null +++ b/rock/ts-sdk/src/sandbox/remote_user.ts @@ -0,0 +1,79 @@ +/** + * RemoteUser - Remote user management for sandbox + */ + +import { initLogger } from '../logger.js'; +import type { CommandResponse } from '../types/responses.js'; +import type { AbstractSandbox } from './client.js'; + +const logger = initLogger('rock.sandbox.user'); + +/** + * Abstract remote user interface + */ +export abstract class RemoteUser { + protected sandbox: AbstractSandbox; + protected currentUser: string = 'root'; + + constructor(sandbox: AbstractSandbox) { + this.sandbox = sandbox; + } + + getCurrentUser(): string { + return this.currentUser; + } + + abstract createRemoteUser(userName: string): Promise; + abstract isUserExist(userName: string): Promise; +} + +/** + * Linux remote user implementation + */ +export class LinuxRemoteUser extends RemoteUser { + constructor(sandbox: AbstractSandbox) { + super(sandbox); + } + + async createRemoteUser(userName: string): Promise { + try { + if (await this.isUserExist(userName)) { + return true; + } + + const response: CommandResponse = await this.sandbox.execute({ + command: ['useradd', '-m', '-s', '/bin/bash', userName], + timeout: 30, + }); + + logger.info(`user add execute response: ${JSON.stringify(response)}`); + + if (response.exitCode !== 0) { + return false; + } + + return true; + } catch (e) { + logger.error('create_remote_user failed', e as Error); + throw e; + } + } + + async isUserExist(userName: string): Promise { + try { + const response: CommandResponse = await this.sandbox.execute({ + command: ['id', userName], + timeout: 30, + }); + + if (response.exitCode === 0) { + logger.info(`user ${userName} already exists`); + return true; + } + return false; + } catch (e) { + logger.info(`is_user_exist exception: ${e}`); + throw e; + } + } +} diff --git a/rock/ts-sdk/src/sandbox/runtime_env/base.test.ts b/rock/ts-sdk/src/sandbox/runtime_env/base.test.ts new file mode 100644 index 000000000..d2cd8bc58 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/base.test.ts @@ -0,0 +1,304 @@ +import { RuntimeEnv, RuntimeEnvId, createRuntimeEnvId } from './base.js'; +import { RuntimeEnvConfig } from './config.js'; +import type { Observation } from '../../types/responses.js'; + +/** + * Mock Sandbox for testing + */ +interface MockSandbox { + sandboxId: string; + runtimeEnvs: Record; + createSession: jest.Mock; + arun: jest.Mock; +} + +function createMockSandbox(): MockSandbox { + return { + sandboxId: 'test-sandbox-id', + runtimeEnvs: {}, + createSession: jest.fn().mockResolvedValue({}), + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + } as Observation), + }; +} + +/** + * Test implementation of RuntimeEnv + */ +class TestRuntimeEnv extends RuntimeEnv { + readonly runtimeEnvType = 'test'; + + protected _getInstallCmd(): string { + return 'echo "installing test runtime"'; + } +} + +describe('RuntimeEnv', () => { + describe('createRuntimeEnvId', () => { + it('should create a valid RuntimeEnvId', () => { + const id = createRuntimeEnvId(); + expect(typeof id).toBe('string'); + expect(id.length).toBe(8); + }); + + it('should create unique IDs', () => { + const id1 = createRuntimeEnvId(); + const id2 = createRuntimeEnvId(); + expect(id1).not.toBe(id2); + }); + }); + + describe('RuntimeEnv base class', () => { + let mockSandbox: MockSandbox; + let config: RuntimeEnvConfig; + + beforeEach(() => { + mockSandbox = createMockSandbox(); + config = { + type: 'test', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: [], + }; + }); + + describe('constructor', () => { + it('should initialize with correct properties', () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + expect(env.runtimeEnvId).toBeDefined(); + expect(env.runtimeEnvId.length).toBe(8); + expect(env.workdir).toContain('/tmp/rock-runtime-envs/test/default/'); + expect(env.binDir).toContain('/tmp/rock-runtime-envs/test/default/'); + expect(env.binDir).toContain('/runtime-env/bin'); + expect(env.initialized).toBe(false); + }); + + it('should use version in workdir path', () => { + config.version = '1.0.0'; + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + expect(env.workdir).toContain('/tmp/rock-runtime-envs/test/1.0.0/'); + }); + + it('should use "default" when version is empty', () => { + config.version = ''; + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + expect(env.workdir).toContain('/tmp/rock-runtime-envs/test/default/'); + }); + }); + + describe('init', () => { + it('should initialize runtime environment', async () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + await env.init(); + + expect(env.initialized).toBe(true); + expect(mockSandbox.createSession).toHaveBeenCalled(); + expect(mockSandbox.arun).toHaveBeenCalled(); + }); + + it('should be idempotent - calling init multiple times only initializes once', async () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + await env.init(); + await env.init(); + await env.init(); + + // Should only create session once + expect(mockSandbox.createSession).toHaveBeenCalledTimes(1); + }); + + it('should execute custom install command if provided', async () => { + config.customInstallCmd = 'npm install'; + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + await env.init(); + + const calls = mockSandbox.arun.mock.calls; + const customInstallCall = calls.find((call: unknown[]) => + typeof call[0] === 'string' && call[0].includes('npm install') + ); + expect(customInstallCall).toBeDefined(); + }); + + it('should create symlinks if extraSymlinkDir and executables are provided', async () => { + config.extraSymlinkDir = '/usr/local/bin'; + config.extraSymlinkExecutables = ['python', 'pip']; + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + await env.init(); + + const lastCall = mockSandbox.arun.mock.calls[mockSandbox.arun.mock.calls.length - 1]; + expect(lastCall[0]).toContain('ln -sf'); + expect(lastCall[0]).toContain('python'); + expect(lastCall[0]).toContain('pip'); + }); + }); + + describe('run', () => { + it('should wrap command with PATH and execute', async () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + await env.init(); + + mockSandbox.arun.mockClear(); + await env.run('python --version'); + + expect(mockSandbox.arun).toHaveBeenCalledWith( + expect.stringContaining('export PATH='), + expect.objectContaining({ + session: expect.stringContaining('runtime-env-test-'), + mode: 'nohup', + }) + ); + }); + + it('should raise error when command fails', async () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + await env.init(); + + // Mock the next arun call to return a failure + mockSandbox.arun.mockResolvedValueOnce({ + output: 'error message', + exitCode: 1, + failureReason: 'command failed', + expectString: '', + } as Observation); + + await expect(env.run('failing-command')).rejects.toThrow(); + }); + + it('should use custom timeout and error message', async () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + await env.init(); + + mockSandbox.arun.mockClear(); + await env.run('long-command', { waitTimeout: 1200, errorMsg: 'custom error' }); + + expect(mockSandbox.arun).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + waitTimeout: 1200, + }) + ); + }); + }); + + describe('wrappedCmd', () => { + it('should prepend bin dir to PATH when prepend=true', () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + const wrapped = env.wrappedCmd('python --version', true); + + expect(wrapped).toContain('export PATH='); + expect(wrapped).toContain('$PATH'); + expect(wrapped).toContain('bash -c'); + }); + + it('should append bin dir to PATH when prepend=false', () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + const wrapped = env.wrappedCmd('python --version', false); + + expect(wrapped).toContain('$PATH:'); + expect(wrapped).toContain('bash -c'); + }); + + it('should shell-escape the bin dir path', () => { + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + const wrapped = env.wrappedCmd('python --version', true); + + // Path should be quoted to handle spaces + expect(wrapped).toMatch(/export PATH='[^']+'/); + }); + }); + + describe('command injection protection', () => { + it('should safely handle workdir with special characters', async () => { + // Create a config that would result in workdir with special characters + config.type = 'test space'; + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + await env.init(); + + // Check that workdir path is properly quoted + const calls = mockSandbox.arun.mock.calls; + const mkdirCall = calls.find((call: unknown[]) => { + const cmd = call[0] as string; + return cmd.includes('mkdir'); + }); + + expect(mkdirCall).toBeDefined(); + const cmd = (mkdirCall as unknown[])[0] as string; + // Path with space should be properly quoted or escaped + expect(cmd).toMatch(/mkdir -p/); + }); + + it('should safely handle binDir with spaces in wrappedCmd', () => { + config.type = 'test space'; + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + const wrapped = env.wrappedCmd('python --version', true); + + // Spaces in path should be properly quoted/escaped + // The path should contain the escaped space + expect(wrapped).toContain('test space'); + // The entire command should be wrapped in bash -c with proper escaping + expect(wrapped).toMatch(/bash -c/); + }); + + it('should safely handle symlink directory with injection attempt', async () => { + config.extraSymlinkDir = '/tmp; rm -rf /'; + config.extraSymlinkExecutables = ['python']; + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + await env.init(); + + // Check that injection is not executed - it should be quoted + const calls = mockSandbox.arun.mock.calls; + const symlinkCall = calls.find((call: unknown[]) => { + const cmd = call[0] as string; + return cmd.includes('ln -sf'); + }); + + if (symlinkCall) { + const cmd = symlinkCall[0] as string; + // The path should be quoted, preventing injection + // If properly quoted, the semicolon won't be interpreted as command separator + expect(cmd).toMatch(/ln -sf/); + } + }); + + it('should safely handle executable name with injection attempt', async () => { + config.extraSymlinkDir = '/usr/local/bin'; + config.extraSymlinkExecutables = ['python; rm -rf /']; + const env = new TestRuntimeEnv(mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, config); + + await env.init(); + + // Check that injection is not executed + const calls = mockSandbox.arun.mock.calls; + const symlinkCall = calls.find((call: unknown[]) => { + const cmd = call[0] as string; + return cmd.includes('ln -sf'); + }); + + if (symlinkCall) { + const cmd = symlinkCall[0] as string; + // The executable name should be quoted + expect(cmd).toMatch(/ln -sf/); + } + }); + }); + }); +}); \ No newline at end of file diff --git a/rock/ts-sdk/src/sandbox/runtime_env/base.ts b/rock/ts-sdk/src/sandbox/runtime_env/base.ts new file mode 100644 index 000000000..97237fe3e --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/base.ts @@ -0,0 +1,324 @@ +import { randomUUID } from 'crypto'; +import { initLogger } from '../../logger.js'; +import { RuntimeEnvConfig } from './config.js'; +import type { Observation } from '../../types/responses.js'; + +const logger = initLogger('rock.sandbox.runtime_env'); + +/** Unique identifier for a RuntimeEnv instance */ +export type RuntimeEnvId = string; + +/** Create a new unique RuntimeEnvId */ +export function createRuntimeEnvId(): RuntimeEnvId { + return randomUUID().slice(0, 8); +} + +/** Registry for RuntimeEnv subclasses */ +const RUNTIME_ENV_REGISTRY: Record = {}; + +/** + * Interface for Sandbox with methods needed by RuntimeEnv + */ +export interface SandboxLike { + sandboxId: string; + runtimeEnvs: Record; + createSession(request: { session: string; envEnable: boolean; env?: Record }): Promise; + arun( + cmd: string, + options?: { + session?: string; + mode?: 'normal' | 'nohup' | 'NOHUP'; + waitTimeout?: number; + waitInterval?: number; + } + ): Promise; + startNohupProcess?( + cmd: string, + tmpFile: string, + session: string + ): Promise<{ pid: number | null; errorResponse: Observation | null }>; + waitForProcessCompletion?( + pid: number, + session: string, + waitTimeout: number, + waitInterval: number + ): Promise<{ success: boolean; message: string }>; + handleNohupOutput?( + tmpFile: string, + session: string, + success: boolean, + message: string, + ignoreOutput: boolean, + responseLimitedBytes: number | null + ): Promise; +} + +/** + * Runtime environment (e.g., Python/Node). + * + * Each RuntimeEnv is identified by (type, version) tuple and is managed by Sandbox.runtimeEnvs. + * workdir is auto-generated as: /tmp/rock-runtime-envs/{type}/{version}/{runtimeEnvId} + * session is auto-generated as: runtime-env-{type}-{version}-{runtimeEnvId} + * + * @example + * ```typescript + * const env = await RuntimeEnv.create(sandbox, config); + * await env.run("python --version"); + * ``` + */ +export abstract class RuntimeEnv { + /** Registry for subclasses */ + protected static registry: Record = RUNTIME_ENV_REGISTRY; + + /** Runtime type discriminator - must be defined by subclass */ + abstract readonly runtimeEnvType: string; + + /** Sandbox instance */ + protected _sandbox: SandboxLike; + + /** Configuration */ + protected _config: RuntimeEnvConfig; + + /** Version */ + protected _version: string; + + /** Environment variables */ + protected _env: Record; + + /** Install timeout in seconds */ + protected _installTimeout: number; + + /** Custom install command */ + protected _customInstallCmd: string | null; + + /** Extra symlink directory */ + protected _extraSymlinkDir: string | null; + + /** Extra symlink executables */ + protected _extraSymlinkExecutables: string[]; + + /** Unique ID for this runtime env instance */ + protected _runtimeEnvId: RuntimeEnvId; + + /** Working directory */ + protected _workdir: string; + + /** Session name */ + protected _session: string; + + /** Whether the runtime has been initialized */ + protected _initialized: boolean = false; + + /** Whether the session is ready */ + protected _sessionReady: boolean = false; + + constructor(sandbox: SandboxLike, config: RuntimeEnvConfig) { + this._sandbox = sandbox; + this._config = config; + + // Extract values from config + this._version = config.version; + this._env = config.env; + this._installTimeout = config.installTimeout; + this._customInstallCmd = config.customInstallCmd; + this._extraSymlinkDir = config.extraSymlinkDir; + this._extraSymlinkExecutables = config.extraSymlinkExecutables; + + // Unique ID for this runtime env instance + this._runtimeEnvId = createRuntimeEnvId(); + + // Generate workdir and session + const versionStr = this._version || 'default'; // avoid empty version + this._workdir = `/tmp/rock-runtime-envs/${config.type}/${versionStr}/${this._runtimeEnvId}`; + this._session = `runtime-env-${config.type}-${versionStr}-${this._runtimeEnvId}`; + } + + /** Whether the runtime has been initialized */ + get initialized(): boolean { + return this._initialized; + } + + /** Unique ID for this runtime env instance */ + get runtimeEnvId(): RuntimeEnvId { + return this._runtimeEnvId; + } + + /** Working directory for this runtime env instance */ + get workdir(): string { + return this._workdir; + } + + /** Binary directory for this runtime env instance */ + get binDir(): string { + return `${this._workdir}/runtime-env/bin`; + } + + /** + * Initialize the runtime environment. + * This method performs installation and validation. + * It is idempotent: calling multiple times only initializes once. + */ + async init(): Promise { + if (this._initialized) { + return; + } + + // Common setup: ensure session and workdir + await this._ensureSession(); + await this._ensureWorkdir(); + + // Install runtime and then do additional initialization + await this._installRuntime(); + await this._postInit(); + + // Execute custom install command after _postInit + if (this._customInstallCmd) { + await this._doCustomInstall(); + } + + // Create symlinks for executables + await this._createSysPathLinks(); + + this._initialized = true; + } + + /** + * Run a command under this runtime + */ + async run( + cmd: string, + options: { + mode?: 'normal' | 'nohup'; + waitTimeout?: number; + errorMsg?: string; + } = {} + ): Promise { + const { mode = 'nohup', waitTimeout = 600, errorMsg = 'runtime env command failed' } = options; + + await this._ensureSession(); + const wrapped = this.wrappedCmd(cmd, true); + + logger.debug(`[${this._sandbox.sandboxId}] RuntimeEnv run cmd: ${wrapped}`); + + const result = await this._sandbox.arun(wrapped, { + session: this._session, + mode, + waitTimeout, + }); + + // If exit_code is not 0, raise an exception to trigger retry + if (result.exitCode !== undefined && result.exitCode !== 0) { + throw new Error(`${errorMsg} with exit code: ${result.exitCode}, output: ${result.output}`); + } + return result; + } + + /** + * Wrap command with PATH export. + * Always wrap with bash -c to ensure it only affects current cmd. + * Default prepend=true to give current runtime_env highest priority. + */ + wrappedCmd(cmd: string, prepend: boolean = true): string { + const binDir = this.binDir; + let wrapped: string; + if (prepend) { + wrapped = `export PATH='${binDir}':$PATH && ${cmd}`; + } else { + wrapped = `export PATH=$PATH:'${binDir}' && ${cmd}`; + } + return `bash -c '${wrapped.replace(/'/g, "'\"'\"'")}'`; + } + + /** + * Ensure runtime env session exists. Safe to call multiple times. + */ + protected async _ensureSession(): Promise { + if (this._sessionReady) { + return; + } + + await this._sandbox.createSession({ + session: this._session, + envEnable: true, + env: this._env, + }); + this._sessionReady = true; + } + + /** + * Create workdir for runtime environment. + */ + protected async _ensureWorkdir(): Promise { + const result = await this._sandbox.arun(`mkdir -p ${this._workdir}`, { + session: this._session, + }); + if (result.exitCode !== undefined && result.exitCode !== 0) { + throw new Error(`Failed to create workdir: ${this._workdir}, exit_code: ${result.exitCode}`); + } + } + + /** + * Get installation command for this runtime environment. + */ + protected abstract _getInstallCmd(): string; + + /** + * Install the runtime environment. + */ + protected async _installRuntime(): Promise { + const installCmd = `cd '${this._workdir}' && ${this._getInstallCmd()}`; + const wrappedCmd = `bash -c '${installCmd.replace(/'/g, "'\"'\"'")}'`; + + const result = await this._sandbox.arun(wrappedCmd, { + session: this._session, + mode: 'nohup', + waitTimeout: this._installTimeout, + }); + + if (result.exitCode !== undefined && result.exitCode !== 0) { + throw new Error( + `${this.runtimeEnvType} runtime installation failed with exit code: ${result.exitCode}, output: ${result.output}` + ); + } + } + + /** + * Additional initialization after runtime installation. + * Override in subclasses. + */ + protected async _postInit(): Promise { + // Default: no additional initialization + } + + /** + * Execute custom install command after _postInit. + */ + protected async _doCustomInstall(): Promise { + if (!this._customInstallCmd) { + return; + } + await this.run(this._customInstallCmd, { + waitTimeout: this._installTimeout, + errorMsg: 'custom_install_cmd failed', + }); + } + + /** + * Create symlinks in target directory for executables. + */ + protected async _createSysPathLinks(): Promise { + if (this._extraSymlinkDir === null) { + return; + } + if (this._extraSymlinkExecutables.length === 0) { + return; + } + + // Build a single command with all symlinks + const links = this._extraSymlinkExecutables + .map((exe) => `ln -sf '${this.binDir}/${exe}' '${this._extraSymlinkDir}/${exe}'`) + .join(' && '); + + await this.run(links); + } +} diff --git a/rock/ts-sdk/src/sandbox/runtime_env/config.test.ts b/rock/ts-sdk/src/sandbox/runtime_env/config.test.ts new file mode 100644 index 000000000..03d708258 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/config.test.ts @@ -0,0 +1,84 @@ +import { RuntimeEnvConfig, RuntimeEnvConfigSchema } from './config.js'; + +describe('RuntimeEnvConfig', () => { + describe('RuntimeEnvConfigSchema', () => { + it('should parse valid config with required type field', () => { + const result = RuntimeEnvConfigSchema.safeParse({ type: 'python' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('python'); + } + }); + + it('should use default values for optional fields', () => { + const result = RuntimeEnvConfigSchema.safeParse({ type: 'python' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.version).toBe('default'); + expect(result.data.env).toEqual({}); + expect(result.data.installTimeout).toBe(600); + expect(result.data.customInstallCmd).toBeNull(); + expect(result.data.extraSymlinkDir).toBeNull(); + expect(result.data.extraSymlinkExecutables).toEqual([]); + } + }); + + it('should parse config with all fields specified', () => { + const result = RuntimeEnvConfigSchema.safeParse({ + type: 'node', + version: '22.18.0', + env: { NODE_ENV: 'production' }, + installTimeout: 1200, + customInstallCmd: 'npm install', + extraSymlinkDir: '/usr/local/bin', + extraSymlinkExecutables: ['node', 'npm'], + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('node'); + expect(result.data.version).toBe('22.18.0'); + expect(result.data.env).toEqual({ NODE_ENV: 'production' }); + expect(result.data.installTimeout).toBe(1200); + expect(result.data.customInstallCmd).toBe('npm install'); + expect(result.data.extraSymlinkDir).toBe('/usr/local/bin'); + expect(result.data.extraSymlinkExecutables).toEqual(['node', 'npm']); + } + }); + + it('should reject config without type field', () => { + const result = RuntimeEnvConfigSchema.safeParse({ version: '3.11' }); + expect(result.success).toBe(false); + }); + + it('should reject invalid installTimeout (non-positive)', () => { + const result = RuntimeEnvConfigSchema.safeParse({ + type: 'python', + installTimeout: 0, + }); + expect(result.success).toBe(false); + }); + + it('should reject invalid installTimeout (negative)', () => { + const result = RuntimeEnvConfigSchema.safeParse({ + type: 'python', + installTimeout: -1, + }); + expect(result.success).toBe(false); + }); + }); + + describe('RuntimeEnvConfig type', () => { + it('should be inferred from schema', () => { + const config: RuntimeEnvConfig = { + type: 'python', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: [], + }; + expect(config.type).toBe('python'); + }); + }); +}); diff --git a/rock/ts-sdk/src/sandbox/runtime_env/config.ts b/rock/ts-sdk/src/sandbox/runtime_env/config.ts new file mode 100644 index 000000000..7482b4f13 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/config.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; + +/** + * Base configuration for runtime environments. + */ +export const RuntimeEnvConfigSchema = z.object({ + /** Runtime type discriminator. */ + type: z.string(), + + /** Runtime version. Use 'default' for the default version of each runtime. */ + version: z.string().default('default'), + + /** Environment variables for the runtime session. */ + env: z.record(z.string()).default({}), + + /** Timeout in seconds for installation commands. */ + installTimeout: z.number().int().positive().default(600), + + /** Custom install command to run after init. */ + customInstallCmd: z.string().nullable().default(null), + + /** Directory to create symlinks of executables. If null, no symlinks are created. */ + extraSymlinkDir: z.string().nullable().default(null), + + /** List of executable names to symlink. Empty list means no symlinks. */ + extraSymlinkExecutables: z.array(z.string()).default([]), +}); + +export type RuntimeEnvConfig = z.infer; diff --git a/rock/ts-sdk/src/sandbox/runtime_env/index.ts b/rock/ts-sdk/src/sandbox/runtime_env/index.ts new file mode 100644 index 000000000..4a82ac52c --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/index.ts @@ -0,0 +1,32 @@ +/** + * Runtime Environment module + * + * Provides runtime environment management for sandbox containers. + * Supports Python and Node.js runtimes. + */ + +// Types +export type { RuntimeEnvConfig } from './config.js'; + +// Config schemas +export { RuntimeEnvConfigSchema } from './config.js'; + +// Python runtime +export { + PythonRuntimeEnvConfigSchema, + PythonRuntimeEnv, + getDefaultPipIndexUrl, +} from './python_runtime_env.js'; +export type { PythonRuntimeEnvConfig } from './python_runtime_env.js'; + +// Node runtime +export { + NodeRuntimeEnvConfigSchema, + NodeRuntimeEnv, + NODE_DEFAULT_VERSION, +} from './node_runtime_env.js'; +export type { NodeRuntimeEnvConfig } from './node_runtime_env.js'; + +// Base class and types +export { RuntimeEnv, createRuntimeEnvId } from './base.js'; +export type { RuntimeEnvId, SandboxLike } from './base.js'; diff --git a/rock/ts-sdk/src/sandbox/runtime_env/node_runtime_env.test.ts b/rock/ts-sdk/src/sandbox/runtime_env/node_runtime_env.test.ts new file mode 100644 index 000000000..ee1b2ea8a --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/node_runtime_env.test.ts @@ -0,0 +1,246 @@ +import { + NodeRuntimeEnvConfig, + NodeRuntimeEnvConfigSchema, + NodeRuntimeEnv, + NODE_DEFAULT_VERSION, +} from './node_runtime_env.js'; +import { RuntimeEnvId } from './base.js'; +import type { Observation } from '../../types/responses.js'; + +/** + * Mock Sandbox for testing + */ +interface MockSandbox { + sandboxId: string; + runtimeEnvs: Record; + createSession: jest.Mock; + arun: jest.Mock; +} + +function createMockSandbox(): MockSandbox { + return { + sandboxId: 'test-sandbox-id', + runtimeEnvs: {}, + createSession: jest.fn().mockResolvedValue({}), + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + } as Observation), + }; +} + +describe('NodeRuntimeEnvConfig', () => { + describe('NodeRuntimeEnvConfigSchema', () => { + it('should parse valid config with default values', () => { + const result = NodeRuntimeEnvConfigSchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('node'); + expect(result.data.version).toBe('default'); + expect(result.data.npmRegistry).toBeNull(); + expect(result.data.extraSymlinkExecutables).toEqual(['node', 'npm', 'npx']); + } + }); + + it('should parse config with version 22.18.0', () => { + const result = NodeRuntimeEnvConfigSchema.safeParse({ version: '22.18.0' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.version).toBe('22.18.0'); + } + }); + + it('should reject invalid version', () => { + const result = NodeRuntimeEnvConfigSchema.safeParse({ version: '20.10.0' }); + expect(result.success).toBe(false); + }); + + it('should parse config with npmRegistry', () => { + const result = NodeRuntimeEnvConfigSchema.safeParse({ + npmRegistry: 'https://registry.npmmirror.com', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.npmRegistry).toBe('https://registry.npmmirror.com'); + } + }); + + it('should parse config with custom extraSymlinkExecutables', () => { + const result = NodeRuntimeEnvConfigSchema.safeParse({ + extraSymlinkExecutables: ['node', 'npm'], + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.extraSymlinkExecutables).toEqual(['node', 'npm']); + } + }); + + it('should inherit base config fields', () => { + const result = NodeRuntimeEnvConfigSchema.safeParse({ + version: '22.18.0', + env: { NODE_ENV: 'production' }, + installTimeout: 900, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.version).toBe('22.18.0'); + expect(result.data.env).toEqual({ NODE_ENV: 'production' }); + expect(result.data.installTimeout).toBe(900); + } + }); + }); + + describe('NodeRuntimeEnvConfig type', () => { + it('should be inferred from schema', () => { + const config: NodeRuntimeEnvConfig = { + type: 'node', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['node', 'npm', 'npx'], + npmRegistry: null, + }; + expect(config.type).toBe('node'); + }); + }); + + describe('NODE_DEFAULT_VERSION constant', () => { + it('should be 22.18.0', () => { + expect(NODE_DEFAULT_VERSION).toBe('22.18.0'); + }); + }); +}); + +describe('NodeRuntimeEnv', () => { + let mockSandbox: MockSandbox; + + beforeEach(() => { + mockSandbox = createMockSandbox(); + }); + + describe('constructor', () => { + it('should create instance with default config', () => { + const config: NodeRuntimeEnvConfig = { + type: 'node', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['node', 'npm', 'npx'], + npmRegistry: null, + }; + + const env = new NodeRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + expect(env.runtimeEnvType).toBe('node'); + expect(env.initialized).toBe(false); + }); + + it('should throw error for unsupported version', () => { + const config: NodeRuntimeEnvConfig = { + type: 'node', + version: '20.10.0' as '22.18.0', // Force invalid version + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['node', 'npm', 'npx'], + npmRegistry: null, + }; + + expect(() => { + new NodeRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + }).toThrow('Unsupported Node version'); + }); + }); + + describe('_getInstallCmd', () => { + it('should return node install command', () => { + const config: NodeRuntimeEnvConfig = { + type: 'node', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['node', 'npm', 'npx'], + npmRegistry: null, + }; + + const env = new NodeRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + const installCmd = (env as unknown as { _getInstallCmd: () => string })._getInstallCmd(); + expect(installCmd).toContain('node'); + }); + }); + + describe('init', () => { + it('should validate node exists after installation', async () => { + const config: NodeRuntimeEnvConfig = { + type: 'node', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['node', 'npm', 'npx'], + npmRegistry: null, + }; + + const env = new NodeRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + await env.init(); + + // Check that test -x node was called + const calls = mockSandbox.arun.mock.calls; + const validateCall = calls.find((call: unknown[]) => + typeof call[0] === 'string' && call[0].includes('test -x node') + ); + expect(validateCall).toBeDefined(); + }); + + it('should configure npm registry if specified', async () => { + const config: NodeRuntimeEnvConfig = { + type: 'node', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['node', 'npm', 'npx'], + npmRegistry: 'https://registry.npmmirror.com', + }; + + const env = new NodeRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + await env.init(); + + // Check that npm config was called + const calls = mockSandbox.arun.mock.calls; + const npmConfigCall = calls.find((call: unknown[]) => + typeof call[0] === 'string' && call[0].includes('npm config set registry') + ); + expect(npmConfigCall).toBeDefined(); + }); + }); +}); diff --git a/rock/ts-sdk/src/sandbox/runtime_env/node_runtime_env.ts b/rock/ts-sdk/src/sandbox/runtime_env/node_runtime_env.ts new file mode 100644 index 000000000..a9bea43e3 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/node_runtime_env.ts @@ -0,0 +1,94 @@ +/** + * Node.js runtime environment configuration and implementation + */ + +import { z } from 'zod'; +import { RuntimeEnvConfigSchema } from './config.js'; +import { RuntimeEnv, type RuntimeEnvId, type SandboxLike } from './base.js'; +import { envVars } from '../../env_vars.js'; + +/** Default Node.js version */ +export const NODE_DEFAULT_VERSION = '22.18.0'; + +/** + * Configuration for Node.js runtime environment. + */ +export const NodeRuntimeEnvConfigSchema = RuntimeEnvConfigSchema.extend({ + /** Runtime type discriminator. Must be 'node'. */ + type: z.literal('node').default('node'), + + /** Node.js version. Use "default" for 22.18.0. */ + version: z.enum(['22.18.0', 'default']).default('default'), + + /** NPM registry URL. If set, will run 'npm config set registry ' during init. */ + npmRegistry: z.string().nullable().default(null), + + /** List of Node.js executables to symlink. */ + extraSymlinkExecutables: z.array(z.string()).default(['node', 'npm', 'npx']), +}); + +/** + * Node runtime environment configuration type + */ +export type NodeRuntimeEnvConfig = z.infer; + +/** + * Node runtime environment + * + * Provides Node.js runtime with npm package management. + * Supports Node.js 22.18.0. + * + * @example + * ```typescript + * const config: NodeRuntimeEnvConfig = { + * version: 'default', + * npmRegistry: 'https://registry.npmmirror.com', + * }; + * const env = new NodeRuntimeEnv(sandbox, config); + * await env.init(); + * await env.run('node --version'); + * ``` + */ +export class NodeRuntimeEnv extends RuntimeEnv { + readonly runtimeEnvType = 'node'; + + private _npmRegistry: string | null | undefined; + + constructor( + sandbox: SandboxLike & { runtimeEnvs: Record }, + config: NodeRuntimeEnvConfig + ) { + if (config.version !== 'default' && config.version !== NODE_DEFAULT_VERSION) { + throw new Error( + `Unsupported Node version: ${config.version}. Only ${NODE_DEFAULT_VERSION} is supported right now.` + ); + } + + super(sandbox, config); + + this._npmRegistry = config.npmRegistry; + } + + protected _getInstallCmd(): string { + return envVars.ROCK_RTENV_NODE_V22180_INSTALL_CMD; + } + + protected override async _postInit(): Promise { + // Step 1: validate node exists + await this._validateNode(); + + // Step 2: configure npm registry if specified + if (this._npmRegistry) { + await this._configureNpmRegistry(); + } + } + + private async _validateNode(): Promise { + await this.run('test -x node'); + } + + private async _configureNpmRegistry(): Promise { + const escapedRegistry = this._npmRegistry!.replace(/'/g, "'\\''"); + await this.run(`npm config set registry '${escapedRegistry}'`); + } +} \ No newline at end of file diff --git a/rock/ts-sdk/src/sandbox/runtime_env/python_runtime_env.test.ts b/rock/ts-sdk/src/sandbox/runtime_env/python_runtime_env.test.ts new file mode 100644 index 000000000..9441c6525 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/python_runtime_env.test.ts @@ -0,0 +1,348 @@ +import { + PythonRuntimeEnvConfig, + PythonRuntimeEnvConfigSchema, + PythonRuntimeEnv, +} from './python_runtime_env.js'; +import { RuntimeEnvId } from './base.js'; +import type { Observation } from '../../types/responses.js'; + +/** + * Mock Sandbox for testing + */ +interface MockSandbox { + sandboxId: string; + runtimeEnvs: Record; + createSession: jest.Mock; + arun: jest.Mock; +} + +function createMockSandbox(): MockSandbox { + return { + sandboxId: 'test-sandbox-id', + runtimeEnvs: {}, + createSession: jest.fn().mockResolvedValue({}), + arun: jest.fn().mockResolvedValue({ + output: '', + exitCode: 0, + failureReason: '', + expectString: '', + } as Observation), + }; +} + +describe('PythonRuntimeEnvConfig', () => { + describe('PythonRuntimeEnvConfigSchema', () => { + it('should parse valid config with default values', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('python'); + expect(result.data.version).toBe('default'); + expect(result.data.pip).toBeNull(); + expect(result.data.pipIndexUrl).toBeNull(); + expect(result.data.extraSymlinkExecutables).toEqual(['python', 'python3', 'pip', 'pip3']); + } + }); + + it('should parse config with version 3.11', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({ version: '3.11' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.version).toBe('3.11'); + } + }); + + it('should parse config with version 3.12', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({ version: '3.12' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.version).toBe('3.12'); + } + }); + + it('should reject invalid version', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({ version: '3.10' }); + expect(result.success).toBe(false); + }); + + it('should parse config with pip packages as array', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({ + pip: ['langchain', 'langchain-openai'], + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.pip).toEqual(['langchain', 'langchain-openai']); + } + }); + + it('should parse config with pip as requirements.txt path', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({ + pip: './requirements.txt', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.pip).toBe('./requirements.txt'); + } + }); + + it('should parse config with pipIndexUrl', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({ + pipIndexUrl: 'https://mirrors.aliyun.com/pypi/simple/', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.pipIndexUrl).toBe('https://mirrors.aliyun.com/pypi/simple/'); + } + }); + + it('should parse config with custom extraSymlinkExecutables', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({ + extraSymlinkExecutables: ['python', 'pip'], + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.extraSymlinkExecutables).toEqual(['python', 'pip']); + } + }); + + it('should inherit base config fields', () => { + const result = PythonRuntimeEnvConfigSchema.safeParse({ + version: '3.12', + env: { PYTHONPATH: '/app' }, + installTimeout: 900, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.version).toBe('3.12'); + expect(result.data.env).toEqual({ PYTHONPATH: '/app' }); + expect(result.data.installTimeout).toBe(900); + } + }); + }); + + describe('PythonRuntimeEnvConfig type', () => { + it('should be inferred from schema', () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: null, + pipIndexUrl: null, + }; + expect(config.type).toBe('python'); + }); + }); +}); + +describe('PythonRuntimeEnv', () => { + let mockSandbox: MockSandbox; + + beforeEach(() => { + mockSandbox = createMockSandbox(); + }); + + describe('constructor', () => { + it('should create instance with default config', () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: null, + pipIndexUrl: null, + }; + + const env = new PythonRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + expect(env.runtimeEnvType).toBe('python'); + expect(env.initialized).toBe(false); + }); + + it('should throw error for unsupported version', () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: '3.10' as '3.11', // Force invalid version + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: null, + pipIndexUrl: null, + }; + + expect(() => { + new PythonRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + }).toThrow('Unsupported Python version'); + }); + }); + + describe('_getInstallCmd', () => { + it('should return 3.11 install command for version 3.11', () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: '3.11', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: null, + pipIndexUrl: null, + }; + + const env = new PythonRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + // Access protected method via type assertion + const installCmd = (env as unknown as { _getInstallCmd: () => string })._getInstallCmd(); + expect(installCmd).toContain('cpython-3.11'); + }); + + it('should return 3.12 install command for version 3.12', () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: '3.12', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: null, + pipIndexUrl: null, + }; + + const env = new PythonRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + const installCmd = (env as unknown as { _getInstallCmd: () => string })._getInstallCmd(); + expect(installCmd).toContain('cpython-3.12'); + }); + + it('should return 3.11 install command for default version', () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: null, + pipIndexUrl: null, + }; + + const env = new PythonRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + const installCmd = (env as unknown as { _getInstallCmd: () => string })._getInstallCmd(); + expect(installCmd).toContain('cpython-3.11'); + }); + }); + + describe('init', () => { + it('should validate python exists after installation', async () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: null, + pipIndexUrl: null, + }; + + const env = new PythonRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + await env.init(); + + // Check that test -x python was called + const calls = mockSandbox.arun.mock.calls; + const validateCall = calls.find((call: unknown[]) => + typeof call[0] === 'string' && call[0].includes('test -x python') + ); + expect(validateCall).toBeDefined(); + }); + + it('should configure pip index url if specified', async () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: null, + pipIndexUrl: 'https://mirrors.aliyun.com/pypi/simple/', + }; + + const env = new PythonRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + await env.init(); + + // Check that pip config was called + const calls = mockSandbox.arun.mock.calls; + const pipConfigCall = calls.find((call: unknown[]) => + typeof call[0] === 'string' && call[0].includes('pip config set global.index-url') + ); + expect(pipConfigCall).toBeDefined(); + }); + + it('should install pip packages if specified as array', async () => { + const config: PythonRuntimeEnvConfig = { + type: 'python', + version: 'default', + env: {}, + installTimeout: 600, + customInstallCmd: null, + extraSymlinkDir: null, + extraSymlinkExecutables: ['python', 'python3', 'pip', 'pip3'], + pip: ['langchain', 'langchain-openai'], + pipIndexUrl: null, + }; + + const env = new PythonRuntimeEnv( + mockSandbox as unknown as MockSandbox & { runtimeEnvs: Record }, + config + ); + + await env.init(); + + // Check that pip install was called + const calls = mockSandbox.arun.mock.calls; + const pipInstallCall = calls.find((call: unknown[]) => + typeof call[0] === 'string' && call[0].includes('pip install') + ); + expect(pipInstallCall).toBeDefined(); + expect(pipInstallCall?.[0]).toContain('langchain'); + }); + }); +}); diff --git a/rock/ts-sdk/src/sandbox/runtime_env/python_runtime_env.ts b/rock/ts-sdk/src/sandbox/runtime_env/python_runtime_env.ts new file mode 100644 index 000000000..9664b57fd --- /dev/null +++ b/rock/ts-sdk/src/sandbox/runtime_env/python_runtime_env.ts @@ -0,0 +1,138 @@ +/** + * Python runtime environment configuration and implementation + */ + +import { z } from 'zod'; +import { RuntimeEnvConfigSchema } from './config.js'; +import { RuntimeEnv, type RuntimeEnvId, type SandboxLike } from './base.js'; +import { envVars } from '../../env_vars.js'; + +/** + * Python runtime environment configuration schema + */ +export const PythonRuntimeEnvConfigSchema = RuntimeEnvConfigSchema.extend({ + /** Runtime type discriminator. Must be 'python'. */ + type: z.literal('python').default('python'), + + /** Python version. Use "default" for 3.11. */ + version: z.enum(['3.11', '3.12', 'default']).default('default'), + + /** + * Pip packages to install. + * Can be: + * - string[]: List of package names to install + * - string: Path to requirements.txt file + * - null: No packages to install + */ + pip: z.union([z.array(z.string()), z.string()]).nullable().default(null), + + /** Pip index URL for package installation. If set, will use this mirror. */ + pipIndexUrl: z.string().nullable().default(null), + + /** List of Python executables to symlink. */ + extraSymlinkExecutables: z.array(z.string()).default(['python', 'python3', 'pip', 'pip3']), +}); + +/** + * Python runtime environment configuration type + */ +export type PythonRuntimeEnvConfig = z.infer; + +/** + * Get default pip index URL from environment variable. + */ +export function getDefaultPipIndexUrl(): string { + return envVars.ROCK_PIP_INDEX_URL; +} + +/** + * Python runtime environment + * + * Provides Python runtime with pip package management. + * Supports Python 3.11 and 3.12 versions. + * + * @example + * ```typescript + * const config: PythonRuntimeEnvConfig = { + * version: 'default', + * pip: ['langchain', 'langchain-openai'], + * pipIndexUrl: 'https://mirrors.aliyun.com/pypi/simple/', + * }; + * const env = new PythonRuntimeEnv(sandbox, config); + * await env.init(); + * await env.run('python --version'); + * ``` + */ +export class PythonRuntimeEnv extends RuntimeEnv { + readonly runtimeEnvType = 'python'; + + private _pip: string[] | string | null | undefined; + private _pipIndexUrl: string | null | undefined; + + constructor( + sandbox: SandboxLike & { runtimeEnvs: Record }, + config: PythonRuntimeEnvConfig + ) { + // Validate version early + if (config.version !== '3.11' && config.version !== '3.12' && config.version !== 'default') { + throw new Error( + `Unsupported Python version: ${config.version}. Supported versions: 3.11, 3.12, default` + ); + } + + super(sandbox, config); + + this._pip = config.pip; + this._pipIndexUrl = config.pipIndexUrl; + } + + protected _getInstallCmd(): string { + const version = this._version; + if (version === '3.11' || version === 'default') { + return envVars.ROCK_RTENV_PYTHON_V31114_INSTALL_CMD; + } + return envVars.ROCK_RTENV_PYTHON_V31212_INSTALL_CMD; + } + + protected override async _postInit(): Promise { + // Step 1: validate python exists + await this._validatePython(); + + // Step 2: configure pip index url if specified + if (this._pipIndexUrl) { + await this._configurePip(); + } + + // Step 3: install pip packages if specified + if (this._pip) { + await this._installPip(); + } + } + + private async _validatePython(): Promise { + await this.run('test -x python'); + } + + private async _configurePip(): Promise { + const escapedUrl = this._pipIndexUrl!.replace(/'/g, "'\\''"); + await this.run(`pip config set global.index-url '${escapedUrl}'`); + } + + private async _installPip(): Promise { + if (!this._pip) { + return; + } + + if (typeof this._pip === 'string') { + // Treat as requirements.txt path - note: for remote sandbox, local file upload + // would need to be handled differently. For now, we assume the file is already + // in the sandbox or use the array form. + // This is a simplified implementation - the Python SDK handles local file upload. + await this.run(`pip install -r '${this._pip.replace(/'/g, "'\\''")}'`); + } else { + // Treat as list of packages + const packages = this._pip.map((pkg) => `'${pkg.replace(/'/g, "'\\''")}'`).join(' '); + await this.run(`pip install ${packages}`); + } + } +} \ No newline at end of file diff --git a/rock/ts-sdk/src/sandbox/utils.test.ts b/rock/ts-sdk/src/sandbox/utils.test.ts new file mode 100644 index 000000000..8cf39caa8 --- /dev/null +++ b/rock/ts-sdk/src/sandbox/utils.test.ts @@ -0,0 +1,33 @@ +/** + * Tests for Sandbox Utils + */ + +import { extractNohupPid } from './utils.js'; +import { extractNohupPid as httpExtractNohupPid } from '../utils/http.js'; +import { PID_PREFIX, PID_SUFFIX } from '../common/constants.js'; + +describe('extractNohupPid', () => { + test('should be the same function as in utils/http.ts', () => { + // After refactoring, extractNohupPid should be imported from utils/http.ts + // and re-exported, so they should be the same function reference + expect(extractNohupPid).toBe(httpExtractNohupPid); + }); + + test('should extract PID from valid output', () => { + const output = `some output\n${PID_PREFIX}12345${PID_SUFFIX}\nmore output`; + expect(extractNohupPid(output)).toBe(12345); + }); + + test('should return null for invalid output', () => { + expect(extractNohupPid('no pid here')).toBeNull(); + }); + + test('should return null for empty output', () => { + expect(extractNohupPid('')).toBeNull(); + }); + + test('should handle PID at start of output', () => { + const output = `${PID_PREFIX}99999${PID_SUFFIX}`; + expect(extractNohupPid(output)).toBe(99999); + }); +}); diff --git a/rock/ts-sdk/src/sandbox/utils.ts b/rock/ts-sdk/src/sandbox/utils.ts new file mode 100644 index 000000000..f7206d67c --- /dev/null +++ b/rock/ts-sdk/src/sandbox/utils.ts @@ -0,0 +1,125 @@ +/** + * Sandbox utilities + */ + +import { initLogger } from '../logger.js'; +import { sleep } from '../utils/retry.js'; +import type { Sandbox } from './client.js'; +import type { RunModeType } from '../common/constants.js'; +import type { Observation } from '../types/responses.js'; + +const logger = initLogger('rock.sandbox.utils'); + +/** + * Get the caller's module name for logger naming + */ +function getCallerLoggerName(): string { + const stack = new Error().stack; + if (!stack) return 'unknown'; + + const lines = stack.split('\n'); + // Skip first two lines (Error and this function) + for (let i = 2; i < lines.length; i++) { + const line = lines[i]; + if (line && !line.includes('utils.ts')) { + const match = line.match(/at\s+(?:(?:async\s+)?(?:\w+\.)?(\w+)|(\w+))/); + if (match) { + return match[1] ?? match[2] ?? 'unknown'; + } + } + } + return 'unknown'; +} + +/** + * Decorator to add timing and logging to functions + */ +export function withTimeLogging(operationName: string): MethodDecorator { + return function ( + target: unknown, + propertyKey: string | symbol, + descriptor: PropertyDescriptor + ): PropertyDescriptor { + const originalMethod = descriptor.value; + + descriptor.value = async function (...args: unknown[]): Promise { + const startTime = Date.now(); + const name = getCallerLoggerName(); + const log = initLogger(name); + + log.debug(`${operationName} started`); + + try { + const result = await originalMethod.apply(this, args); + const elapsed = Date.now() - startTime; + log.info(`${operationName} completed (elapsed: ${elapsed / 1000}s)`); + return result; + } catch (e) { + const elapsed = Date.now() - startTime; + log.error(`${operationName} failed: ${e} (elapsed: ${elapsed / 1000}s)`); + throw e; + } + }; + + return descriptor; + }; +} + +/** + * Run command with retry + */ +export async function arunWithRetry( + sandbox: Sandbox, + cmd: string, + session: string, + mode: RunModeType, + options: { + waitTimeout?: number; + waitInterval?: number; + maxAttempts?: number; + errorMsg?: string; + } = {} +): Promise { + const { + waitTimeout = 300, + waitInterval = 10, + maxAttempts = 3, + errorMsg = 'Command failed', + } = options; + + let lastError: Error | null = null; + let currentDelay = 5000; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const result = await sandbox.arun(cmd, { + session, + mode, + waitTimeout, + waitInterval, + }); + + if (result.exitCode !== 0) { + throw new Error( + `${errorMsg} with exit code: ${result.exitCode}, output: ${result.output}` + ); + } + return result; + } catch (e) { + lastError = e instanceof Error ? e : new Error(String(e)); + logger.warn(`Attempt ${attempt}/${maxAttempts} failed: ${lastError.message}`); + + if (attempt < maxAttempts) { + await sleep(currentDelay); + currentDelay *= 2; + } + } + } + + throw lastError ?? new Error(`${errorMsg}: all attempts failed`); +} + +/** + * Re-export extractNohupPid from utils/http.ts to avoid duplication + */ +export { extractNohupPid } from '../utils/http.js'; diff --git a/rock/ts-sdk/src/types/codes.test.ts b/rock/ts-sdk/src/types/codes.test.ts new file mode 100644 index 000000000..d0773deae --- /dev/null +++ b/rock/ts-sdk/src/types/codes.test.ts @@ -0,0 +1,105 @@ +/** + * Tests for Codes and code utilities + */ + +import { + Codes, + ReasonPhrases, + getReasonPhrase, + isSuccess, + isClientError, + isServerError, + isCommandError, + isError, +} from './codes.js'; + +describe('Codes', () => { + test('should have correct values', () => { + expect(Codes.OK).toBe(2000); + expect(Codes.BAD_REQUEST).toBe(4000); + expect(Codes.INTERNAL_SERVER_ERROR).toBe(5000); + expect(Codes.COMMAND_ERROR).toBe(6000); + }); + + test('should have correct reason phrases', () => { + expect(ReasonPhrases[Codes.OK]).toBe('OK'); + expect(ReasonPhrases[Codes.BAD_REQUEST]).toBe('Bad Request'); + expect(ReasonPhrases[Codes.INTERNAL_SERVER_ERROR]).toBe('Internal Server Error'); + expect(ReasonPhrases[Codes.COMMAND_ERROR]).toBe('Command Error'); + }); +}); + +describe('getReasonPhrase', () => { + test('should return correct phrase for valid codes', () => { + expect(getReasonPhrase(Codes.OK)).toBe('OK'); + expect(getReasonPhrase(Codes.BAD_REQUEST)).toBe('Bad Request'); + }); + + test('should return empty string for invalid codes', () => { + expect(getReasonPhrase(9999 as Codes)).toBe(''); + }); +}); + +describe('isSuccess', () => { + test('should return true for 2xxx codes', () => { + expect(isSuccess(Codes.OK)).toBe(true); + expect(isSuccess(2001 as Codes)).toBe(true); + expect(isSuccess(2999 as Codes)).toBe(true); + }); + + test('should return false for non-2xxx codes', () => { + expect(isSuccess(Codes.BAD_REQUEST)).toBe(false); + expect(isSuccess(Codes.INTERNAL_SERVER_ERROR)).toBe(false); + }); +}); + +describe('isClientError', () => { + test('should return true for 4xxx codes', () => { + expect(isClientError(Codes.BAD_REQUEST)).toBe(true); + expect(isClientError(4001 as Codes)).toBe(true); + expect(isClientError(4999 as Codes)).toBe(true); + }); + + test('should return false for non-4xxx codes', () => { + expect(isClientError(Codes.OK)).toBe(false); + expect(isClientError(Codes.INTERNAL_SERVER_ERROR)).toBe(false); + }); +}); + +describe('isServerError', () => { + test('should return true for 5xxx codes', () => { + expect(isServerError(Codes.INTERNAL_SERVER_ERROR)).toBe(true); + expect(isServerError(5001 as Codes)).toBe(true); + expect(isServerError(5999 as Codes)).toBe(true); + }); + + test('should return false for non-5xxx codes', () => { + expect(isServerError(Codes.OK)).toBe(false); + expect(isServerError(Codes.BAD_REQUEST)).toBe(false); + }); +}); + +describe('isCommandError', () => { + test('should return true for 6xxx codes', () => { + expect(isCommandError(Codes.COMMAND_ERROR)).toBe(true); + expect(isCommandError(6001 as Codes)).toBe(true); + expect(isCommandError(6999 as Codes)).toBe(true); + }); + + test('should return false for non-6xxx codes', () => { + expect(isCommandError(Codes.OK)).toBe(false); + expect(isCommandError(Codes.BAD_REQUEST)).toBe(false); + }); +}); + +describe('isError', () => { + test('should return true for error codes (4xxx-6xxx)', () => { + expect(isError(Codes.BAD_REQUEST)).toBe(true); + expect(isError(Codes.INTERNAL_SERVER_ERROR)).toBe(true); + expect(isError(Codes.COMMAND_ERROR)).toBe(true); + }); + + test('should return false for success codes', () => { + expect(isError(Codes.OK)).toBe(false); + }); +}); diff --git a/rock/ts-sdk/src/types/codes.ts b/rock/ts-sdk/src/types/codes.ts new file mode 100644 index 000000000..2aad52840 --- /dev/null +++ b/rock/ts-sdk/src/types/codes.ts @@ -0,0 +1,80 @@ +/** + * ROCK status codes enumeration + */ + +/** + * Status codes with phrase descriptions + */ +export enum Codes { + /** + * Success codes (2xxx) + */ + OK = 2000, + + /** + * Client error codes (4xxx) + */ + BAD_REQUEST = 4000, + + /** + * Server error codes (5xxx) + */ + INTERNAL_SERVER_ERROR = 5000, + + /** + * Command/execution error codes (6xxx) + */ + COMMAND_ERROR = 6000, +} + +/** + * Human-readable reason phrases for status codes + */ +export const ReasonPhrases: Record = { + [Codes.OK]: 'OK', + [Codes.BAD_REQUEST]: 'Bad Request', + [Codes.INTERNAL_SERVER_ERROR]: 'Internal Server Error', + [Codes.COMMAND_ERROR]: 'Command Error', +}; + +/** + * Get the reason phrase for a given status code + */ +export function getReasonPhrase(code: Codes): string { + return ReasonPhrases[code] ?? ''; +} + +/** + * Check if a status code indicates success (2xxx range) + */ +export function isSuccess(code: Codes): boolean { + return code >= 2000 && code <= 2999; +} + +/** + * Check if a status code indicates a client error (4xxx range) + */ +export function isClientError(code: Codes): boolean { + return code >= 4000 && code <= 4999; +} + +/** + * Check if a status code indicates a server error (5xxx range) + */ +export function isServerError(code: Codes): boolean { + return code >= 5000 && code <= 5999; +} + +/** + * Check if a status code indicates a command error (6xxx range) + */ +export function isCommandError(code: Codes): boolean { + return code >= 6000 && code <= 6999; +} + +/** + * Check if a status code indicates any kind of error + */ +export function isError(code: Codes): boolean { + return code >= 4000 && code <= 6999; +} diff --git a/rock/ts-sdk/src/types/index.ts b/rock/ts-sdk/src/types/index.ts new file mode 100644 index 000000000..63de987a8 --- /dev/null +++ b/rock/ts-sdk/src/types/index.ts @@ -0,0 +1,7 @@ +/** + * Types module - Request/Response models + */ + +export * from './codes.js'; +export * from './requests.js'; +export * from './responses.js'; diff --git a/rock/ts-sdk/src/types/requests.ts b/rock/ts-sdk/src/types/requests.ts new file mode 100644 index 000000000..b554a317d --- /dev/null +++ b/rock/ts-sdk/src/types/requests.ts @@ -0,0 +1,104 @@ +/** + * Request types + */ + +import { z } from 'zod'; + +/** + * Command execution request + */ +export const CommandSchema = z.object({ + command: z.union([z.string(), z.array(z.string())]), + timeout: z.number().optional().default(1200), + env: z.record(z.string()).optional(), + cwd: z.string().optional(), +}); + +export type Command = z.infer; + +/** + * Bash session creation request + */ +export const CreateBashSessionRequestSchema = z.object({ + session: z.string().default('default'), + startupSource: z.array(z.string()).default([]), + envEnable: z.boolean().default(false), + env: z.record(z.string()).optional(), + remoteUser: z.string().optional(), +}); + +export type CreateBashSessionRequest = z.infer; + +/** + * Bash action for session execution + */ +export const BashActionSchema = z.object({ + command: z.string(), + session: z.string().default('default'), + timeout: z.number().optional(), + check: z.enum(['silent', 'raise', 'ignore']).default('raise'), +}); + +export type BashAction = z.infer; + +/** + * Write file request + */ +export const WriteFileRequestSchema = z.object({ + content: z.string(), + path: z.string(), +}); + +export type WriteFileRequest = z.infer; + +/** + * Read file request + */ +export const ReadFileRequestSchema = z.object({ + path: z.string(), + encoding: z.string().optional(), + errors: z.string().optional(), +}); + +export type ReadFileRequest = z.infer; + +/** + * Upload file request + */ +export const UploadRequestSchema = z.object({ + sourcePath: z.string(), + targetPath: z.string(), +}); + +export type UploadRequest = z.infer; + +/** + * Close session request + */ +export const CloseSessionRequestSchema = z.object({ + session: z.string().default('default'), +}); + +export type CloseSessionRequest = z.infer; + +/** + * Chown request + */ +export const ChownRequestSchema = z.object({ + remoteUser: z.string(), + paths: z.array(z.string()).default([]), + recursive: z.boolean().default(false), +}); + +export type ChownRequest = z.infer; + +/** + * Chmod request + */ +export const ChmodRequestSchema = z.object({ + paths: z.array(z.string()).default([]), + mode: z.string().default('755'), + recursive: z.boolean().default(false), +}); + +export type ChmodRequest = z.infer; diff --git a/rock/ts-sdk/src/types/responses.test.ts b/rock/ts-sdk/src/types/responses.test.ts new file mode 100644 index 000000000..5f02b3bc0 --- /dev/null +++ b/rock/ts-sdk/src/types/responses.test.ts @@ -0,0 +1,192 @@ +/** + * Tests for API Response parsing + * + * Verifies that API responses are correctly parsed with camelCase fields + * (HTTP layer converts snake_case from API to camelCase) + */ + +import { + SandboxStatusResponseSchema, + IsAliveResponseSchema, + CommandResponseSchema, + ObservationSchema, +} from './responses.js'; + +describe('SandboxStatusResponse', () => { + // Response after HTTP layer conversion (camelCase) + const convertedResponse = { + sandboxId: '295264ad162d43e6af25cf7974a76657', + status: { + imagePull: { + status: 'success', + message: 'use cached image, skip image pull', + }, + dockerRun: { + status: 'success', + message: 'docker run success', + }, + }, + state: null, + portMapping: { + '22555': 50787, + '22': 26571, + '8080': 48803, + }, + hostName: 'etao-jqb011166008116.na131', + hostIp: '11.166.8.116', + isAlive: true, + image: 'python:3.11', + gatewayVersion: '0.0.45', + sweRexVersion: '1.2.17', + userId: 'default', + experimentId: 'default', + namespace: 'default', + cpus: 2.0, + memory: '8g', + }; + + test('should parse converted response correctly', () => { + const result = SandboxStatusResponseSchema.parse(convertedResponse); + + expect(result.sandboxId).toBe('295264ad162d43e6af25cf7974a76657'); + expect(result.hostName).toBe('etao-jqb011166008116.na131'); + expect(result.hostIp).toBe('11.166.8.116'); + expect(result.isAlive).toBe(true); + expect(result.image).toBe('python:3.11'); + expect(result.gatewayVersion).toBe('0.0.45'); + expect(result.sweRexVersion).toBe('1.2.17'); + expect(result.userId).toBe('default'); + expect(result.experimentId).toBe('default'); + expect(result.namespace).toBe('default'); + expect(result.cpus).toBe(2.0); + expect(result.memory).toBe('8g'); + }); + + test('should parse portMapping correctly', () => { + const result = SandboxStatusResponseSchema.parse(convertedResponse); + + expect(result.portMapping).toEqual({ + '22555': 50787, + '22': 26571, + '8080': 48803, + }); + }); + + test('should parse status object correctly', () => { + const result = SandboxStatusResponseSchema.parse(convertedResponse); + + expect(result.status).toEqual({ + imagePull: { + status: 'success', + message: 'use cached image, skip image pull', + }, + dockerRun: { + status: 'success', + message: 'docker run success', + }, + }); + }); + + test('should handle minimal response', () => { + const minimalResponse = { + sandboxId: 'test-id', + isAlive: true, + }; + + const result = SandboxStatusResponseSchema.parse(minimalResponse); + + expect(result.sandboxId).toBe('test-id'); + expect(result.isAlive).toBe(true); + expect(result.hostName).toBeUndefined(); + expect(result.image).toBeUndefined(); + }); + + test('should default isAlive to true if not provided', () => { + const response = { + sandboxId: 'test-id', + }; + + const result = SandboxStatusResponseSchema.parse(response); + + expect(result.isAlive).toBe(true); + }); +}); + +describe('IsAliveResponse', () => { + test('should parse isAlive field correctly', () => { + const result = IsAliveResponseSchema.parse({ + isAlive: true, + message: 'host-name', + }); + + expect(result.isAlive).toBe(true); + expect(result.message).toBe('host-name'); + }); + + test('should default message to empty string', () => { + const result = IsAliveResponseSchema.parse({ + isAlive: false, + }); + + expect(result.message).toBe(''); + }); +}); + +describe('CommandResponse', () => { + test('should parse with camelCase fields', () => { + const result = CommandResponseSchema.parse({ + stdout: 'output', + stderr: '', + exitCode: 0, + }); + + expect(result.stdout).toBe('output'); + expect(result.stderr).toBe(''); + expect(result.exitCode).toBe(0); + }); + + test('should default stdout and stderr to empty strings', () => { + const result = CommandResponseSchema.parse({}); + + expect(result.stdout).toBe(''); + expect(result.stderr).toBe(''); + }); +}); + +describe('Observation', () => { + test('should parse with camelCase fields', () => { + const result = ObservationSchema.parse({ + output: 'command output', + exitCode: 0, + failureReason: '', + expectString: '', + }); + + expect(result.output).toBe('command output'); + expect(result.exitCode).toBe(0); + expect(result.failureReason).toBe(''); + expect(result.expectString).toBe(''); + }); + + test('should handle error response', () => { + const result = ObservationSchema.parse({ + output: '', + exitCode: 1, + failureReason: 'Command failed', + expectString: '', + }); + + expect(result.exitCode).toBe(1); + expect(result.failureReason).toBe('Command failed'); + }); + + test('should default optional fields', () => { + const result = ObservationSchema.parse({ + output: 'test', + }); + + expect(result.exitCode).toBeUndefined(); + expect(result.failureReason).toBe(''); + expect(result.expectString).toBe(''); + }); +}); \ No newline at end of file diff --git a/rock/ts-sdk/src/types/responses.ts b/rock/ts-sdk/src/types/responses.ts new file mode 100644 index 000000000..8d57732ae --- /dev/null +++ b/rock/ts-sdk/src/types/responses.ts @@ -0,0 +1,175 @@ +/** + * Response types + * All field names use camelCase for TypeScript convention + * HTTP layer automatically converts from API snake_case + */ + +import { z } from 'zod'; +import { Codes } from './codes.js'; + +/** + * Base sandbox response + */ +export const SandboxResponseSchema = z.object({ + code: z.nativeEnum(Codes).optional(), + exitCode: z.number().optional(), + failureReason: z.string().optional(), +}); + +export type SandboxResponse = z.infer; + +/** + * Is alive response + */ +export const IsAliveResponseSchema = z.object({ + isAlive: z.boolean(), + message: z.string().default(''), +}); + +export type IsAliveResponse = z.infer; + +/** + * Sandbox status response + */ +export const SandboxStatusResponseSchema = z.object({ + sandboxId: z.string().optional(), + status: z.record(z.unknown()).optional(), + portMapping: z.record(z.unknown()).optional(), + hostName: z.string().optional(), + hostIp: z.string().optional(), + isAlive: z.boolean().default(true), + image: z.string().optional(), + gatewayVersion: z.string().optional(), + sweRexVersion: z.string().optional(), + userId: z.string().optional(), + experimentId: z.string().optional(), + namespace: z.string().optional(), + cpus: z.number().optional(), + memory: z.string().optional(), + state: z.unknown().optional(), + // Response headers info + cluster: z.string().optional(), + requestId: z.string().optional(), + eagleeyeTraceid: z.string().optional(), +}); + +export type SandboxStatusResponse = z.infer; + +/** + * Command execution response + */ +export const CommandResponseSchema = z.object({ + stdout: z.string().default(''), + stderr: z.string().default(''), + exitCode: z.number().optional(), +}); + +export type CommandResponse = z.infer; + +/** + * Write file response + */ +export const WriteFileResponseSchema = z.object({ + success: z.boolean().default(false), + message: z.string().default(''), +}); + +export type WriteFileResponse = z.infer; + +/** + * Read file response + */ +export const ReadFileResponseSchema = z.object({ + content: z.string().default(''), +}); + +export type ReadFileResponse = z.infer; + +/** + * Upload response + */ +export const UploadResponseSchema = z.object({ + success: z.boolean().default(false), + message: z.string().default(''), + fileName: z.string().optional(), +}); + +export type UploadResponse = z.infer; + +/** + * Bash observation (execution result) + */ +export const ObservationSchema = z.object({ + output: z.string().default(''), + exitCode: z.number().optional(), + failureReason: z.string().default(''), + expectString: z.string().default(''), +}); + +export type Observation = z.infer; + +/** + * Create session response + */ +export const CreateSessionResponseSchema = z.object({ + output: z.string().default(''), + sessionType: z.literal('bash').default('bash'), +}); + +export type CreateSessionResponse = z.infer; + +/** + * Close session response + */ +export const CloseSessionResponseSchema = z.object({ + sessionType: z.literal('bash').default('bash'), +}); + +export type CloseSessionResponse = z.infer; + +/** + * Close response + */ +export const CloseResponseSchema = z.object({}); + +export type CloseResponse = z.infer; + +/** + * Chown response + */ +export const ChownResponseSchema = z.object({ + success: z.boolean().default(false), + message: z.string().default(''), +}); + +export type ChownResponse = z.infer; + +/** + * Chmod response + */ +export const ChmodResponseSchema = z.object({ + success: z.boolean().default(false), + message: z.string().default(''), +}); + +export type ChmodResponse = z.infer; + +/** + * Execute bash session response + */ +export const ExecuteBashSessionResponseSchema = z.object({ + success: z.boolean().default(false), + message: z.string().default(''), +}); + +export type ExecuteBashSessionResponse = z.infer; + +/** + * OSS setup response + */ +export const OssSetupResponseSchema = z.object({ + success: z.boolean().default(false), + message: z.string().default(''), +}); + +export type OssSetupResponse = z.infer; \ No newline at end of file diff --git a/rock/ts-sdk/src/utils/case.test.ts b/rock/ts-sdk/src/utils/case.test.ts new file mode 100644 index 000000000..2797fc905 --- /dev/null +++ b/rock/ts-sdk/src/utils/case.test.ts @@ -0,0 +1,129 @@ +/** + * Case conversion utilities tests + */ + +import { objectToCamel, objectToSnake } from './case.js'; + +describe('case conversion', () => { + describe('objectToCamel', () => { + test('converts snake_case keys to camelCase', () => { + const input = { + sandbox_id: '123', + is_alive: true, + host_name: 'localhost', + }; + + const result = objectToCamel(input); + + expect(result).toEqual({ + sandboxId: '123', + isAlive: true, + hostName: 'localhost', + }); + }); + + test('handles nested objects', () => { + const input = { + outer_key: { + inner_key: 'value', + }, + }; + + const result = objectToCamel(input); + + expect(result).toEqual({ + outerKey: { + innerKey: 'value', + }, + }); + }); + + test('handles arrays of objects', () => { + const input = { + items: [ + { item_id: 1, item_name: 'first' }, + { item_id: 2, item_name: 'second' }, + ], + }; + + const result = objectToCamel(input); + + expect(result).toEqual({ + items: [ + { itemId: 1, itemName: 'first' }, + { itemId: 2, itemName: 'second' }, + ], + }); + }); + + test('preserves primitive values', () => { + const input = { + string_val: 'hello', + number_val: 42, + bool_val: true, + null_val: null, + }; + + const result = objectToCamel(input); + + expect(result).toEqual({ + stringVal: 'hello', + numberVal: 42, + boolVal: true, + nullVal: null, + }); + }); + }); + + describe('objectToSnake', () => { + test('converts camelCase keys to snake_case', () => { + const input = { + sandboxId: '123', + isAlive: true, + hostName: 'localhost', + }; + + const result = objectToSnake(input); + + expect(result).toEqual({ + sandbox_id: '123', + is_alive: true, + host_name: 'localhost', + }); + }); + + test('handles nested objects', () => { + const input = { + outerKey: { + innerKey: 'value', + }, + }; + + const result = objectToSnake(input); + + expect(result).toEqual({ + outer_key: { + inner_key: 'value', + }, + }); + }); + + test('handles arrays of objects', () => { + const input = { + items: [ + { itemId: 1, itemName: 'first' }, + { itemId: 2, itemName: 'second' }, + ], + }; + + const result = objectToSnake(input); + + expect(result).toEqual({ + items: [ + { item_id: 1, item_name: 'first' }, + { item_id: 2, item_name: 'second' }, + ], + }); + }); + }); +}); diff --git a/rock/ts-sdk/src/utils/case.ts b/rock/ts-sdk/src/utils/case.ts new file mode 100644 index 000000000..1c0967c10 --- /dev/null +++ b/rock/ts-sdk/src/utils/case.ts @@ -0,0 +1,19 @@ +/** + * Case conversion utilities + * Wraps ts-case-convert for consistent API + */ + +import { + objectToCamel as toCamel, + objectToSnake as toSnake, +} from 'ts-case-convert'; + +/** + * Convert object keys from snake_case to camelCase + */ +export const objectToCamel = toCamel; + +/** + * Convert object keys from camelCase to snake_case + */ +export const objectToSnake = toSnake; diff --git a/rock/ts-sdk/src/utils/deprecated.test.ts b/rock/ts-sdk/src/utils/deprecated.test.ts new file mode 100644 index 000000000..f235202b1 --- /dev/null +++ b/rock/ts-sdk/src/utils/deprecated.test.ts @@ -0,0 +1,242 @@ +/** + * Deprecated decorator tests + */ + +import { deprecated, deprecatedClass, clearDeprecatedWarnings } from './deprecated.js'; +import { initLogger, clearLoggerCache } from '../logger.js'; + +describe('deprecated', () => { + let warnSpy: jest.SpyInstance; + let consoleWarnSpy: jest.SpyInstance; + + beforeEach(() => { + clearLoggerCache(); + clearDeprecatedWarnings(); + // Create a spy on the logger's warn method + const logger = initLogger('rock.deprecated'); + warnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + // Spy on console.warn to verify it's NOT used + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + }); + + afterEach(() => { + warnSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + }); + + describe('warn-once behavior', () => { + test('warns only once when deprecated method is called multiple times', () => { + class TestClass { + @deprecated('Use newMethod instead') + oldMethod(): string { + return 'result'; + } + } + + const instance = new TestClass(); + + // Call the method 3 times + instance.oldMethod(); + instance.oldMethod(); + instance.oldMethod(); + + // Should only warn once + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith('oldMethod is deprecated. Use newMethod instead'); + }); + + test('different deprecated methods each warn once', () => { + class TestClass { + @deprecated('Use method1 instead') + oldMethod1(): string { + return 'result1'; + } + + @deprecated('Use method2 instead') + oldMethod2(): string { + return 'result2'; + } + } + + const instance = new TestClass(); + + instance.oldMethod1(); + instance.oldMethod1(); + instance.oldMethod2(); + instance.oldMethod2(); + + // Each method should warn once + expect(warnSpy).toHaveBeenCalledTimes(2); + expect(warnSpy).toHaveBeenNthCalledWith(1, 'oldMethod1 is deprecated. Use method1 instead'); + expect(warnSpy).toHaveBeenNthCalledWith(2, 'oldMethod2 is deprecated. Use method2 instead'); + }); + + test('different instances share the same warn-once state', () => { + class TestClass { + @deprecated('Use newMethod instead') + oldMethod(): string { + return 'result'; + } + } + + const instance1 = new TestClass(); + const instance2 = new TestClass(); + + instance1.oldMethod(); + instance2.oldMethod(); + + // Should only warn once across instances + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('Winston logger integration', () => { + test('uses Winston logger instead of console.warn', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + class TestClass { + @deprecated('This is deprecated') + deprecatedMethod(): string { + return 'result'; + } + } + + const instance = new TestClass(); + instance.deprecatedMethod(); + + // Should use Winston logger, not console.warn + expect(warnSpy).toHaveBeenCalled(); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + }); + + describe('functionality preservation', () => { + test('deprecated method still returns correct value', () => { + class TestClass { + @deprecated('Use newMethod instead') + oldMethod(value: number): number { + return value * 2; + } + } + + const instance = new TestClass(); + expect(instance.oldMethod(5)).toBe(10); + }); + + test('deprecated method preserves this context', () => { + class TestClass { + private multiplier = 3; + + @deprecated('Use newMethod instead') + oldMethod(value: number): number { + return value * this.multiplier; + } + } + + const instance = new TestClass(); + expect(instance.oldMethod(4)).toBe(12); + }); + + test('deprecated method passes all arguments correctly', () => { + class TestClass { + @deprecated() + sum(a: number, b: number, c: number): number { + return a + b + c; + } + } + + const instance = new TestClass(); + expect(instance.sum(1, 2, 3)).toBe(6); + }); + }); + + describe('default reason', () => { + test('works without providing a reason', () => { + class TestClass { + @deprecated() + oldMethod(): string { + return 'result'; + } + } + + const instance = new TestClass(); + instance.oldMethod(); + + expect(warnSpy).toHaveBeenCalledWith('oldMethod is deprecated. '); + }); + }); +}); + +describe('deprecatedClass', () => { + let warnSpy: jest.SpyInstance; + let consoleWarnSpy: jest.SpyInstance; + + beforeEach(() => { + clearLoggerCache(); + clearDeprecatedWarnings(); + const logger = initLogger('rock.deprecated'); + warnSpy = jest.spyOn(logger, 'warn').mockImplementation(); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + }); + + afterEach(() => { + warnSpy.mockRestore(); + consoleWarnSpy.mockRestore(); + }); + + describe('warn-once behavior', () => { + test('warns only once when deprecated class is instantiated multiple times', () => { + @deprecatedClass('Use NewClass instead') + class OldClass { + value: string; + constructor(value: string) { + this.value = value; + } + } + + // Create 3 instances + new OldClass('a'); + new OldClass('b'); + new OldClass('c'); + + // Should only warn once + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith('OldClass is deprecated. Use NewClass instead'); + }); + }); + + describe('Winston logger integration', () => { + test('uses Winston logger instead of console.warn', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + @deprecatedClass('This is deprecated') + class DeprecatedClass { + constructor() {} + } + + new DeprecatedClass(); + + expect(warnSpy).toHaveBeenCalled(); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + }); + }); + + describe('functionality preservation', () => { + test('deprecated class constructor still works correctly', () => { + @deprecatedClass('Use NewClass instead') + class OldClass { + value: number; + constructor(value: number) { + this.value = value * 2; + } + } + + const instance = new OldClass(5); + expect(instance.value).toBe(10); + }); + }); +}); diff --git a/rock/ts-sdk/src/utils/deprecated.ts b/rock/ts-sdk/src/utils/deprecated.ts new file mode 100644 index 000000000..c05f34c61 --- /dev/null +++ b/rock/ts-sdk/src/utils/deprecated.ts @@ -0,0 +1,77 @@ +/** + * Deprecated decorator utilities + */ + +import { initLogger } from '../logger.js'; +import type { Logger } from '../logger.js'; + +/** + * Set to track which deprecation warnings have already been shown + */ +const warnedKeys = new Set(); + +/** + * Get or create the deprecation logger (lazy initialization) + */ +function getLogger(): Logger { + return initLogger('rock.deprecated'); +} + +/** + * Clear all deprecation warning states (useful for testing) + */ +export function clearDeprecatedWarnings(): void { + warnedKeys.clear(); +} + +/** + * Issue a deprecation warning only once per key + */ +function warnOnce(key: string, message: string): void { + if (warnedKeys.has(key)) { + return; + } + getLogger().warn(message); + warnedKeys.add(key); +} + +/** + * Mark a function as deprecated + */ +export function deprecated(reason: string = ''): MethodDecorator { + return function ( + target: unknown, + propertyKey: string | symbol, + descriptor: PropertyDescriptor + ): PropertyDescriptor { + const originalMethod = descriptor.value; + const key = String(propertyKey); + + descriptor.value = function (...args: unknown[]): unknown { + warnOnce(key, `${key} is deprecated. ${reason}`); + return originalMethod.apply(this, args); + }; + + return descriptor; + }; +} + +/** + * Mark a class as deprecated + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function deprecatedClass(reason: string = ''): any>(constructor: T) => T { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function any>( + constructor: T + ): T { + const key = constructor.name; + return class extends constructor { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(...args: any[]) { + warnOnce(key, `${key} is deprecated. ${reason}`); + super(...args); + } + }; + }; +} diff --git a/rock/ts-sdk/src/utils/http.test.ts b/rock/ts-sdk/src/utils/http.test.ts new file mode 100644 index 000000000..f2140455c --- /dev/null +++ b/rock/ts-sdk/src/utils/http.test.ts @@ -0,0 +1,486 @@ +/** + * HTTP utilities tests - case conversion integration + */ + +import axios, { AxiosError, AxiosResponse } from 'axios'; +import https from 'https'; +import { HttpUtils, HttpResponse, sharedHttpsAgent } from './http.js'; + +// Save the real AxiosError class before mocking axios +// This is necessary for instanceof checks in HTTP error tests +const RealAxiosError = AxiosError; + +// Mock axios +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +/** + * Helper to create a proper AxiosResponse for tests + */ +function createMockResponse(status: number, statusText: string, data: unknown = {}): AxiosResponse { + return { + status, + statusText, + headers: {}, + data, + config: {} as any, + request: {} as any, + }; +} + +/** + * Helper to create an AxiosError with response property properly set + * (needed for Jest environment where constructor doesn't set response) + */ +function createAxiosError( + message: string, + code: string, + status: number, + statusText: string, + data: unknown = {} +): AxiosError { + const error = new RealAxiosError( + message, + code, + { headers: {} } as any, + { url: 'http://test/api', method: 'POST' } as any + ); + // Manually set response property (axios does this internally) + (error as any).response = createMockResponse(status, statusText, data); + return error; +} + +// Mock FormData for Node.js environment +class MockFormData { + private entries: [string, string | Blob][] = []; + + append(key: string, value: string | Blob, filename?: string): void { + this.entries.push([key, value]); + } + + getEntries(): [string, string | Blob][] { + return this.entries; + } +} + +// @ts-expect-error - Mocking global FormData +global.FormData = MockFormData; + +// Mock Blob for Node.js environment +class MockBlob { + private content: Buffer; + private type: string; + + constructor(parts: Buffer[], options?: { type?: string }) { + this.content = Buffer.concat(parts); + this.type = options?.type ?? ''; + } +} + +// Blob may already exist in Node.js 18+, so we use type assertion +(global as unknown as { Blob: typeof MockBlob }).Blob = MockBlob; + +describe('HttpUtils case conversion', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('post', () => { + test('converts request body from camelCase to snake_case', async () => { + const mockPost = jest.fn().mockResolvedValue({ + data: { status: 'Success', result: { sandbox_id: '123' } }, + headers: { 'x-request-id': 'test-request-id' }, + }); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + // Send camelCase request + await HttpUtils.post( + 'http://test/api', + {}, + { sandboxId: 'test-id', isAlive: true } + ); + + // Verify request was converted to snake_case + expect(mockPost).toHaveBeenCalledWith( + 'http://test/api', + expect.objectContaining({ + sandbox_id: 'test-id', + is_alive: true, + }) + ); + }); + + test('converts response from snake_case to camelCase', async () => { + const mockPost = jest.fn().mockResolvedValue({ + data: { + status: 'Success', + result: { + sandbox_id: '123', + host_name: 'localhost', + is_alive: true, + }, + }, + headers: { 'x-request-id': 'test-request-id' }, + }); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + interface TestResponse { + sandboxId: string; + hostName: string; + isAlive: boolean; + } + + const result = await HttpUtils.post( + 'http://test/api', + {}, + { sandboxId: 'test-id' } + ); + + // Verify response was converted to camelCase + expect(result.status).toBe('Success'); + expect(result.result).toEqual({ + sandboxId: '123', + hostName: 'localhost', + isAlive: true, + }); + expect(result.headers).toHaveProperty('x-request-id'); + }); + + test('handles nested objects in response', async () => { + const mockPost = jest.fn().mockResolvedValue({ + data: { + status: 'Success', + result: { + sandbox_id: '123', + port_mapping: { + http_port: 8080, + https_port: 8443, + }, + }, + }, + headers: {}, + }); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + interface TestResult { + sandboxId: string; + portMapping: { + httpPort: number; + httpsPort: number; + }; + } + + const result = await HttpUtils.post( + 'http://test/api', + {}, + {} + ); + + expect(result.result!.portMapping).toEqual({ + httpPort: 8080, + httpsPort: 8443, + }); + }); + }); + + describe('get', () => { + test('converts response from snake_case to camelCase', async () => { + const mockGet = jest.fn().mockResolvedValue({ + data: { + status: 'Success', + result: { + sandbox_id: '123', + is_alive: true, + host_name: 'localhost', + }, + }, + headers: { 'x-request-id': 'test-request-id' }, + }); + mockedAxios.create = jest.fn().mockReturnValue({ get: mockGet }); + + interface TestResponse { + sandboxId: string; + isAlive: boolean; + hostName: string; + } + + const result = await HttpUtils.get('http://test/api', {}); + + expect(result.status).toBe('Success'); + expect(result.result).toEqual({ + sandboxId: '123', + isAlive: true, + hostName: 'localhost', + }); + expect(result.headers).toHaveProperty('x-request-id'); + }); + }); + + describe('postMultipart', () => { + test('converts form data keys from camelCase to snake_case', async () => { + const mockPost = jest.fn().mockResolvedValue({ + data: { status: 'Success', result: null }, + headers: {}, + }); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + await HttpUtils.postMultipart( + 'http://test/upload', + {}, + { targetPath: '/tmp/test', sandboxId: '123' }, + {} + ); + + // Verify FormData was created with snake_case keys + const formData = mockPost.mock.calls[0][1] as MockFormData; + const entries = formData.getEntries(); + + const keys = entries.map(([key]) => key); + expect(keys).toContain('target_path'); + expect(keys).toContain('sandbox_id'); + }); + + test('sets Content-Type to null to let axios auto-detect FormData', async () => { + // This test verifies the fix for: manually setting Content-Type: multipart/form-data + // prevents axios from adding the required boundary parameter. + // The correct behavior is to set Content-Type to null (removing default 'application/json') + // so axios can auto-detect FormData and set Content-Type: multipart/form-data; boundary=xxx + + let capturedHeaders: Record | undefined; + let capturedConfig: { headers?: Record } | undefined; + const mockPost = jest.fn().mockImplementation((_url, _data, config) => { + capturedConfig = config; + return Promise.resolve({ + data: { status: 'Success', result: null }, + headers: {}, + }); + }); + const mockCreate = jest.fn().mockImplementation((config) => { + // Capture the headers passed to axios.create + capturedHeaders = config?.headers; + return { + post: mockPost, + defaults: { headers: { 'Content-Type': 'application/json' } }, + }; + }); + mockedAxios.create = mockCreate; + + await HttpUtils.postMultipart( + 'http://test/upload', + { Authorization: 'Bearer token' }, + { sandboxId: '123' }, + {} + ); + + // CRITICAL: Content-Type should be set to null in the post config + // This removes the default 'application/json' and allows axios to auto-detect FormData + // and set the correct Content-Type with boundary + expect(capturedConfig).toBeDefined(); + expect(capturedConfig?.headers).toHaveProperty('Content-Type', null); + + // Headers passed to createClient should preserve other headers + expect(capturedHeaders).toHaveProperty('Authorization', 'Bearer token'); + }); + + test('adds files to FormData with snake_case field names', async () => { + const mockPost = jest.fn().mockResolvedValue({ + data: { status: 'Success', result: null }, + headers: {}, + }); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + const fileContent = Buffer.from('test file content'); + await HttpUtils.postMultipart( + 'http://test/upload', + {}, + {}, + { myFile: ['test.txt', fileContent, 'text/plain'] } + ); + + const formData = mockPost.mock.calls[0][1] as MockFormData; + const entries = formData.getEntries(); + + // Field name should be converted to snake_case + const fileEntry = entries.find(([key]) => key === 'my_file'); + expect(fileEntry).toBeDefined(); + }); + + test('handles Buffer files correctly', async () => { + const mockPost = jest.fn().mockResolvedValue({ + data: { status: 'Success', result: null }, + headers: {}, + }); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + const fileBuffer = Buffer.from('binary data'); + await HttpUtils.postMultipart( + 'http://test/upload', + {}, + {}, + { file: fileBuffer } + ); + + const formData = mockPost.mock.calls[0][1] as MockFormData; + const entries = formData.getEntries(); + + expect(entries.length).toBe(1); + expect(entries[0]?.[0]).toBe('file'); + }); + }); +}); + +describe('HttpUtils shared https.Agent', () => { + test('exports a shared https.Agent for connection pooling', () => { + // Following Python SDK pattern: _SHARED_SSL_CONTEXT + // The agent should be created once at module level + expect(sharedHttpsAgent).toBeDefined(); + expect(sharedHttpsAgent).toBeInstanceOf(https.Agent); + }); + + test('shared agent has keepAlive enabled for connection reuse', () => { + // keepAlive enables TCP keep-alive sockets for better performance + // Access through options property as per Node.js Agent implementation + expect((sharedHttpsAgent as unknown as { keepAlive: boolean }).keepAlive).toBe(true); + }); + + test('shared agent has rejectUnauthorized enabled for security', () => { + expect((sharedHttpsAgent.options as { rejectUnauthorized?: boolean }).rejectUnauthorized).toBe(true); + }); + + test('multiple requests use the same agent instance', async () => { + const mockResponse = { + data: { status: 'Success', result: { sandbox_id: '123' } }, + headers: {}, + }; + const mockPost = jest.fn().mockResolvedValue(mockResponse); + const mockGet = jest.fn().mockResolvedValue(mockResponse); + + const capturedAgents: https.Agent[] = []; + mockedAxios.create = jest.fn().mockImplementation((config) => { + if (config?.httpsAgent) { + capturedAgents.push(config.httpsAgent); + } + return { post: mockPost, get: mockGet }; + }); + + // Make multiple requests + await HttpUtils.post('http://test/api1', {}, {}); + await HttpUtils.post('http://test/api2', {}, {}); + await HttpUtils.get('http://test/api3', {}); + + // All requests should use the same agent instance (same reference) + expect(capturedAgents.length).toBe(3); + expect(capturedAgents[0]).toBe(sharedHttpsAgent); + expect(capturedAgents[1]).toBe(sharedHttpsAgent); + expect(capturedAgents[2]).toBe(sharedHttpsAgent); + + // All captured agents should be the exact same object reference + expect(capturedAgents[0]).toBe(capturedAgents[1]); + expect(capturedAgents[1]).toBe(capturedAgents[2]); + }); +}); + +describe('HttpUtils HTTP error handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('post', () => { + test('preserves response property in HTTP errors (e.g., 401 Unauthorized)', async () => { + const axiosError = createAxiosError( + 'Request failed with status code 401', + 'ERR_BAD_REQUEST', + 401, + 'Unauthorized', + { error: 'Invalid API key' } + ); + + const mockPost = jest.fn().mockRejectedValue(axiosError); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + const error = await HttpUtils.post('http://test/api', {}, {}).catch(e => e); + + // Verify: error should be AxiosError with response property preserved + expect(error).toBeInstanceOf(RealAxiosError); + expect(error.response).toBeDefined(); + expect(error.response.status).toBe(401); + expect(error.response.statusText).toBe('Unauthorized'); + }); + + test('preserves response property for 403 Forbidden', async () => { + const axiosError = createAxiosError( + 'Request failed with status code 403', + 'ERR_BAD_REQUEST', + 403, + 'Forbidden', + { error: 'Permission denied' } + ); + + const mockPost = jest.fn().mockRejectedValue(axiosError); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + const error = await HttpUtils.post('http://test/api', {}, {}).catch(e => e); + + expect(error).toBeInstanceOf(RealAxiosError); + expect(error.response.status).toBe(403); + }); + + test('preserves response property for 500 Internal Server Error', async () => { + const axiosError = createAxiosError( + 'Request failed with status code 500', + 'ERR_BAD_RESPONSE', + 500, + 'Internal Server Error', + { error: 'Database connection failed' } + ); + + const mockPost = jest.fn().mockRejectedValue(axiosError); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + const error = await HttpUtils.post('http://test/api', {}, {}).catch(e => e); + + expect(error).toBeInstanceOf(RealAxiosError); + expect(error.response.status).toBe(500); + }); + }); + + describe('get', () => { + test('preserves response property in HTTP errors', async () => { + const axiosError = createAxiosError( + 'Request failed with status code 404', + 'ERR_BAD_REQUEST', + 404, + 'Not Found', + { error: 'Resource not found' } + ); + + const mockGet = jest.fn().mockRejectedValue(axiosError); + mockedAxios.create = jest.fn().mockReturnValue({ get: mockGet }); + + const error = await HttpUtils.get('http://test/api', {}).catch(e => e); + + expect(error).toBeInstanceOf(RealAxiosError); + expect(error.response.status).toBe(404); + }); + }); + + describe('postMultipart', () => { + test('preserves response property in HTTP errors', async () => { + const axiosError = createAxiosError( + 'Request failed with status code 413', + 'ERR_BAD_REQUEST', + 413, + 'Payload Too Large', + { error: 'File size exceeds limit' } + ); + + const mockPost = jest.fn().mockRejectedValue(axiosError); + mockedAxios.create = jest.fn().mockReturnValue({ post: mockPost }); + + const error = await HttpUtils.postMultipart('http://test/upload', {}, {}, {}).catch(e => e); + + expect(error).toBeInstanceOf(RealAxiosError); + expect(error.response.status).toBe(413); + }); + }); +}); \ No newline at end of file diff --git a/rock/ts-sdk/src/utils/http.ts b/rock/ts-sdk/src/utils/http.ts new file mode 100644 index 000000000..b556eb045 --- /dev/null +++ b/rock/ts-sdk/src/utils/http.ts @@ -0,0 +1,272 @@ +/** + * HTTP utilities using axios + */ + +import axios, { AxiosInstance, AxiosError, AxiosResponse } from 'axios'; +import https from 'https'; +import { PID_PREFIX, PID_SUFFIX } from '../common/constants.js'; +import { objectToCamel, objectToSnake } from './case.js'; + +/** + * Shared HTTPS agent for connection pooling and TLS session reuse. + * Following Python SDK pattern: _SHARED_SSL_CONTEXT + * + * This prevents performance degradation under load by: + * - Enabling TLS session reuse + * - Enabling connection pooling via keepAlive + */ +export const sharedHttpsAgent = new https.Agent({ + rejectUnauthorized: true, + keepAlive: true, +}); + +/** + * HTTP client configuration + */ +export interface HttpConfig { + baseURL?: string; + timeout?: number; + headers?: Record; +} + +/** + * HTTP response with headers + */ +export interface HttpResponse { + status: string; + result?: T; + error?: string; + headers: Record; +} + +/** + * HTTP utilities class + */ +export class HttpUtils { + private static defaultTimeout = 300000; // 5 minutes + private static defaultConnectTimeout = 300000; + + /** + * Create axios instance with default config + */ + private static createClient(config?: HttpConfig): AxiosInstance { + return axios.create({ + timeout: config?.timeout ?? this.defaultTimeout, + headers: { + 'Content-Type': 'application/json', + ...config?.headers, + }, + httpsAgent: sharedHttpsAgent, + }); + } + + /** + * Send POST request + * Automatically converts request body from camelCase to snake_case + * Automatically converts response from snake_case to camelCase + */ + static async post( + url: string, + headers: Record, + data: Record, + readTimeout?: number + ): Promise> { + const client = this.createClient({ + timeout: readTimeout ?? this.defaultTimeout, + headers, + }); + + // Convert request body to snake_case for API + const snakeData = objectToSnake(data); + + try { + const response = await client.post(url, snakeData); + // Convert response to camelCase for SDK users + const camelData = objectToCamel(response.data as object) as { status?: string; result?: T; error?: string }; + const httpResponse: HttpResponse = { + status: camelData.status ?? 'Success', + headers: this.extractHeaders(response), + }; + if (camelData.result !== undefined) { + httpResponse.result = camelData.result; + } + if (camelData.error !== undefined) { + httpResponse.error = camelData.error; + } + return httpResponse; + } catch (error) { + // Re-throw original error to preserve response property (e.g., status code) + // This allows callers to detect specific HTTP errors like 401, 403, etc. + throw error; + } + } + + /** + * Send GET request + * Automatically converts response from snake_case to camelCase + */ + static async get( + url: string, + headers: Record + ): Promise> { + const client = this.createClient({ headers }); + + try { + const response = await client.get(url); + // Convert response to camelCase for SDK users + const camelData = objectToCamel(response.data as object) as { status?: string; result?: T; error?: string }; + const httpResponse: HttpResponse = { + status: camelData.status ?? 'Success', + headers: this.extractHeaders(response), + }; + if (camelData.result !== undefined) { + httpResponse.result = camelData.result; + } + if (camelData.error !== undefined) { + httpResponse.error = camelData.error; + } + return httpResponse; + } catch (error) { + // Re-throw original error to preserve response property (e.g., status code) + throw error; + } + } + + /** + * Extract headers from axios response + */ + private static extractHeaders(response: AxiosResponse): Record { + const headers: Record = {}; + for (const [key, value] of Object.entries(response.headers)) { + if (typeof value === 'string') { + headers[key.toLowerCase()] = value; + } else if (Array.isArray(value)) { + headers[key.toLowerCase()] = value.join(', '); + } + } + return headers; + } + + /** + * Convert camelCase key to snake_case + */ + private static camelToSnakeKey(key: string): string { + return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + } + + /** + * Send multipart/form-data request + * Automatically converts form data keys to snake_case + * Automatically converts response from snake_case to camelCase + */ + static async postMultipart( + url: string, + headers: Record, + data?: Record, + files?: Record + ): Promise> { + const formData = new FormData(); + + // Add form fields (convert keys to snake_case) + if (data) { + for (const [key, value] of Object.entries(data)) { + if (value !== undefined && value !== null) { + const snakeKey = this.camelToSnakeKey(key); + formData.append(snakeKey, String(value)); + } + } + } + + // Add files (convert field names to snake_case) + if (files) { + for (const [fieldName, fileData] of Object.entries(files)) { + if (fileData !== undefined && fileData !== null) { + const snakeFieldName = this.camelToSnakeKey(fieldName); + if (Array.isArray(fileData)) { + // [filename, content, contentType] + const [filename, content, contentType] = fileData; + const blob = new Blob([content], { type: contentType }); + formData.append(snakeFieldName, blob, filename); + } else if (fileData instanceof Buffer) { + const blob = new Blob([fileData], { type: 'application/octet-stream' }); + formData.append(snakeFieldName, blob, 'file'); + } else if (fileData instanceof File) { + formData.append(snakeFieldName, fileData); + } + } + } + } + + const client = this.createClient({ + headers: { + ...headers, + }, + }); + + try { + // CRITICAL: Use post with config to override Content-Type. + // Setting Content-Type to null tells axios to remove the default + // 'application/json' and auto-detect FormData, setting the correct + // Content-Type with boundary (e.g., 'multipart/form-data; boundary=xxx') + const response = await client.post(url, formData, { + headers: { + 'Content-Type': null, + }, + }); + // Convert response to camelCase for SDK users + const camelData = objectToCamel(response.data as object) as { status?: string; result?: T; error?: string }; + const httpResponse: HttpResponse = { + status: camelData.status ?? 'Success', + headers: this.extractHeaders(response), + }; + if (camelData.result !== undefined) { + httpResponse.result = camelData.result; + } + if (camelData.error !== undefined) { + httpResponse.error = camelData.error; + } + return httpResponse; + } catch (error) { + // Re-throw original error to preserve response property (e.g., status code) + throw error; + } + } + + /** + * Guess MIME type from filename + */ + static guessContentType(filename: string): string { + const ext = filename.split('.').pop()?.toLowerCase(); + const mimeTypes: Record = { + txt: 'text/plain', + html: 'text/html', + css: 'text/css', + js: 'application/javascript', + json: 'application/json', + xml: 'application/xml', + pdf: 'application/pdf', + zip: 'application/zip', + tar: 'application/x-tar', + gz: 'application/gzip', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + svg: 'image/svg+xml', + }; + + return mimeTypes[ext ?? ''] ?? 'application/octet-stream'; + } +} + +/** + * Extract nohup PID from output + */ +export function extractNohupPid(output: string): number | null { + const pattern = new RegExp(`${PID_PREFIX}(\\d+)${PID_SUFFIX}`); + const match = output.match(pattern); + if (match?.[1]) { + return parseInt(match[1], 10); + } + return null; +} diff --git a/rock/ts-sdk/src/utils/index.ts b/rock/ts-sdk/src/utils/index.ts new file mode 100644 index 000000000..9635dfba0 --- /dev/null +++ b/rock/ts-sdk/src/utils/index.ts @@ -0,0 +1,9 @@ +/** + * Utils module - HTTP, Retry, and other utilities + */ + +export * from './case.js'; +export * from './http.js'; +export * from './retry.js'; +export * from './deprecated.js'; +export * from './system.js'; diff --git a/rock/ts-sdk/src/utils/retry.test.ts b/rock/ts-sdk/src/utils/retry.test.ts new file mode 100644 index 000000000..25cdef41d --- /dev/null +++ b/rock/ts-sdk/src/utils/retry.test.ts @@ -0,0 +1,110 @@ +/** + * Tests for Retry utilities + */ + +import { retryAsync, sleep, withRetry } from './retry.js'; + +describe('retryAsync', () => { + test('should succeed on first attempt', async () => { + const fn = jest.fn().mockResolvedValue('success'); + const result = await retryAsync(fn); + + expect(result).toBe('success'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + test('should retry on failure', async () => { + const fn = jest + .fn() + .mockRejectedValueOnce(new Error('fail 1')) + .mockRejectedValueOnce(new Error('fail 2')) + .mockResolvedValue('success'); + + const result = await retryAsync(fn, { maxAttempts: 3, delaySeconds: 0.01 }); + + expect(result).toBe('success'); + expect(fn).toHaveBeenCalledTimes(3); + }); + + test('should throw after max attempts', async () => { + const fn = jest.fn().mockRejectedValue(new Error('always fails')); + + await expect( + retryAsync(fn, { maxAttempts: 3, delaySeconds: 0.01 }) + ).rejects.toThrow('always fails'); + + expect(fn).toHaveBeenCalledTimes(3); + }); + + test('should apply backoff', async () => { + const fn = jest + .fn() + .mockRejectedValueOnce(new Error('fail')) + .mockResolvedValue('success'); + + const startTime = Date.now(); + await retryAsync(fn, { + maxAttempts: 2, + delaySeconds: 0.05, + backoff: 2, + }); + const elapsed = Date.now() - startTime; + + // Should have waited at least 50ms (0.05s) + expect(elapsed).toBeGreaterThanOrEqual(40); + }); + + test('should use exponential backoff by default (backoff=2.0)', async () => { + // Test that default backoff is 2.0, not 1.0 + // With backoff=2.0 and delaySeconds=0.05: + // - After 1st failure: wait 0.05s + // - After 2nd failure: wait 0.10s (0.05 * 2) + // - Total: ~0.15s (150ms) + // With backoff=1.0: + // - After 1st failure: wait 0.05s + // - After 2nd failure: wait 0.05s (0.05 * 1) + // - Total: ~0.10s (100ms) + const fn = jest + .fn() + .mockRejectedValueOnce(new Error('fail 1')) + .mockRejectedValueOnce(new Error('fail 2')) + .mockRejectedValue(new Error('fail 3')); + + const startTime = Date.now(); + await retryAsync(fn, { + maxAttempts: 3, + delaySeconds: 0.05, + // NOT passing backoff - testing default value + }).catch(() => {}); // Ignore final error + const elapsed = Date.now() - startTime; + + // With exponential backoff (2.0), should wait at least 140ms (0.05 + 0.10 = 0.15s) + // With linear backoff (1.0), would only wait about 100ms (0.05 + 0.05 = 0.10s) + expect(elapsed).toBeGreaterThanOrEqual(140); + }); +}); + +describe('sleep', () => { + test('should sleep for specified time', async () => { + const startTime = Date.now(); + await sleep(50); + const elapsed = Date.now() - startTime; + + expect(elapsed).toBeGreaterThanOrEqual(40); + }); +}); + +describe('withRetry', () => { + test('should wrap function with retry logic', async () => { + const fn = jest + .fn() + .mockRejectedValueOnce(new Error('fail')) + .mockResolvedValue('success'); + + const wrapped = withRetry(fn, { maxAttempts: 2, delaySeconds: 0.01 }); + const result = await wrapped(); + + expect(result).toBe('success'); + expect(fn).toHaveBeenCalledTimes(2); + }); +}); diff --git a/rock/ts-sdk/src/utils/retry.ts b/rock/ts-sdk/src/utils/retry.ts new file mode 100644 index 000000000..27d2a5f23 --- /dev/null +++ b/rock/ts-sdk/src/utils/retry.ts @@ -0,0 +1,85 @@ +/** + * Retry utilities + */ + +/** + * Retry options + */ +export interface RetryOptions { + maxAttempts?: number; + delaySeconds?: number; + backoff?: number; + jitter?: boolean; +} + +/** + * Retry decorator for async functions + */ +export function retryAsync( + fn: () => Promise, + options: RetryOptions = {} +): Promise { + const { + maxAttempts = 3, + delaySeconds = 1.0, + backoff = 2.0, + jitter = false, + } = options; + + return retryAsyncImpl(fn, { + maxAttempts, + delaySeconds, + backoff, + jitter, + }); +} + +async function retryAsyncImpl( + fn: () => Promise, + options: Required +): Promise { + const { maxAttempts, delaySeconds, backoff, jitter } = options; + let lastError: Error | null = null; + let currentDelay = delaySeconds; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (e) { + lastError = e instanceof Error ? e : new Error(String(e)); + + if (attempt === maxAttempts) { + break; + } + + let sleepTime = currentDelay; + if (jitter) { + sleepTime = Math.random() * currentDelay * 2; + } + + await sleep(sleepTime * 1000); + currentDelay *= backoff; + } + } + + throw lastError ?? new Error('All retry attempts failed'); +} + +/** + * Sleep utility + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Create a retry wrapper for a function + */ +export function withRetry Promise>( + fn: T, + options: RetryOptions = {} +): T { + return (async (...args: Parameters) => { + return retryAsync(() => fn(...args), options); + }) as T; +} diff --git a/rock/ts-sdk/src/utils/shell.test.ts b/rock/ts-sdk/src/utils/shell.test.ts new file mode 100644 index 000000000..d0fc58500 --- /dev/null +++ b/rock/ts-sdk/src/utils/shell.test.ts @@ -0,0 +1,330 @@ +/** + * Tests for shell utility functions + * These tests verify command injection protection + */ + +import { + shellQuote, + buildCommand, + bashWrap, + SafeString, + validateUrl, + validateIpAddress, + validatePath, + validateUsername, + validateChmodMode, +} from './shell.js'; + +describe('shellQuote', () => { + test('quotes simple string', () => { + expect(shellQuote('hello')).toBe("'hello'"); + }); + + test('quotes string with spaces', () => { + expect(shellQuote('hello world')).toBe("'hello world'"); + }); + + test('escapes single quotes in string', () => { + // Single quotes in input should be escaped + expect(shellQuote("it's")).toBe("'it'\\''s'"); + }); + + test('quotes path with special characters', () => { + expect(shellQuote('/tmp/my file.txt')).toBe("'/tmp/my file.txt'"); + }); + + test('handles injection attempt with semicolon', () => { + // rm -rf / should NOT be executed + const malicious = '/tmp; rm -rf /'; + expect(shellQuote(malicious)).toBe("'/tmp; rm -rf /'"); + }); + + test('handles injection attempt with backticks', () => { + const malicious = '/tmp/`whoami`'; + expect(shellQuote(malicious)).toBe("'/tmp/`whoami`'"); + }); + + test('handles injection attempt with $()', () => { + const malicious = '/tmp/$(whoami)'; + expect(shellQuote(malicious)).toBe("'/tmp/$(whoami)'"); + }); + + test('handles injection attempt with &&', () => { + const malicious = '/tmp && cat /etc/passwd'; + expect(shellQuote(malicious)).toBe("'/tmp && cat /etc/passwd'"); + }); + + test('handles injection attempt with ||', () => { + const malicious = '/tmp || rm -rf /'; + expect(shellQuote(malicious)).toBe("'/tmp || rm -rf /'"); + }); + + test('handles injection attempt with pipe', () => { + const malicious = '/tmp | cat /etc/passwd'; + expect(shellQuote(malicious)).toBe("'/tmp | cat /etc/passwd'"); + }); + + test('handles newline injection attempt', () => { + const malicious = '/tmp\necho pwned'; + expect(shellQuote(malicious)).toBe("'/tmp\necho pwned'"); + }); + + test('handles empty string', () => { + expect(shellQuote('')).toBe("''"); + }); +}); + +describe('buildCommand', () => { + test('builds command from multiple parts', () => { + const cmd = buildCommand(['echo', 'hello world']); + // All parts are quoted for maximum safety + expect(cmd).toBe("'echo' 'hello world'"); + }); + + test('quotes all string parts', () => { + const cmd = buildCommand(['rm', '-rf', '/tmp/my dir']); + expect(cmd).toBe("'rm' '-rf' '/tmp/my dir'"); + }); + + test('does not quote SafeString parts', () => { + const cmd = buildCommand([new SafeString('echo'), new SafeString('hello')]); + expect(cmd).toBe('echo hello'); + }); + + test('handles injection attempts in parts', () => { + const cmd = buildCommand(['echo', '/tmp; rm -rf /']); + // The malicious part is quoted, neutralizing the injection + expect(cmd).toBe("'echo' '/tmp; rm -rf /'"); + }); + + test('mixed SafeString and regular strings', () => { + const cmd = buildCommand([new SafeString('echo'), 'hello world']); + expect(cmd).toBe("echo 'hello world'"); + }); +}); + +describe('bashWrap', () => { + test('wraps command with bash -c', () => { + const result = bashWrap('echo hello'); + expect(result).toBe("bash -c 'echo hello'"); + }); + + test('properly escapes complex command', () => { + const result = bashWrap('rm -rf /tmp/my dir'); + expect(result).toBe("bash -c 'rm -rf /tmp/my dir'"); + }); + + test('handles injection attempts', () => { + const result = bashWrap('echo test; rm -rf /'); + expect(result).toBe("bash -c 'echo test; rm -rf /'"); + // The entire string is quoted, so the injection is neutralized + }); +}); + +describe('validateUrl', () => { + test('accepts valid http URL', () => { + expect(validateUrl('http://example.com')).toBe('http://example.com'); + }); + + test('accepts valid https URL', () => { + expect(validateUrl('https://example.com')).toBe('https://example.com'); + }); + + test('removes trailing slash', () => { + expect(validateUrl('https://example.com/')).toBe('https://example.com'); + }); + + test('rejects ftp URL', () => { + expect(() => validateUrl('ftp://example.com')).toThrow('Protocol ftp:'); + }); + + test('rejects invalid URL', () => { + expect(() => validateUrl('not a url')).toThrow('Invalid URL'); + }); + + test('rejects javascript URL', () => { + expect(() => validateUrl('javascript:alert(1)')).toThrow('Protocol javascript:'); + }); + + test('rejects file URL', () => { + expect(() => validateUrl('file:///etc/passwd')).toThrow('Protocol file:'); + }); + + test('accepts URL with path', () => { + expect(validateUrl('https://example.com/path/to/resource')).toBe( + 'https://example.com/path/to/resource' + ); + }); + + test('accepts URL with port', () => { + expect(validateUrl('https://example.com:8080')).toBe('https://example.com:8080'); + }); +}); + +describe('validateIpAddress', () => { + test('accepts valid IP address', () => { + expect(validateIpAddress('192.168.1.1')).toBe('192.168.1.1'); + }); + + test('accepts 0.0.0.0', () => { + expect(validateIpAddress('0.0.0.0')).toBe('0.0.0.0'); + }); + + test('accepts 255.255.255.255', () => { + expect(validateIpAddress('255.255.255.255')).toBe('255.255.255.255'); + }); + + test('rejects IP with octet > 255', () => { + expect(() => validateIpAddress('192.168.1.256')).toThrow('Invalid IP address'); + }); + + test('rejects IP with missing octet', () => { + expect(() => validateIpAddress('192.168.1')).toThrow('Invalid IP address format'); + }); + + test('rejects IP with extra octet', () => { + expect(() => validateIpAddress('192.168.1.1.1')).toThrow('Invalid IP address format'); + }); + + test('rejects non-numeric IP', () => { + expect(() => validateIpAddress('192.168.1.abc')).toThrow('Invalid IP address format'); + }); + + test('rejects IP with injection attempt', () => { + expect(() => validateIpAddress('192.168.1.1; rm -rf /')).toThrow('Invalid IP address format'); + }); + + test('rejects empty string', () => { + expect(() => validateIpAddress('')).toThrow('Invalid IP address format'); + }); +}); + +describe('validatePath', () => { + test('accepts valid absolute path', () => { + expect(() => validatePath('/tmp/file.txt')).not.toThrow(); + }); + + test('accepts path with subdirectories', () => { + expect(() => validatePath('/tmp/dir/file.txt')).not.toThrow(); + }); + + test('rejects relative path', () => { + expect(() => validatePath('tmp/file.txt')).toThrow('must be absolute'); + }); + + test('rejects path traversal with ..', () => { + expect(() => validatePath('/tmp/../etc/passwd')).toThrow('cannot contain ..'); + }); + + test('rejects path with backticks', () => { + expect(() => validatePath('/tmp/`whoami`')).toThrow('forbidden characters'); + }); + + test('rejects path with $()', () => { + expect(() => validatePath('/tmp/$(whoami)')).toThrow('forbidden characters'); + }); + + test('rejects path with semicolon', () => { + expect(() => validatePath('/tmp; rm -rf /')).toThrow('forbidden characters'); + }); + + test('rejects path with pipe', () => { + expect(() => validatePath('/tmp | cat')).toThrow('forbidden characters'); + }); + + test('rejects path with &&', () => { + expect(() => validatePath('/tmp && echo pwned')).toThrow('forbidden characters'); + }); +}); + +describe('SafeString', () => { + test('creates SafeString with value', () => { + const safe = new SafeString('trusted'); + expect(safe.value).toBe('trusted'); + }); + + test('SafeString instance check works', () => { + const safe = new SafeString('trusted'); + expect(safe instanceof SafeString).toBe(true); + }); +}); + +describe('validateUsername', () => { + test('accepts valid username', () => { + expect(() => validateUsername('root')).not.toThrow(); + }); + + test('accepts username with underscore', () => { + expect(() => validateUsername('valid_user')).not.toThrow(); + }); + + test('accepts username with hyphen', () => { + expect(() => validateUsername('valid-user')).not.toThrow(); + }); + + test('accepts username starting with letter', () => { + expect(() => validateUsername('user123')).not.toThrow(); + }); + + test('accepts username starting with underscore', () => { + expect(() => validateUsername('_system')).not.toThrow(); + }); + + test('rejects username starting with dash', () => { + expect(() => validateUsername('-rf')).toThrow('not allowed'); + }); + + test('rejects username starting with digit', () => { + expect(() => validateUsername('123user')).toThrow('Invalid username'); + }); + + test('rejects username with semicolon', () => { + expect(() => validateUsername('user;rm')).toThrow('Invalid username'); + }); + + test('rejects username with space', () => { + expect(() => validateUsername('user name')).toThrow('Invalid username'); + }); + + test('rejects empty username', () => { + expect(() => validateUsername('')).toThrow('cannot be empty'); + }); +}); + +describe('validateChmodMode', () => { + test('accepts valid octal mode (3 digits)', () => { + expect(() => validateChmodMode('755')).not.toThrow(); + }); + + test('accepts valid octal mode (4 digits)', () => { + expect(() => validateChmodMode('0755')).not.toThrow(); + }); + + test('accepts valid symbolic mode', () => { + expect(() => validateChmodMode('u+x')).not.toThrow(); + }); + + test('accepts symbolic mode with multiple clauses', () => { + expect(() => validateChmodMode('u+x,go-w')).not.toThrow(); + }); + + test('accepts symbolic mode with a (all)', () => { + expect(() => validateChmodMode('a+r')).not.toThrow(); + }); + + test('rejects mode with semicolon', () => { + expect(() => validateChmodMode('755; rm -rf /')).toThrow('Invalid chmod mode'); + }); + + test('rejects mode with invalid octal digit', () => { + expect(() => validateChmodMode('789')).toThrow('Invalid chmod mode'); + }); + + test('rejects non-octal letters', () => { + expect(() => validateChmodMode('abc')).toThrow('Invalid chmod mode'); + }); + + test('rejects empty mode', () => { + expect(() => validateChmodMode('')).toThrow('cannot be empty'); + }); +}); diff --git a/rock/ts-sdk/src/utils/shell.ts b/rock/ts-sdk/src/utils/shell.ts new file mode 100644 index 000000000..a4aa66d8d --- /dev/null +++ b/rock/ts-sdk/src/utils/shell.ts @@ -0,0 +1,236 @@ +/** + * Shell utility functions for safe command construction + * Provides protection against command injection attacks + * + * Inspired by Python's shlex module and security best practices + */ + +/** + * Safely escape a string for use in shell commands + * This is the TypeScript equivalent of Python's shlex.quote() + * + * The algorithm wraps the string in single quotes and escapes any + * existing single quotes by ending the quote, adding an escaped quote, + * and starting a new quote. + * + * @param str - The string to escape + * @returns The escaped string, safe for shell use + * + * @example + * ```typescript + * shellQuote('hello'); // Returns: 'hello' + * shellQuote("it's"); // Returns: 'it'\''s' + * shellQuote('/tmp; rm -rf /'); // Returns: '/tmp; rm -rf /' + * ``` + */ +export function shellQuote(str: string): string { + // Empty string + if (str === '') { + return "''"; + } + + // If string only contains safe characters (alphanumeric, underscore, hyphen, dot, slash) + // we could potentially skip quoting, but for maximum safety we always quote. + // This follows the principle of "defense in depth". + + // Wrap in single quotes and escape any single quotes within + // The pattern: 'str' becomes 'str'\''str' for each embedded quote + return `'${str.replace(/'/g, "'\\''")}'`; +} + +/** + * Mark a string as safe (not to be escaped) + * Use with caution - only for trusted, pre-validated strings + */ +export class SafeString { + constructor(public readonly value: string) {} +} + +/** + * Build a safe command from parts + * Strings are automatically quoted, SafeString instances are passed through + * + * @param parts - Array of command parts (strings or SafeString) + * @returns The constructed command string + * + * @example + * ```typescript + * const cmd = buildCommand(['echo', 'hello world']); + * // Returns: "echo 'hello world'" + * + * const cmd2 = buildCommand(['rm', '-rf', new SafeString('/tmp/*')]); + * // Returns: "rm -rf /tmp/*" + * ``` + */ +export function buildCommand(parts: Array): string { + return parts.map((p) => (p instanceof SafeString ? p.value : shellQuote(p))).join(' '); +} + +/** + * Wrap a command with bash -c safely + * The entire command string is quoted to prevent injection + * + * @param cmd - The command to wrap + * @returns The wrapped command string + * + * @example + * ```typescript + * const wrapped = bashWrap('echo "hello world"'); + * // Returns: "bash -c 'echo \"hello world\"'" + * ``` + */ +export function bashWrap(cmd: string): string { + return `bash -c ${shellQuote(cmd)}`; +} + +/** + * Validate and normalize a URL + * Returns the normalized URL or throws an error + * + * @param url - The URL to validate + * @param allowedProtocols - Array of allowed protocols (default: ['http:', 'https:']) + * @returns The normalized URL (trailing slash removed) + * @throws Error if URL is invalid or uses disallowed protocol + * + * @example + * ```typescript + * validateUrl('https://example.com/'); // Returns 'https://example.com' + * validateUrl('ftp://example.com'); // Throws error + * ``` + */ +export function validateUrl(url: string, allowedProtocols = ['http:', 'https:']): string { + try { + const parsed = new URL(url); + if (!allowedProtocols.includes(parsed.protocol)) { + throw new Error( + `Protocol ${parsed.protocol} is not allowed. Allowed: ${allowedProtocols.join(', ')}` + ); + } + // Remove trailing slash for consistency + return url.replace(/\/$/, ''); + } catch (e) { + if (e instanceof Error && e.message.includes('Protocol')) { + throw e; + } + throw new Error(`Invalid URL: ${url}. ${e instanceof Error ? e.message : String(e)}`); + } +} + +/** + * Validate an IPv4 address + * Returns the validated IP or throws an error + * + * @param ip - The IP address to validate + * @returns The validated IP address + * @throws Error if IP format is invalid + * + * @example + * ```typescript + * validateIpAddress('192.168.1.1'); // Returns '192.168.1.1' + * validateIpAddress('192.168.1.256'); // Throws error + * ``` + */ +export function validateIpAddress(ip: string): string { + const pattern = /^(\d{1,3}\.){3}\d{1,3}$/; + if (!pattern.test(ip)) { + throw new Error(`Invalid IP address format: ${ip}. Expected format: x.x.x.x`); + } + const octets = ip.split('.'); + for (const octet of octets) { + const value = parseInt(octet, 10); + if (value < 0 || value > 255) { + throw new Error(`Invalid IP address: ${ip}. Each octet must be 0-255.`); + } + } + return ip; +} + +/** + * Validate a file path for basic security + * Checks for: + * - Absolute path requirement + * - Path traversal attempts (..) + * - Forbidden shell metacharacters + * + * @param path - The path to validate + * @throws Error if path fails validation + * + * @example + * ```typescript + * validatePath('/tmp/file.txt'); // OK + * validatePath('/tmp/../etc/passwd'); // Throws error + * validatePath('/tmp/$(whoami)'); // Throws error + * ``` + */ +export function validatePath(path: string): void { + if (!path.startsWith('/')) { + throw new Error(`Path must be absolute: ${path}`); + } + if (path.includes('..')) { + throw new Error(`Path cannot contain ..: ${path}`); + } + // Check for shell metacharacters that could enable injection + if (/[`$(){};|&<>()]/.test(path)) { + throw new Error(`Path contains forbidden characters: ${path}`); + } +} + +/** + * Validate a Unix username + * Only allows alphanumeric characters and underscore + * Cannot start with a dash or digit + * + * @param username - The username to validate + * @throws Error if username fails validation + * + * @example + * ```typescript + * validateUsername('root'); // OK + * validateUsername('valid_user'); // OK + * validateUsername('-rf'); // Throws error + * validateUsername('user;rm'); // Throws error + * ``` + */ +export function validateUsername(username: string): void { + if (username.length === 0) { + throw new Error('Username cannot be empty'); + } + // Cannot start with dash (could be interpreted as option) + if (username.startsWith('-')) { + throw new Error(`Username starting with dash is not allowed: ${username}`); + } + // Only allow alphanumeric and underscore (POSIX username rules) + if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(username)) { + throw new Error(`Invalid username format: ${username}. Only alphanumeric, underscore, and hyphen allowed.`); + } +} + +/** + * Validate a chmod mode string + * Accepts octal (e.g., '755', '0755') or symbolic modes (e.g., 'u+x', 'go-w') + * + * @param mode - The mode string to validate + * @throws Error if mode fails validation + * + * @example + * ```typescript + * validateChmodMode('755'); // OK + * validateChmodMode('u+x'); // OK + * validateChmodMode('abc'); // Throws error + * ``` + */ +export function validateChmodMode(mode: string): void { + if (mode.length === 0) { + throw new Error('Mode cannot be empty'); + } + // Octal mode: 3-4 digits (optional leading 0) + if (/^[0-7]{3,4}$/.test(mode)) { + return; + } + // Symbolic mode: [ugoa...][+-=][rwxXst...] + // Multiple clauses separated by comma + if (/^[ugoa]*[+-=][rwxXstugo]*([,][ugoa]*[+-=][rwxXstugo]*)*$/.test(mode)) { + return; + } + throw new Error(`Invalid chmod mode: ${mode}. Expected octal (e.g., 755) or symbolic (e.g., u+x)`); +} \ No newline at end of file diff --git a/rock/ts-sdk/src/utils/system.test.ts b/rock/ts-sdk/src/utils/system.test.ts new file mode 100644 index 000000000..0a147068c --- /dev/null +++ b/rock/ts-sdk/src/utils/system.test.ts @@ -0,0 +1,121 @@ +/** + * Tests for system utilities + * + * These tests verify that the SDK explicitly requires Node.js environment + * and does not include misleading browser compatibility checks. + */ + +import { isNode, getEnv, getRequiredEnv, isEnvSet } from './system.js'; + +describe('system utilities', () => { + describe('isNode', () => { + test('returns true in Node.js environment', () => { + expect(isNode()).toBe(true); + }); + }); + + describe('getEnv', () => { + const originalEnv = process.env; + + beforeEach(() => { + // Reset process.env for each test + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + test('returns environment variable value when set', () => { + process.env.TEST_VAR = 'test-value'; + expect(getEnv('TEST_VAR')).toBe('test-value'); + }); + + test('returns undefined when variable not set and no default', () => { + delete process.env.NONEXISTENT_VAR; + expect(getEnv('NONEXISTENT_VAR')).toBeUndefined(); + }); + + test('returns default value when variable not set', () => { + delete process.env.NONEXISTENT_VAR; + expect(getEnv('NONEXISTENT_VAR', 'default-value')).toBe('default-value'); + }); + + test('returns actual value even when default is provided', () => { + process.env.TEST_VAR = 'actual-value'; + expect(getEnv('TEST_VAR', 'default-value')).toBe('actual-value'); + }); + }); + + describe('getRequiredEnv', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + test('returns value when environment variable is set', () => { + process.env.REQUIRED_VAR = 'required-value'; + expect(getRequiredEnv('REQUIRED_VAR')).toBe('required-value'); + }); + + test('throws error when environment variable is not set', () => { + delete process.env.NONEXISTENT_REQUIRED_VAR; + expect(() => getRequiredEnv('NONEXISTENT_REQUIRED_VAR')).toThrow( + 'Required environment variable NONEXISTENT_REQUIRED_VAR is not set' + ); + }); + }); + + describe('isEnvSet', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + test('returns true when environment variable is set', () => { + process.env.SET_VAR = 'value'; + expect(isEnvSet('SET_VAR')).toBe(true); + }); + + test('returns true when environment variable is set to empty string', () => { + process.env.EMPTY_VAR = ''; + expect(isEnvSet('EMPTY_VAR')).toBe(true); + }); + + test('returns false when environment variable is not set', () => { + delete process.env.UNSET_VAR; + expect(isEnvSet('UNSET_VAR')).toBe(false); + }); + }); +}); + +/** + * Test that isBrowser is NOT exported from the main index. + * This SDK requires Node.js and should not have misleading browser checks. + */ +describe('Node.js only requirement', () => { + test('isBrowser should not be exported from main index', async () => { + const mainModule = await import('../index.js'); + + // isBrowser should NOT be exported - this is intentional + expect('isBrowser' in mainModule).toBe(false); + }); + + test('isBrowser should not be exported from system.js', async () => { + // Re-import to check the module's exports + const systemModule = await import('./system.js'); + + // isBrowser should NOT be exported + expect('isBrowser' in systemModule).toBe(false); + }); +}); diff --git a/rock/ts-sdk/src/utils/system.ts b/rock/ts-sdk/src/utils/system.ts new file mode 100644 index 000000000..46fb70302 --- /dev/null +++ b/rock/ts-sdk/src/utils/system.ts @@ -0,0 +1,44 @@ +/** + * System utilities + * + * Note: This SDK requires Node.js environment (v20.8.0+). + * It cannot run in browsers due to dependencies on Node.js modules. + */ + +/** + * Check if running in Node.js environment + */ +export function isNode(): boolean { + return ( + typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null + ); +} + +/** + * Get environment variable + * Directly accesses process.env since this SDK requires Node.js + */ +export function getEnv(key: string, defaultValue?: string): string | undefined { + return process.env[key] ?? defaultValue; +} + +/** + * Get required environment variable + */ +export function getRequiredEnv(key: string): string { + const value = getEnv(key); + if (value === undefined) { + throw new Error(`Required environment variable ${key} is not set`); + } + return value; +} + +/** + * Check if environment variable is set + * Directly checks process.env since this SDK requires Node.js + */ +export function isEnvSet(key: string): boolean { + return key in process.env; +} diff --git a/rock/ts-sdk/tests/integration/esm-shims.test.ts b/rock/ts-sdk/tests/integration/esm-shims.test.ts new file mode 100644 index 000000000..28263854f --- /dev/null +++ b/rock/ts-sdk/tests/integration/esm-shims.test.ts @@ -0,0 +1,84 @@ +/** + * Test for ESM shims compatibility + * + * This test verifies that __dirname is correctly shimmed in ESM builds. + * Without shims, __dirname is undefined in ESM modules. + */ + +import { execSync } from 'child_process'; +import { existsSync, readFileSync } from 'fs'; +import { join } from 'path'; + +describe('ESM Shims', () => { + const distDir = join(__dirname, '../../dist'); + const esmEntryPath = join(distDir, 'index.mjs'); + + beforeAll(() => { + // Build the project first + execSync('pnpm build', { cwd: join(__dirname, '../..'), stdio: 'inherit' }); + }); + + it('should have ESM output file', () => { + expect(existsSync(esmEntryPath)).toBe(true); + }); + + it('should shim __dirname in ESM build', () => { + // Read the ESM output + const esmContent = readFileSync(esmEntryPath, 'utf-8'); + + // Check if __dirname is properly shimmed + // When shims are enabled, tsup should inject the shim code: + // var __dirname = path.dirname(fileURLToPath(import.meta.url)) + const hasDirnameShim = + esmContent.includes('fileURLToPath') && + esmContent.includes('import.meta.url'); + + expect(hasDirnameShim).toBe(true); + }); + + it('should have valid __dirname value when ESM module is loaded', async () => { + // Write test script to a temp file to avoid shell escaping issues + const { writeFileSync, unlinkSync } = await import('fs'); + const { tmpdir } = await import('os'); + const tempFile = join(tmpdir(), 'test-esm-shim.mjs'); + + const testScript = ` + import { fileURLToPath } from 'url'; + import { dirname, resolve } from 'path'; + + // Simulate what tsup shims should inject + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + + // The __dirname should be a valid absolute path + if (typeof __dirname !== 'string') { + throw new Error('__dirname is not a string: ' + typeof __dirname); + } + if (!__dirname.startsWith('/')) { + throw new Error('__dirname is not an absolute path: ' + __dirname); + } + + // Resolve should work with __dirname + const serverPath = resolve(__dirname, 'server'); + if (!serverPath.startsWith('/')) { + throw new Error('resolve did not produce absolute path'); + } + + console.log(JSON.stringify({ __dirname, serverPath })); + `; + + try { + writeFileSync(tempFile, testScript); + const result = execSync(`node ${tempFile}`, { + encoding: 'utf-8', + }); + + const parsed = JSON.parse(result); + expect(parsed.__dirname).toBeDefined(); + expect(typeof parsed.__dirname).toBe('string'); + expect(parsed.__dirname.startsWith('/')).toBe(true); + } finally { + unlinkSync(tempFile); + } + }); +}); diff --git a/rock/ts-sdk/tests/integration/file-system.test.ts b/rock/ts-sdk/tests/integration/file-system.test.ts new file mode 100644 index 000000000..263582a68 --- /dev/null +++ b/rock/ts-sdk/tests/integration/file-system.test.ts @@ -0,0 +1,154 @@ +/** + * Integration test for FileSystem operations + * + * Prerequisites: + * - ROCK_BASE_URL environment variable or default baseUrl + * - Access to the ROCK sandbox service + * - Valid Docker image with tar command + */ + +import { Sandbox, RunMode } from '../../src'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +const TEST_CONFIG = { + baseUrl: process.env.ROCK_BASE_URL || 'http://11.166.8.116:8080', + image: 'reg.docker.alibaba-inc.com/yanan/python:3.11', + cluster: 'zb', + startupTimeout: 120, +}; + +describe('FileSystem Integration', () => { + let sandbox: Sandbox; + let tempDir: string; + + beforeEach(async () => { + sandbox = new Sandbox(TEST_CONFIG); + await sandbox.start(); + + // Create default session for NORMAL mode commands (new behavior: arun() no longer auto-creates sessions) + await sandbox.createSession({ session: 'default', startupSource: [], envEnable: false }); + + // Create a temporary directory for test files + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rock-test-')); + }, 180000); // 3 minutes timeout for sandbox startup + + afterEach(async () => { + // Cleanup: ensure sandbox is stopped even if test fails + if (sandbox) { + try { + await sandbox.close(); + } catch (e) { + // Ignore cleanup errors + } + } + + // Cleanup local temp directory + if (tempDir && fs.existsSync(tempDir)) { + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (e) { + // Ignore cleanup errors + } + } + }); + + describe('uploadDir', () => { + test('should upload a directory to sandbox', async () => { + // Arrange: Create a local directory with files + const sourceDir = path.join(tempDir, 'source'); + fs.mkdirSync(sourceDir, { recursive: true }); + fs.writeFileSync(path.join(sourceDir, 'file1.txt'), 'Hello World'); + fs.writeFileSync(path.join(sourceDir, 'file2.txt'), 'Test Content'); + fs.mkdirSync(path.join(sourceDir, 'subdir'), { recursive: true }); + fs.writeFileSync(path.join(sourceDir, 'subdir', 'file3.txt'), 'Nested File'); + + const targetDir = '/tmp/uploaded_test_dir'; + + // Act: Upload the directory + const result = await sandbox.getFs().uploadDir(sourceDir, targetDir); + + // Assert: Upload should succeed + expect(result.exitCode).toBe(0); + expect(result.failureReason).toBe(''); + + // Assert: Files should exist in sandbox + const listResult = await sandbox.arun(`ls -la ${targetDir}`, { mode: RunMode.NORMAL }); + expect(listResult.exitCode).toBe(0); + expect(listResult.output).toContain('file1.txt'); + expect(listResult.output).toContain('file2.txt'); + expect(listResult.output).toContain('subdir'); + + // Assert: File contents should match + const contentResult = await sandbox.arun(`cat ${targetDir}/file1.txt`, { mode: RunMode.NORMAL }); + expect(contentResult.output.trim()).toBe('Hello World'); + + // Assert: Nested files should exist + const nestedResult = await sandbox.arun(`cat ${targetDir}/subdir/file3.txt`, { mode: RunMode.NORMAL }); + expect(nestedResult.output.trim()).toBe('Nested File'); + }, 180000); + + test('should return error when source directory does not exist', async () => { + const nonExistentDir = path.join(tempDir, 'nonexistent'); + const targetDir = '/tmp/should_not_exist'; + + const result = await sandbox.getFs().uploadDir(nonExistentDir, targetDir); + + expect(result.exitCode).toBe(1); + expect(result.failureReason).toContain('source_dir not found'); + }); + + test('should return error when source is not a directory', async () => { + const filePath = path.join(tempDir, 'notadir.txt'); + fs.writeFileSync(filePath, 'I am a file, not a directory'); + const targetDir = '/tmp/should_not_exist'; + + const result = await sandbox.getFs().uploadDir(filePath, targetDir); + + expect(result.exitCode).toBe(1); + expect(result.failureReason).toContain('source_dir must be a directory'); + }); + + test('should return error when target path is not absolute', async () => { + const sourceDir = path.join(tempDir, 'source'); + fs.mkdirSync(sourceDir, { recursive: true }); + fs.writeFileSync(path.join(sourceDir, 'file.txt'), 'content'); + + const relativeTarget = 'relative/path'; + + const result = await sandbox.getFs().uploadDir(sourceDir, relativeTarget); + + expect(result.exitCode).toBe(1); + expect(result.failureReason).toMatch(/must be absolute|Path must be absolute/i); + }); + + test('should overwrite existing target directory', async () => { + // Arrange: Create source directory + const sourceDir = path.join(tempDir, 'source'); + fs.mkdirSync(sourceDir, { recursive: true }); + fs.writeFileSync(path.join(sourceDir, 'newfile.txt'), 'New Content'); + + const targetDir = '/tmp/overwrite_test_dir'; + + // Create existing directory in sandbox with different content + await sandbox.arun(`mkdir -p ${targetDir}`, { mode: RunMode.NORMAL }); + await sandbox.arun(`echo "old content" > ${targetDir}/oldfile.txt`, { mode: RunMode.NORMAL }); + + // Act: Upload should succeed and overwrite + const result = await sandbox.getFs().uploadDir(sourceDir, targetDir); + + // Assert: Upload should succeed + expect(result.exitCode).toBe(0); + + // Assert: New file should exist + const newFileResult = await sandbox.arun(`cat ${targetDir}/newfile.txt`, { mode: RunMode.NORMAL }); + expect(newFileResult.output.trim()).toBe('New Content'); + + // Assert: Old file should no longer exist (directory was replaced) + // Use test command instead of ls to avoid throwing on non-existent file + const oldFileCheck = await sandbox.arun(`test -f ${targetDir}/oldfile.txt && echo "exists" || echo "not exists"`, { mode: RunMode.NORMAL }); + expect(oldFileCheck.output.trim()).toBe('not exists'); + }, 180000); + }); +}); diff --git a/rock/ts-sdk/tests/integration/sandbox-lifecycle.test.ts b/rock/ts-sdk/tests/integration/sandbox-lifecycle.test.ts new file mode 100644 index 000000000..5916d03de --- /dev/null +++ b/rock/ts-sdk/tests/integration/sandbox-lifecycle.test.ts @@ -0,0 +1,57 @@ +/** + * Integration test for Sandbox lifecycle + * + * Prerequisites: + * - ROCK_BASE_URL environment variable or default baseUrl + * - Access to the ROCK sandbox service + * - Valid Docker image + */ + +import { Sandbox } from '../../src/sandbox/client.js'; + +const TEST_CONFIG = { + baseUrl: process.env.ROCK_BASE_URL || 'http://11.166.8.116:8080', + image: 'reg.docker.alibaba-inc.com/yanan/python:3.11', + cluster: 'zb', + startupTimeout: 120, // 2 minutes timeout for sandbox startup +}; + +describe('Sandbox Lifecycle Integration', () => { + let sandbox: Sandbox; + + beforeEach(() => { + sandbox = new Sandbox(TEST_CONFIG); + }); + + afterEach(async () => { + // Cleanup: ensure sandbox is stopped even if test fails + if (sandbox) { + try { + await sandbox.close(); + } catch (e) { + // Ignore cleanup errors + } + } + }); + + test('should start sandbox, wait for isAlive=true, then stop sandbox', async () => { + // Step 1: Start sandbox + await sandbox.start(); + + // Step 2: Verify sandbox is alive + const aliveResponse = await sandbox.isAlive(); + expect(aliveResponse.isAlive).toBe(true); + + // Step 3: Stop sandbox + await sandbox.stop(); + + // Step 4: Verify sandbox is stopped (isAlive should be false or throw error) + try { + const afterStopResponse = await sandbox.isAlive(); + expect(afterStopResponse.isAlive).toBe(false); + } catch (e) { + // After stop, isAlive may throw error, which is acceptable + expect(String(e)).toContain('Failed to get is alive'); + } + }, 180000); // 3 minutes timeout for the whole test +}); diff --git a/rock/ts-sdk/tsconfig.json b/rock/ts-sdk/tsconfig.json new file mode 100644 index 000000000..f5b63c6c1 --- /dev/null +++ b/rock/ts-sdk/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowJs": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "__tests__"] +} diff --git a/rock/ts-sdk/tsup.config.ts b/rock/ts-sdk/tsup.config.ts new file mode 100644 index 000000000..f351fd8cf --- /dev/null +++ b/rock/ts-sdk/tsup.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + shims: true, + dts: true, + splitting: false, + sourcemap: true, + clean: true, + target: 'es2022', + outDir: 'dist', + minify: false, + treeshake: true, + external: ['ali-oss'], +});