mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: containerd utils
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Generated
+125
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user