feat: containerd utils

This commit is contained in:
Acbox
2026-01-15 17:07:09 +08:00
parent afc6dc6cb1
commit 31d8103ed2
9 changed files with 1308 additions and 0 deletions
+358
View File
@@ -0,0 +1,358 @@
# @memoh/container
基于 containerd 的容器化工具包,提供简单易用的容器管理 API。
## 特性
- 🚀 基于 containerd 的高性能容器管理
- 📦 简洁的 API 设计
- 🔧 完整的容器生命周期管理
- 📝 TypeScript 支持
- 🎯 命名空间隔离
## 安装
```bash
pnpm install @memoh/container
```
## 前置要求
系统需要安装 containerd 和 ctr 命令行工具:
```bash
# macOS (使用 Homebrew)
brew install containerd
# Ubuntu/Debian
apt-get install containerd
# 启动 containerd 服务
sudo systemctl start containerd
```
## 快速开始
### 创建容器
使用 `createContainer` 创建一个新容器:
```typescript
import { createContainer } from '@memoh/container';
const container = await createContainer({
name: 'my-nginx',
image: 'docker.io/library/nginx:latest',
env: {
PORT: '8080',
NODE_ENV: 'production',
},
});
console.log('Container created:', container.id);
console.log('Status:', container.status);
```
### 操作容器
使用 `useContainer` 获取容器操作方法:
```typescript
import { useContainer } from '@memoh/container';
const container = useContainer('my-nginx');
// 启动容器
await container.start();
// 获取容器信息
const info = await container.info();
console.log('Container status:', info.status);
// 执行命令
const result = await container.exec(['nginx', '-v']);
console.log('Output:', result.stdout);
// 查看日志
const logs = await container.logs();
console.log(logs);
// 暂停容器
await container.pause();
// 恢复容器
await container.resume();
// 停止容器
await container.stop(10); // 10秒超时
// 删除容器
await container.remove();
```
## API 文档
### createContainer
创建并返回容器信息。
```typescript
function createContainer(
config: ContainerConfig,
options?: ContainerdOptions
): Promise<ContainerInfo>
```
**参数:**
- `config.name` - 容器名称(必需)
- `config.image` - 镜像引用(必需)
- `config.command` - 容器启动命令
- `config.env` - 环境变量
- `config.workingDir` - 工作目录
- `config.namespace` - 命名空间(默认:default)
- `config.labels` - 容器标签
**返回:** `ContainerInfo` 对象
### useContainer
获取容器操作方法。
```typescript
function useContainer(
containerIdOrName: string,
options?: ContainerdOptions
): ContainerOperations
```
**返回的操作方法:**
- `start()` - 启动容器
- `stop(timeout?)` - 停止容器
- `restart(timeout?)` - 重启容器
- `pause()` - 暂停容器
- `resume()` - 恢复容器
- `remove(force?)` - 删除容器
- `exec(command)` - 执行命令
- `info()` - 获取容器信息
- `logs(follow?)` - 获取日志
- `stats()` - 获取统计信息
### listContainers
列出所有容器。
```typescript
function listContainers(options?: ContainerdOptions): Promise<ContainerInfo[]>
```
**示例:**
```typescript
import { listContainers } from '@memoh/container';
const containers = await listContainers();
for (const container of containers) {
console.log(`${container.name}: ${container.status}`);
}
```
### containerExists
检查容器是否存在。
```typescript
function containerExists(
containerIdOrName: string,
options?: ContainerdOptions
): Promise<boolean>
```
### removeAllContainers
删除所有容器。
```typescript
function removeAllContainers(
force?: boolean,
options?: ContainerdOptions
): Promise<void>
```
## 高级用法
### 自定义命名空间
```typescript
import { createContainer, useContainer } from '@memoh/container';
// 在自定义命名空间中创建容器
const container = await createContainer(
{
name: 'my-app',
image: 'docker.io/library/node:18',
},
{
namespace: 'production',
}
);
// 操作同一命名空间的容器
const ops = useContainer('my-app', { namespace: 'production' });
await ops.start();
```
### 自定义 Socket 路径
```typescript
const container = useContainer('my-app', {
socket: '/custom/path/to/containerd.sock',
});
```
### 完整示例:Web 服务部署
```typescript
import { createContainer, useContainer } from '@memoh/container';
async function deployWebService() {
// 创建容器
const container = await createContainer({
name: 'web-service',
image: 'docker.io/library/nginx:alpine',
env: {
NGINX_PORT: '8080',
},
labels: {
app: 'web-service',
version: '1.0.0',
},
});
console.log('Container created:', container.id);
// 启动容器
const ops = useContainer(container.name);
await ops.start();
console.log('Container started');
// 等待服务就绪
await new Promise(resolve => setTimeout(resolve, 2000));
// 检查状态
const info = await ops.info();
console.log('Status:', info.status);
// 执行健康检查
const health = await ops.exec(['curl', '-f', 'http://localhost:8080']);
if (health.exitCode === 0) {
console.log('Service is healthy');
}
return ops;
}
// 使用
const service = await deployWebService();
// 稍后停止服务
await service.stop();
await service.remove();
```
## 类型定义
```typescript
interface ContainerConfig {
name: string;
image: string;
command?: string[];
env?: Record<string, string>;
workingDir?: string;
network?: string;
mounts?: Mount[];
labels?: Record<string, string>;
namespace?: string;
}
interface ContainerInfo {
id: string;
name: string;
image: string;
status: ContainerStatus;
namespace: string;
createdAt: Date;
labels?: Record<string, string>;
}
type ContainerStatus = 'created' | 'running' | 'paused' | 'stopped' | 'unknown';
interface ExecResult {
exitCode: number;
stdout: string;
stderr: string;
}
```
## 注意事项
1. **权限要求**:操作容器通常需要 root 权限或将用户添加到适当的组
2. **containerd 服务**:确保 containerd 服务正在运行
3. **镜像拉取**:首次使用镜像时会自动拉取,可能需要一些时间
4. **命名空间**:不同命名空间的容器相互隔离
5. **清理资源**:使用完容器后记得清理(stop + remove
## 故障排查
### 命令未找到
如果遇到 `ctr command not found` 错误:
```bash
# 检查 containerd 是否安装
which ctr
# 安装 containerd
brew install containerd # macOS
apt-get install containerd # Linux
```
### 权限被拒绝
如果遇到权限错误:
```bash
# 将用户添加到 docker 组(如果存在)
sudo usermod -aG docker $USER
# 或者使用 sudo 运行你的程序
sudo node your-script.js
```
### 容器无法启动
检查容器日志:
```typescript
const container = useContainer('my-container');
const logs = await container.logs();
console.log(logs);
```
## 开发
```bash
# 安装依赖
pnpm install
# 运行测试
pnpm test
# 构建
pnpm build
```
## 许可证
MIT
+101
View File
@@ -0,0 +1,101 @@
/**
* Basic usage examples for @memoh/container
*/
import { createContainer, useContainer, listContainers } from '../src'
async function main() {
console.log('🚀 Container Management Examples\n')
// Example 1: Create and start a container
console.log('📦 Example 1: Create and start a container')
try {
const container = await createContainer({
name: 'example-nginx',
image: 'docker.io/library/nginx:alpine',
env: {
NGINX_HOST: 'localhost',
NGINX_PORT: '80',
},
labels: {
example: 'basic',
version: '1.0',
},
})
console.log('✅ Container created:', container.id)
console.log(' Status:', container.status)
console.log(' Image:', container.image)
console.log('')
// Start the container
const ops = useContainer(container.name)
await ops.start()
console.log('✅ Container started\n')
// Get container info
const info = await ops.info()
console.log('📊 Container info:')
console.log(' Name:', info.name)
console.log(' Status:', info.status)
console.log(' Created:', info.createdAt)
console.log('')
// Stop and remove
await ops.stop(5)
console.log('⏹️ Container stopped')
await ops.remove()
console.log('🗑️ Container removed\n')
} catch (error) {
console.error('❌ Error:', error)
}
// Example 2: List all containers
console.log('📋 Example 2: List all containers')
try {
const containers = await listContainers()
if (containers.length === 0) {
console.log(' No containers found\n')
} else {
for (const container of containers) {
console.log(` - ${container.name}: ${container.status}`)
}
console.log('')
}
} catch (error) {
console.error('❌ Error:', error)
}
// Example 3: Execute commands in container
console.log('🔧 Example 3: Execute commands in container')
try {
const container = await createContainer({
name: 'example-alpine',
image: 'docker.io/library/alpine:latest',
command: ['sh', '-c', 'while true; do sleep 1; done'],
})
const ops = useContainer(container.name)
await ops.start()
console.log('✅ Container started')
// Execute command
const result = await ops.exec(['echo', 'Hello from container!'])
console.log('📤 Command output:', result.stdout)
console.log(' Exit code:', result.exitCode)
console.log('')
// Cleanup
await ops.stop(2)
await ops.remove()
console.log('🧹 Cleaned up\n')
} catch (error) {
console.error('❌ Error:', error)
}
console.log('✨ All examples completed!')
}
// Run examples
main().catch(console.error)
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@memoh/container",
"version": "1.0.0",
"description": "Containerd-based container management utilities",
"exports": {
".": "./src/index.ts"
},
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"execa": "^9.5.2"
},
"devDependencies": {
"@types/node": "^22.10.5",
"typescript": "^5.7.3"
},
"packageManager": "pnpm@10.27.0"
}
+246
View File
@@ -0,0 +1,246 @@
/**
* High-level container management API
*/
import { ContainerdClient } from './containerd'
import type {
ContainerConfig,
ContainerInfo,
ContainerOperations,
ExecResult,
ContainerStats,
ContainerdOptions,
} from './types'
/**
* Create a new container
*
* @param config - Container configuration
* @param options - Containerd client options
* @returns Container information including ID and metadata
*
* @example
* ```typescript
* const container = await createContainer({
* name: 'my-app',
* image: 'docker.io/library/nginx:latest',
* env: { PORT: '8080' },
* });
*
* console.log('Container created:', container.id);
* ```
*/
export async function createContainer(
config: ContainerConfig,
options?: ContainerdOptions
): Promise<ContainerInfo> {
const client = new ContainerdClient(options)
// Ensure image is pulled
await client.pullImage(config.image)
// Create container
const containerInfo = await client.createContainer(config)
return containerInfo
}
/**
* Get container operations for an existing container
*
* @param containerIdOrName - Container ID or name
* @param options - Containerd client options
* @returns Object with methods to operate on the container
*
* @example
* ```typescript
* const container = useContainer('my-app');
*
* // Start the container
* await container.start();
*
* // Get container info
* const info = await container.info();
* console.log('Status:', info.status);
*
* // Execute command
* const result = await container.exec(['echo', 'hello']);
* console.log(result.stdout);
*
* // Stop and remove
* await container.stop();
* await container.remove();
* ```
*/
export function useContainer(
containerIdOrName: string,
options?: ContainerdOptions
): ContainerOperations {
const client = new ContainerdClient(options)
const containerName = containerIdOrName
return {
/**
* Start the container
*/
async start(): Promise<void> {
await client.startContainer(containerName)
},
/**
* Stop the container
* @param timeout - Graceful shutdown timeout in seconds (default: 10)
*/
async stop(timeout: number = 10): Promise<void> {
await client.stopContainer(containerName, timeout)
},
/**
* Restart the container
* @param timeout - Graceful shutdown timeout in seconds (default: 10)
*/
async restart(timeout: number = 10): Promise<void> {
await client.stopContainer(containerName, timeout)
await client.startContainer(containerName)
},
/**
* Pause the container
*/
async pause(): Promise<void> {
await client.pauseContainer(containerName)
},
/**
* Resume a paused container
*/
async resume(): Promise<void> {
await client.resumeContainer(containerName)
},
/**
* Remove the container
* @param force - Force remove even if running (default: false)
*/
async remove(force: boolean = false): Promise<void> {
await client.removeContainer(containerName, force)
},
/**
* Execute a command in the container
* @param command - Command and arguments to execute
* @returns Execution result with exit code and output
*/
async exec(command: string[]): Promise<ExecResult> {
const result = await client.execInContainer(containerName, command)
return {
exitCode: result.exitCode,
stdout: result.stdout,
stderr: result.stderr,
}
},
/**
* Get container information
*/
async info(): Promise<ContainerInfo> {
return await client.getContainerInfo(containerName)
},
/**
* Get container logs
* @param follow - Follow log output (not implemented yet)
*/
async logs(follow: boolean = false): Promise<string> {
if (follow) {
throw new Error('Follow mode not implemented yet')
}
return await client.getContainerLogs(containerName)
},
/**
* Get container stats
* Note: This is a placeholder implementation
* Real implementation would require parsing ctr metrics
*/
async stats(): Promise<ContainerStats> {
// This is a simplified implementation
// Full implementation would require parsing ctr metrics output
return {
cpuUsage: 0,
memoryUsage: 0,
memoryLimit: 0,
networkIO: {
rxBytes: 0,
txBytes: 0,
},
}
},
}
}
/**
* List all containers in the namespace
*
* @param options - Containerd client options
* @returns Array of container information
*
* @example
* ```typescript
* const containers = await listContainers();
* for (const container of containers) {
* console.log(`${container.name}: ${container.status}`);
* }
* ```
*/
export async function listContainers(options?: ContainerdOptions): Promise<ContainerInfo[]> {
const client = new ContainerdClient(options)
return await client.listContainers()
}
/**
* Check if a container exists
*
* @param containerIdOrName - Container ID or name
* @param options - Containerd client options
* @returns True if container exists, false otherwise
*
* @example
* ```typescript
* if (await containerExists('my-app')) {
* console.log('Container exists');
* }
* ```
*/
export async function containerExists(
containerIdOrName: string,
options?: ContainerdOptions
): Promise<boolean> {
const client = new ContainerdClient(options)
return await client.containerExists(containerIdOrName)
}
/**
* Remove all containers in the namespace
*
* @param force - Force remove even if running
* @param options - Containerd client options
*
* @example
* ```typescript
* await removeAllContainers(true);
* console.log('All containers removed');
* ```
*/
export async function removeAllContainers(
force: boolean = false,
options?: ContainerdOptions
): Promise<void> {
const client = new ContainerdClient(options)
const containers = await client.listContainers()
for (const container of containers) {
await client.removeContainer(container.name, force)
}
}
+275
View File
@@ -0,0 +1,275 @@
/**
* Containerd client implementation using ctr CLI
*/
import { execa } from 'execa'
import type { ContainerConfig, ContainerInfo, ContainerStatus, ContainerdOptions } from './types'
export const buildExecCommand = (name: string, command: string[]) => ['task', 'exec', '--exec-id', `exec-${Date.now()}`, name, ...command]
/**
* Containerd client for managing containers
*/
export class ContainerdClient {
private namespace: string
private socket?: string
private timeout: number
constructor(options: ContainerdOptions = {}) {
this.namespace = options.namespace || 'default'
this.socket = options.socket
this.timeout = options.timeout || 30000
}
/**
* Build ctr command with options
*/
private buildCtrCommand(args: string[]): string[] {
const cmd = ['ctr']
if (this.socket) {
cmd.push('--address', this.socket)
}
cmd.push('--namespace', this.namespace)
cmd.push(...args)
return cmd
}
/**
* Execute ctr command
*/
private async exec(args: string[]): Promise<{ stdout: string; stderr: string }> {
const cmd = this.buildCtrCommand(args)
const [program, ...programArgs] = cmd
try {
const result = await execa(program, programArgs, {
timeout: this.timeout,
})
return {
stdout: result.stdout,
stderr: result.stderr,
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
throw new Error(`Containerd command failed: ${message}`)
}
}
/**
* Pull container image
*/
async pullImage(image: string): Promise<void> {
await this.exec(['image', 'pull', image])
}
/**
* Create a new container
*/
async createContainer(config: ContainerConfig): Promise<ContainerInfo> {
const args = ['container', 'create']
// Add image
args.push(config.image)
// Add container name
args.push(config.name)
// Add command if specified
if (config.command && config.command.length > 0) {
args.push(...config.command)
}
await this.exec(args)
// Return container info
return this.getContainerInfo(config.name)
}
/**
* Start a container
*/
async startContainer(name: string): Promise<void> {
await this.exec(['task', 'start', '--detach', name])
}
/**
* Stop a container
*/
async stopContainer(name: string, timeout: number = 10): Promise<void> {
try {
await this.exec(['task', 'kill', '--signal', 'SIGTERM', name])
// Wait for graceful shutdown
await new Promise(resolve => setTimeout(resolve, timeout * 1000))
// Force kill if still running
try {
await this.exec(['task', 'kill', '--signal', 'SIGKILL', name])
} catch {
// Container might have already stopped
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : ''
if (!message.includes('not found')) {
throw error
}
}
}
/**
* Pause a container
*/
async pauseContainer(name: string): Promise<void> {
await this.exec(['task', 'pause', name])
}
/**
* Resume a paused container
*/
async resumeContainer(name: string): Promise<void> {
await this.exec(['task', 'resume', name])
}
/**
* Remove a container
*/
async removeContainer(name: string, force: boolean = false): Promise<void> {
if (force) {
// Try to stop the task first
try {
await this.exec(['task', 'kill', '--signal', 'SIGKILL', name])
await this.exec(['task', 'delete', name])
} catch {
// Task might not exist
}
}
await this.exec(['container', 'delete', name])
}
/**
* Execute command in container
*/
async execInContainer(name: string, command: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const args = buildExecCommand(name, command)
try {
const result = await this.exec(args)
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: 0,
}
} catch (error: unknown) {
const err = error as { stdout?: string; stderr?: string; exitCode?: number; message?: string }
return {
stdout: err.stdout || '',
stderr: err.stderr || err.message || '',
exitCode: err.exitCode || 1,
}
}
}
/**
* Get container information
*/
async getContainerInfo(name: string): Promise<ContainerInfo> {
const result = await this.exec(['container', 'info', name])
try {
const info = JSON.parse(result.stdout)
return {
id: info.ID || name,
name: name,
image: info.Image || '',
status: await this.getContainerStatus(name),
namespace: this.namespace,
createdAt: info.CreatedAt ? new Date(info.CreatedAt) : new Date(),
labels: info.Labels || {},
}
} catch {
// Fallback if JSON parsing fails
return {
id: name,
name: name,
image: '',
status: 'unknown',
namespace: this.namespace,
createdAt: new Date(),
}
}
}
/**
* Get container status
*/
async getContainerStatus(name: string): Promise<ContainerStatus> {
try {
const result = await this.exec(['task', 'list'])
const lines = result.stdout.split('\n')
for (const line of lines) {
if (line.includes(name)) {
if (line.includes('RUNNING')) return 'running'
if (line.includes('PAUSED')) return 'paused'
if (line.includes('STOPPED')) return 'stopped'
}
}
// Container exists but no task
return 'created'
} catch {
return 'unknown'
}
}
/**
* Get container logs
*/
async getContainerLogs(name: string): Promise<string> {
try {
const result = await this.exec(['task', 'logs', name])
return result.stdout
} catch (error: unknown) {
return error instanceof Error ? error.message : ''
}
}
/**
* List all containers
*/
async listContainers(): Promise<ContainerInfo[]> {
const result = await this.exec(['container', 'list', '--quiet'])
const containerNames = result.stdout.split('\n').filter(name => name.trim())
const containers: ContainerInfo[] = []
for (const name of containerNames) {
try {
const info = await this.getContainerInfo(name)
containers.push(info)
} catch {
// Skip containers that can't be accessed
}
}
return containers
}
/**
* Check if container exists
*/
async containerExists(name: string): Promise<boolean> {
try {
await this.getContainerInfo(name)
return true
} catch {
return false
}
}
}
+28
View File
@@ -0,0 +1,28 @@
/**
* @memoh/container - Containerd-based container management utilities
*/
// Export main API
export {
createContainer,
useContainer,
listContainers,
containerExists,
removeAllContainers,
} from './container'
// Export client
export { ContainerdClient, buildExecCommand } from './containerd'
// Export types
export type {
ContainerConfig,
ContainerInfo,
ContainerStatus,
ContainerOperations,
ContainerStats,
ExecResult,
Mount,
ContainerdOptions,
} from './types'
+134
View File
@@ -0,0 +1,134 @@
/**
* Container runtime types and interfaces
*/
/**
* Container configuration options
*/
export interface ContainerConfig {
/** Container name/ID */
name: string;
/** Container image reference */
image: string;
/** Command to run in the container */
command?: string[];
/** Environment variables */
env?: Record<string, string>;
/** Working directory */
workingDir?: string;
/** Network namespace */
network?: string;
/** Mount points */
mounts?: Mount[];
/** Labels for the container */
labels?: Record<string, string>;
/** Container namespace (default: "default") */
namespace?: string;
}
/**
* Mount configuration
*/
export interface Mount {
/** Mount type: bind, volume, tmpfs */
type: 'bind' | 'volume' | 'tmpfs';
/** Source path (host) */
source: string;
/** Target path (container) */
target: string;
/** Read-only mount */
readonly?: boolean;
}
/**
* Container information
*/
export interface ContainerInfo {
/** Container ID */
id: string;
/** Container name */
name: string;
/** Container image */
image: string;
/** Container status */
status: ContainerStatus;
/** Container namespace */
namespace: string;
/** Creation timestamp */
createdAt: Date;
/** Labels */
labels?: Record<string, string>;
}
/**
* Container status
*/
export type ContainerStatus = 'created' | 'running' | 'paused' | 'stopped' | 'unknown';
/**
* Container execution result
*/
export interface ExecResult {
/** Exit code */
exitCode: number;
/** Standard output */
stdout: string;
/** Standard error */
stderr: string;
}
/**
* Container stats
*/
export interface ContainerStats {
/** CPU usage percentage */
cpuUsage: number;
/** Memory usage in bytes */
memoryUsage: number;
/** Memory limit in bytes */
memoryLimit: number;
/** Network I/O */
networkIO?: {
rxBytes: number;
txBytes: number;
};
}
/**
* Container operations interface
*/
export interface ContainerOperations {
/** Start the container */
start(): Promise<void>;
/** Stop the container */
stop(timeout?: number): Promise<void>;
/** Restart the container */
restart(timeout?: number): Promise<void>;
/** Pause the container */
pause(): Promise<void>;
/** Resume the container */
resume(): Promise<void>;
/** Remove the container */
remove(force?: boolean): Promise<void>;
/** Execute a command in the container */
exec(command: string[]): Promise<ExecResult>;
/** Get container info */
info(): Promise<ContainerInfo>;
/** Get container logs */
logs(follow?: boolean): Promise<string>;
/** Get container stats */
stats(): Promise<ContainerStats>;
}
/**
* Containerd client options
*/
export interface ContainerdOptions {
/** Containerd socket path */
socket?: string;
/** Containerd namespace */
namespace?: string;
/** Timeout for operations (ms) */
timeout?: number;
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+125
View File
@@ -204,6 +204,19 @@ importers:
specifier: latest
version: 1.3.6
packages/container:
dependencies:
execa:
specifier: ^9.5.2
version: 9.6.1
devDependencies:
'@types/node':
specifier: ^22.10.5
version: 22.19.5
typescript:
specifier: ^5.7.3
version: 5.9.3
packages/db:
dependencies:
'@memoh/shared':
@@ -1944,6 +1957,9 @@ packages:
'@rushstack/ts-command-line@5.1.5':
resolution: {integrity: sha512-YmrFTFUdHXblYSa+Xc9OO9FsL/XFcckZy0ycQ6q7VSBsVs5P0uD9vcges5Q9vctGlVdu27w+Ct6IuJ458V0cTQ==}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@sevinf/maybe@0.5.0':
resolution: {integrity: sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg==}
@@ -1977,6 +1993,10 @@ packages:
'@sinclair/typebox@0.34.47':
resolution: {integrity: sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==}
'@sindresorhus/merge-streams@4.0.0':
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -3350,6 +3370,10 @@ packages:
'@sinclair/typebox':
optional: true
execa@9.6.1:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0}
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
@@ -3406,6 +3430,10 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -3530,6 +3558,10 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-stream@9.0.1:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
get-tsconfig@4.13.0:
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
@@ -3632,6 +3664,10 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
human-signals@8.0.1:
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
engines: {node: '>=18.18.0'}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
@@ -3748,9 +3784,17 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-plain-obj@4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
is-stream@4.0.1:
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
engines: {node: '>=18'}
is-unicode-supported@1.3.0:
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
engines: {node: '>=12'}
@@ -4304,6 +4348,10 @@ packages:
engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'}
hasBin: true
npm-run-path@6.0.0:
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
engines: {node: '>=18'}
npmlog@6.0.2:
resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@@ -4409,6 +4457,10 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-ms@4.0.0:
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
engines: {node: '>=18'}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -4428,6 +4480,10 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-key@4.0.0:
resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
engines: {node: '>=12'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -4556,6 +4612,10 @@ packages:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
pretty-ms@9.3.0:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'}
promise-inflight@1.0.1:
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
peerDependencies:
@@ -4901,6 +4961,10 @@ packages:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
strip-final-newline@4.0.0:
resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==}
engines: {node: '>=18'}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
@@ -5064,6 +5128,10 @@ packages:
resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==}
engines: {node: '>=14.0'}
unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
unique-filename@1.1.1:
resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==}
@@ -5493,6 +5561,10 @@ packages:
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
engines: {node: '>=18'}
yoctocolors@2.1.2:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'}
zod-to-json-schema@3.25.1:
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
peerDependencies:
@@ -6902,6 +6974,8 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@sec-ant/readable-stream@0.4.1': {}
'@sevinf/maybe@0.5.0': {}
'@shikijs/core@2.5.0':
@@ -6948,6 +7022,8 @@ snapshots:
'@sinclair/typebox@0.34.47': {}
'@sindresorhus/merge-streams@4.0.0': {}
'@standard-schema/spec@1.1.0': {}
'@supabase/auth-js@2.90.1':
@@ -8423,6 +8499,21 @@ snapshots:
optionalDependencies:
'@sinclair/typebox': 0.34.47
execa@9.6.1:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
cross-spawn: 7.0.6
figures: 6.1.0
get-stream: 9.0.1
human-signals: 8.0.1
is-plain-obj: 4.1.0
is-stream: 4.0.1
npm-run-path: 6.0.0
pretty-ms: 9.3.0
signal-exit: 4.1.0
strip-final-newline: 4.0.0
yoctocolors: 2.1.2
expand-template@2.0.3: {}
expect-type@1.3.0: {}
@@ -8495,6 +8586,10 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
figures@6.1.0:
dependencies:
is-unicode-supported: 2.1.0
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -8644,6 +8739,11 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-stream@9.0.1:
dependencies:
'@sec-ant/readable-stream': 0.4.1
is-stream: 4.0.1
get-tsconfig@4.13.0:
dependencies:
resolve-pkg-maps: 1.0.0
@@ -8795,6 +8895,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
human-signals@8.0.1: {}
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
@@ -8899,8 +9001,12 @@ snapshots:
is-number@7.0.0: {}
is-plain-obj@4.1.0: {}
is-promise@4.0.0: {}
is-stream@4.0.1: {}
is-unicode-supported@1.3.0: {}
is-unicode-supported@2.1.0: {}
@@ -9445,6 +9551,11 @@ snapshots:
shell-quote: 1.8.3
which: 5.0.0
npm-run-path@6.0.0:
dependencies:
path-key: 4.0.0
unicorn-magic: 0.3.0
npmlog@6.0.2:
dependencies:
are-we-there-yet: 3.0.1
@@ -9585,6 +9696,8 @@ snapshots:
dependencies:
callsites: 3.1.0
parse-ms@4.0.0: {}
parseurl@1.3.3: {}
path-browserify@1.0.1: {}
@@ -9596,6 +9709,8 @@ snapshots:
path-key@3.1.1: {}
path-key@4.0.0: {}
path-parse@1.0.7: {}
path-scurry@1.11.1:
@@ -9721,6 +9836,10 @@ snapshots:
ansi-styles: 5.2.0
react-is: 18.3.1
pretty-ms@9.3.0:
dependencies:
parse-ms: 4.0.0
promise-inflight@1.0.1:
optional: true
@@ -10133,6 +10252,8 @@ snapshots:
dependencies:
ansi-regex: 6.2.2
strip-final-newline@4.0.0: {}
strip-json-comments@2.0.1: {}
strip-json-comments@3.1.1: {}
@@ -10296,6 +10417,8 @@ snapshots:
dependencies:
'@fastify/busboy': 2.1.1
unicorn-magic@0.3.0: {}
unique-filename@1.1.1:
dependencies:
unique-slug: 2.0.2
@@ -10728,6 +10851,8 @@ snapshots:
yoctocolors-cjs@2.1.3: {}
yoctocolors@2.1.2: {}
zod-to-json-schema@3.25.1(zod@3.25.76):
dependencies:
zod: 3.25.76