diff --git a/.dockerignore b/.dockerignore index 29a756d..0506b18 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,79 @@ -.git/ -.idea/ -.vscode/ -node_modules/ -*.log -.env -.env.local -.env.*.local +# Git +.git +.gitignore -# Exclude all target/ content, then selectively re-include release binaries +# IDE +.idea +.vscode +*.swp +*.swo +*~ + +# Rust build artifacts target/ -!target/release/ -!target/x86_64-unknown-linux-gnu/release/ +**/target/ + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +bun.lockb + +# Build output +dist/ +build/ + +# Environment and secrets +.env +.env.* +!.env.example + +# Docker +docker/ +docker-compose*.yml +.dockerignore +Dockerfile* +*.Dockerfile + +# Documentation +*.md +LICENSE +doc/ + +# Test and CI +tests/ +__tests__/ +*.test.* +*.spec.* +.github/ +.gitlab/ +.circleci/ + +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# Logs +logs/ +*.log + +# Temporary files +tmp/ +temp/ +.tmp/ + +# Certificates (use secrets in production) +cert/ + +# Data directories +data/ + +# Agent configs +.agent/ +.agents/ +.claude/ +CLAUDE.md +AGENTS.md \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index bbba4ed..0000000 --- a/.env.example +++ /dev/null @@ -1,136 +0,0 @@ -# ============================================================================= -# Required - 程序启动必须配置 -# ============================================================================= - -# 数据库连接 -APP_DATABASE_URL=postgresql://user:password@localhost:5432/dbname -APP_DATABASE_SCHEMA_SEARCH_PATH=public - -# Redis(支持多节点,逗号分隔) -APP_REDIS_URL=redis://localhost:6379 -# APP_REDIS_URLS=redis://localhost:6379,redis://localhost:6378 - -# AI 服务 -APP_AI_BASIC_URL=https://api.openai.com/v1 -APP_AI_API_KEY=sk-xxxxx - -# Embedding + 向量检索 -APP_EMBED_MODEL_BASE_URL=https://api.openai.com/v1 -APP_EMBED_MODEL_API_KEY=sk-xxxxx -APP_EMBED_MODEL_NAME=text-embedding-3-small -APP_EMBED_MODEL_DIMENSIONS=1536 -APP_QDRANT_URL=http://localhost:6333 -# APP_QDRANT_API_KEY= - -# SMTP 邮件 -APP_SMTP_HOST=smtp.example.com -APP_SMTP_PORT=587 -APP_SMTP_USERNAME=noreply@example.com -APP_SMTP_PASSWORD=xxxxx -APP_SMTP_FROM=noreply@example.com -APP_SMTP_TLS=true -APP_SMTP_TIMEOUT=30 - -# 文件存储 -APP_AVATAR_PATH=/data/avatars -# Git 仓库存储根目录 -APP_REPOS_ROOT=/data/repos - -# ============================================================================= -# Domain / URL(可选,有默认值) -# ============================================================================= - -APP_DOMAIN_URL=http://127.0.0.1 -# APP_STATIC_DOMAIN= -# APP_MEDIA_DOMAIN= -# APP_GIT_HTTP_DOMAIN= - -# ============================================================================= -# Database Pool(可选,有默认值) -# ============================================================================= - -# APP_DATABASE_MAX_CONNECTIONS=10 -# APP_DATABASE_MIN_CONNECTIONS=2 -# APP_DATABASE_IDLE_TIMEOUT=60000 (milliseconds, default: 60s) -# APP_DATABASE_MAX_LIFETIME=300000 (milliseconds, default: 300s) -# APP_DATABASE_CONNECTION_TIMEOUT=5000 (milliseconds, default: 5s) -# APP_DATABASE_REPLICAS= -# APP_DATABASE_HEALTH_CHECK_INTERVAL=30 -# APP_DATABASE_RETRY_ATTEMPTS=3 -# APP_DATABASE_RETRY_DELAY=5 - -# ============================================================================= -# Redis Pool(可选,有默认值) -# ============================================================================= - -# APP_REDIS_POOL_SIZE=10 -# APP_REDIS_CONNECT_TIMEOUT=5 -# APP_REDIS_ACQUIRE_TIMEOUT=5 - -# ============================================================================= -# SSH(可选,有默认值) -# ============================================================================= - -# APP_SSH_DOMAIN= -# APP_SSH_PORT=22 -# APP_SSH_SERVER_PRIVATE_KEY= -# APP_SSH_SERVER_PUBLIC_KEY= - -# ============================================================================= -# Logging(可选,有默认值) -# ============================================================================= - -# APP_LOG_LEVEL=info -# APP_LOG_FORMAT=json -# APP_LOG_FILE_ENABLED=false -# APP_LOG_FILE_PATH=./logs -# APP_LOG_FILE_ROTATION=daily -# APP_LOG_FILE_MAX_FILES=7 -# APP_LOG_FILE_MAX_SIZE=104857600 - -# OpenTelemetry(可选,默认关闭) -# APP_OTEL_ENABLED=false -# APP_OTEL_ENDPOINT=http://localhost:5080/api/default/v1/traces -# APP_OTEL_SERVICE_NAME= -# APP_OTEL_SERVICE_VERSION= -# APP_OTEL_AUTHORIZATION= -# APP_OTEL_ORGANIZATION= - -# ============================================================================= -# NATS / Hook Pool(可选,有默认值) -# ============================================================================= - -# HOOK_POOL_MAX_CONCURRENT=(CPU 核数) -# HOOK_POOL_CPU_THRESHOLD=80.0 -# HOOK_POOL_REDIS_LIST_PREFIX={hook} -# HOOK_POOL_REDIS_LOG_CHANNEL=hook:logs -# HOOK_POOL_REDIS_BLOCK_TIMEOUT=5 -# HOOK_POOL_REDIS_MAX_RETRIES=3 -# HOOK_POOL_WORKER_ID=(随机 UUID) - -# ============================================================================= -# Frontend (Vite) — 前端运行环境变量 -# ============================================================================= - -# API 基础 URL(为空时使用 Vite dev 代理 /api -> localhost:8080) -# VITE_API_BASE_URL=http://localhost:8080 - -# 前端 WebSocket 连接地址(开发模式通过 Vite 代理) -VITE_WS_URL=ws://localhost:5080 - -# API URL(前端 API 调用,通过 Vite 代理时可为空) -VITE_API_URL= - -# WebSocket 连接模式: "raw-ws" | "socketio" -VITE_WS_MODE=raw-ws - -# ============================================================================= -# Frontend: Grafana Faro (RUM) — 前端性能监控(可选) -# ============================================================================= - -# VITE_FARO_ENABLED=false -# VITE_FARO_URL=https://faro.example.com/collect -# VITE_FARO_API_KEY= -# VITE_FARO_APP_NAME=GitDataAIWeb -# VITE_FARO_APP_ENV=production -# VITE_FARO_APP_VERSION=0.0.1 diff --git a/.gitignore b/.gitignore index b506cd6..7c3f2ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,62 @@ +# Rust build artifacts /target -node_modules -.claude -.zed -.vscode -.idea +**/target/ + +# Rust IDE and tooling +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Environment files .env .env.local -dist -deploy/secrets.yaml -.codex -.qwen -.opencode -.omc -AGENT.md -ARCHITECTURE.md -.agents -.agents.md -.next +.env.*.local +.env.production + +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# Node.js node_modules/ -coverage/ -.pnpm-store/ -pnpm-lock.yaml -package-lock.json -yarn.lock -.gemini -.omg -/.sqry -deploy/.server.yaml \ No newline at end of file +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build output +dist/ +build/ + +# Logs +logs/ +*.log + +# Data and certificates +data/ +cert/ + +# Docker +docker-compose.override.yml +.docker/ + +# Agent configs +.claude/ +.codex/ +.agent/ +.agents/ +CLAUDE.md +AGENTS.md +migrate.sh +# Temporary files +tmp/ +temp/ +.tmp/ + +# Backup files +*.bak +*.backup +*~ diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index bd98b4f..0000000 --- a/.mcp.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mcpServers": { - "shadcn": { - "command": "npx", - "args": [ - "shadcn@latest", - "mcp" - ] - } - } -} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 0b4a1db..0000000 --- a/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -coverage/ -.pnpm-store/ -pnpm-lock.yaml -package-lock.json -pnpm-lock.yaml -yarn.lock diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 9000bfa..0000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "endOfLine": "lf", - "semi": false, - "singleQuote": false, - "tabWidth": 2, - "trailingComma": "es5", - "printWidth": 80, - "plugins": ["prettier-plugin-tailwindcss"], - "tailwindStylesheet": "src/index.css", - "tailwindFunctions": ["cn", "cva"] -} diff --git a/Cargo.lock b/Cargo.lock index 07f3a4f..5f9b06e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,38 +18,13 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-macros", - "actix-rt", - "actix_derive", - "bitflags 2.11.0", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - [[package]] name = "actix-codec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bytes", "futures-core", "futures-sink", @@ -60,60 +35,22 @@ dependencies = [ "tracing", ] -[[package]] -name = "actix-cors" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more 2.1.1", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-files" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8c4f30e3272d7c345f88ae0aac3848507ef5ba871f9cc2a41c8085a0f0523b" -dependencies = [ - "actix-http", - "actix-service", - "actix-utils", - "actix-web", - "bitflags 2.11.0", - "bytes", - "derive_more 2.1.1", - "futures-core", - "http-range", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "v_htmlescape", -] - [[package]] name = "actix-http" -version = "3.12.0" +version = "3.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +checksum = "93acb4a42f64936f9b8cae4a433b237599dd6eb6ed06124eb67132ef8cc90662" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", "base64 0.22.1", - "bitflags 2.11.0", - "brotli 8.0.2", + "bitflags", + "brotli", "bytes", "bytestring", - "derive_more 2.1.1", + "derive_more", "encoding_rs", "flate2", "foldhash 0.1.5", @@ -128,8 +65,8 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand 0.9.2", - "sha1 0.10.6", + "rand 0.10.1", + "sha1 0.11.0", "smallvec", "tokio", "tokio-util", @@ -147,44 +84,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "actix-multipart" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" -dependencies = [ - "actix-multipart-derive", - "actix-utils", - "actix-web", - "derive_more 0.99.20", - "futures-core", - "futures-util", - "httparse", - "local-waker", - "log", - "memchr", - "mime", - "rand 0.8.5", - "serde", - "serde_json", - "serde_plain", - "tempfile", - "tokio", -] - -[[package]] -name = "actix-multipart-derive" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" -dependencies = [ - "darling 0.20.11", - "parse-size", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "actix-router" version = "0.5.4" @@ -206,7 +105,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" dependencies = [ - "actix-macros", "futures-core", "tokio", ] @@ -238,25 +136,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "actix-tls" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "http 0.2.12", - "http 1.4.0", - "impl-more", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "actix-utils" version = "3.0.1" @@ -286,7 +165,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more 2.1.1", + "derive_more", "encoding_rs", "foldhash 0.1.5", "futures-core", @@ -338,26 +217,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "actix_derive" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -380,7 +239,7 @@ version = "0.6.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" dependencies = [ - "crypto-common 0.2.1", + "crypto-common 0.2.2", "inout 0.2.2", ] @@ -401,7 +260,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" dependencies = [ - "cipher 0.5.1", + "cipher 0.5.2", "cpubits", "cpufeatures 0.3.0", "zeroize", @@ -429,54 +288,13 @@ checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" dependencies = [ "aead 0.6.0-rc.10", "aes 0.9.0", - "cipher 0.5.1", - "ctr 0.10.0", + "cipher 0.5.2", + "ctr 0.10.1", "ghash 0.6.0", "subtle", "zeroize", ] -[[package]] -name = "aes-keywrap" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b6f24a1f796bc46415a1d0d18dc0a8203ccba088acf5def3291c4f61225522" -dependencies = [ - "aes 0.9.0", - "byteorder", -] - -[[package]] -name = "agent" -version = "0.2.9" -dependencies = [ - "async-trait", - "chrono", - "config", - "db", - "futures", - "metrics 0.24.5", - "models", - "once_cell", - "qdrant-client", - "queue", - "redis", - "regex", - "reqwest 0.13.2", - "rig-core", - "rust_decimal", - "sea-orm", - "serde", - "serde_json", - "thiserror 2.0.18", - "tiktoken-rs", - "tokio", - "tokio-stream", - "tracing", - "utoipa", - "uuid", -] - [[package]] name = "ahash" version = "0.7.8" @@ -511,6 +329,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "ai" +version = "1.0.0" +dependencies = [ + "async-trait", + "cache", + "config", + "db", + "futures", + "qdrant-client", + "redis", + "reqwest 0.13.3", + "rig-core", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "uuid", +] + [[package]] name = "aliasable" version = "0.1.3" @@ -556,19 +397,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "ammonia" -version = "4.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" -dependencies = [ - "cssparser", - "html5ever", - "maplit", - "tendril", - "url", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -636,71 +464,106 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "api" -version = "0.2.9" +version = "1.0.0" dependencies = [ - "actix", - "actix-cors", - "actix-multipart", "actix-web", "actix-ws", - "agent", - "anyhow", + "async-stream", "base64 0.22.1", - "brotli 7.0.0", + "channel", "chrono", + "comrak", "config", "db", - "email", - "flate2", - "futures", - "futures-util", "git", - "mime_guess2", - "models", - "queue", + "model", "redis", - "reqwest 0.13.2", - "room", - "rust_decimal", - "sea-orm", "serde", "serde_json", "service", "session", - "sha2 0.11.0", - "tokio", + "socketio", + "storage", "tokio-stream", - "tracing", - "transport", "utoipa", "uuid", ] [[package]] -name = "app" -version = "0.2.9" +name = "app-email" +version = "1.0.0" +dependencies = [ + "anyhow", + "config", + "email", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "app-gitdata" +version = "1.0.0" dependencies = [ - "actix-cors", "actix-web", + "actix-ws", "anyhow", "api", - "chrono", - "clap", + "cache", + "channel", "config", "db", - "futures", - "hkdf 0.13.0", - "hmac 0.13.0", - "migrate", - "observability", - "room", - "sea-orm", + "deadpool-redis", + "email", + "git", + "model", + "redis", "serde_json", "service", "session", - "sha2 0.11.0", + "socketio", + "sqlx 0.9.0", + "storage", + "tokio", + "tonic 0.14.6", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "app-gitpod" +version = "1.0.0" +dependencies = [ + "anyhow", + "cache", + "config", + "db", + "deadpool-redis", + "git", + "redis", "tokio", "tracing", + "tracing-subscriber", +] + +[[package]] +name = "app-gitsync" +version = "1.0.0" +dependencies = [ + "actix-web", + "anyhow", + "cache", + "config", + "db", + "deadpool-redis", + "git", + "redis", + "serde_json", + "sqlx 0.9.0", + "tokio", + "tracing", + "tracing-subscriber", "uuid", ] @@ -727,15 +590,12 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] [[package]] name = "arc-swap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" dependencies = [ "rustversion", ] @@ -764,16 +624,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", - "blake2", + "blake2 0.10.6", "cpufeatures 0.2.17", "password-hash 0.5.0", ] [[package]] -name = "arrayref" -version = "0.3.9" +name = "argon2" +version = "0.6.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +checksum = "7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5" +dependencies = [ + "base64ct", + "blake2 0.11.0-rc.6", + "cpufeatures 0.3.0", + "password-hash 0.6.1", +] [[package]] name = "arrayvec" @@ -783,9 +649,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8" +checksum = "3bd47f2a6ddc39244bd722a27ee5da66c03369d087b9e024eafdb03e98b98ea7" dependencies = [ "arrow-arith", "arrow-array", @@ -801,9 +667,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b" +checksum = "7c7bbd679c5418b8639b92be01f361d60013c4906574b578b77b63c78356594c" dependencies = [ "arrow-array", "arrow-buffer", @@ -815,9 +681,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +checksum = "c8a4ab47b3f3eac60f7fd31b81e9028fda018607bcc63451aca4f2b755269862" dependencies = [ "ahash 0.8.12", "arrow-buffer", @@ -833,9 +699,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +checksum = "0d18b89b4c4f4811d0858175e79541fe98e33e18db3b011708bc287b1240593f" dependencies = [ "bytes", "half", @@ -845,9 +711,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +checksum = "722b5c41dd1d14d0a879a1bce92c6fe33f546101bb2acce57a209825edd075b3" dependencies = [ "arrow-array", "arrow-buffer", @@ -866,9 +732,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +checksum = "c1683705c63dcf0d18972759eda48489028cbbff67af7d6bef2c6b7b74ab778a" dependencies = [ "arrow-buffer", "arrow-schema", @@ -879,9 +745,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +checksum = "082342947d4e5a2bcccf029a0a0397e21cb3bb8421edd9571d34fb5dd2670256" dependencies = [ "arrow-array", "arrow-buffer", @@ -892,9 +758,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0" +checksum = "e3a931b520a2a5e22033e01a6f2486b4cdc26f9106b759abeebc320f125e94d7" dependencies = [ "arrow-array", "arrow-buffer", @@ -905,15 +771,15 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" +checksum = "e4cf0d4a6609679e03002167a61074a21d7b1ad9ea65e462b2c0a97f8a3b2bc6" [[package]] name = "arrow-select" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +checksum = "0b320d86a9806923663bb0fd9baa65ecaba81cb0cd77ff8c1768b9716b4ef891" dependencies = [ "ahash 0.8.12", "arrow-array", @@ -925,9 +791,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "57.3.0" +version = "57.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8" +checksum = "b493e99162e5764077e7823e50ba284858d365922631c7aaefe9487b1abd02c2" dependencies = [ "arrow-array", "arrow-buffer", @@ -955,57 +821,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "asn1-rs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom 7.1.3", - "num-traits", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-lock" version = "3.4.2" @@ -1036,7 +851,7 @@ dependencies = [ "ring", "rustls-native-certs", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.13", "serde", "serde_json", "serde_nanos", @@ -1044,7 +859,7 @@ dependencies = [ "thiserror 2.0.18", "time", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-stream", "tokio-util", "tokio-websockets", @@ -1102,10 +917,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "autocfg" -version = "1.5.0" +name = "auto_enums" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "65398a2893f41bce5c9259f6e1a4f03fbae40637c1bdc755b4f387f48c613b03" +dependencies = [ + "derive_utils", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "av-scenechange" @@ -1141,59 +968,485 @@ dependencies = [ "v_frame", ] -[[package]] -name = "avatar" -version = "0.2.9" -dependencies = [ - "anyhow", - "config", - "image", - "serde", - "tracing", -] - [[package]] name = "avif-serialize" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" dependencies = [ "arrayvec", ] [[package]] -name = "awc" -version = "3.8.2" +name = "aws-config" +version = "1.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7dc0207013c5059ddce268fe12045bd12b2e919318ee660c891bfe297a54f1f" +checksum = "517aa062d8bd9015ee23d6daa5e1c1372328412fdae4e6c4c1be9b69c6ad37a2" dependencies = [ - "actix-codec", - "actix-http", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "base64 0.22.1", + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", "bytes", - "cfg-if", - "cookie", - "derive_more 2.1.1", - "futures-core", - "futures-util", - "h2 0.3.27", + "fastrand", + "hex", + "http 1.4.0", + "sha1 0.10.6", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", "http 0.2.12", - "itoa", - "log", - "mime", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", "percent-encoding", "pin-project-lite", - "rand 0.9.2", - "serde", - "serde_json", - "serde_urlencoded", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.133.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237aba2985e3c0a83e199cc7aa9a64a16c599875bc98170f00932f6199f19922" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "http-body 1.0.1", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.11.0", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4055e6099b2ec264abdc0d9bbfffce306c1601809275c861594779a0b04b45" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02f009ba0284c5d696425fd7b4dcc5b189f5726f4041b7a5794daecb3a68d598" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.104.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa6622798e19e6a76b690562085dd4771c736cd48343464a53ab4ae2f2c9f84" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7083fb918b38474ac65ffbf8a69fc8792d36879f4ac5f1667b43aec61efe9a5" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "p256 0.13.2", + "percent-encoding", + "sha2 0.11.0", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +dependencies = [ + "futures-util", + "pin-project-lite", "tokio", ] +[[package]] +name = "aws-smithy-checksums" +version = "0.64.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8e65f4f81fcccdeb6c3eca2af17ac21d421a1786a26a394aecf421d616d3a" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "md-5 0.11.0", + "pin-project-lite", + "sha1 0.11.0", + "sha2 0.11.0", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.14", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.9.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.9", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower 0.5.3", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517089205f18ab4adc5a3e02888cb139bbbbb2e168eac9f396216925d1fbeaf5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc117c179ecf39a62a0a3f49f600e9ac26a7ad7dd172177999f83933af776c32" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056b66dbce2f81cc0c1e2b05bb402eb58f8a3530479d650efadd5bbae9a4050b" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.7.9" @@ -1201,14 +1454,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "bytes", "futures-util", "http 1.4.0", "http-body 1.0.1", "http-body-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -1221,6 +1474,31 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core 0.5.6", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-core" version = "0.4.5" @@ -1241,6 +1519,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "backon" version = "1.6.0" @@ -1248,23 +1544,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", - "gloo-timers", - "tokio", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", ] [[package]] @@ -1285,18 +1564,22 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -1305,13 +1588,13 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bcrypt-pbkdf" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" dependencies = [ "blowfish", - "pbkdf2 0.12.2", - "sha2 0.10.9", + "pbkdf2", + "sha2 0.11.0", ] [[package]] @@ -1329,10 +1612,13 @@ dependencies = [ ] [[package]] -name = "binstring" -version = "0.1.7" +name = "bincode" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] [[package]] name = "bit-set" @@ -1363,26 +1649,20 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] [[package]] name = "bitstream-io" -version = "4.9.0" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" dependencies = [ - "core2", + "no_std_io2", ] [[package]] @@ -1407,14 +1687,12 @@ dependencies = [ ] [[package]] -name = "blake2b_simd" -version = "1.0.4" +name = "blake2" +version = "0.11.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690" dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", + "digest 0.11.3", ] [[package]] @@ -1456,12 +1734,37 @@ dependencies = [ [[package]] name = "blowfish" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" dependencies = [ "byteorder", - "cipher 0.4.4", + "cipher 0.5.2", +] + +[[package]] +name = "bon" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", ] [[package]] @@ -1488,28 +1791,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "brotli" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor 2.5.1", -] - -[[package]] -name = "brotli" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor 4.0.3", -] - [[package]] name = "brotli" version = "8.0.2" @@ -1518,27 +1799,7 @@ checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", - "brotli-decompressor 5.0.0", -] - -[[package]] -name = "brotli-decompressor" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", + "brotli-decompressor", ] [[package]] @@ -1564,15 +1825,15 @@ dependencies = [ [[package]] name = "built" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytecheck" @@ -1624,36 +1885,40 @@ dependencies = [ ] [[package]] -name = "bytestring" -version = "1.5.0" +name = "bytes-utils" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + +[[package]] +name = "bytestring" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" dependencies = [ "bytes", ] [[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +name = "cache" +version = "1.0.0" dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "calamine" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138646b9af2c5d7f1804ea4bf93afc597737d2bd4f7341d67c48b03316976eb1" -dependencies = [ - "byteorder", - "codepage", - "encoding_rs", - "log", - "quick-xml 0.31.0", + "config", + "moka", + "redis", "serde", - "zip 2.4.2", + "serde_json", + "tokio", ] [[package]] @@ -1666,32 +1931,41 @@ dependencies = [ "base64 0.22.1", "image", "imageproc", - "rand 0.9.2", + "rand 0.9.4", +] + +[[package]] +name = "caseless" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", ] [[package]] name = "cbc" -version = "0.1.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ - "cipher 0.4.4", -] - -[[package]] -name = "cbc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" -dependencies = [ - "cipher 0.5.1", + "cipher 0.5.2", ] [[package]] name = "cc" -version = "1.2.58" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "jobserver", @@ -1699,34 +1973,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cf-rustracing" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6565523d8145e63e0cf1b397a5f1bd4e90d5652a7dffb2de8cec460ff23ef6b1" -dependencies = [ - "backtrace", - "rand 0.10.1", - "tokio", - "trackable", -] - -[[package]] -name = "cf-rustracing-jaeger" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c0e4d8cce27f6a6eaff58d2b66f063a18b8ed0d6ef0947ae7a263afa3b7c08" -dependencies = [ - "cf-rustracing", - "hostname", - "local-ip-address", - "percent-encoding", - "rand 0.10.1", - "thrift_codec", - "tokio", - "trackable", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -1739,17 +1985,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.0" @@ -1757,23 +1992,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cipher 0.5.1", + "cipher 0.5.2", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", "zeroize", ] [[package]] name = "chacha20poly1305" -version = "0.10.1" +version = "0.11.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +checksum = "1c9ed179664f12fd6f155f6dd632edf5f3806d48c228c67ff78366f2a0eb6b5e" dependencies = [ - "aead 0.5.2", - "chacha20 0.9.1", - "cipher 0.4.4", - "poly1305 0.8.0", - "zeroize", + "aead 0.6.0-rc.10", + "chacha20", + "cipher 0.5.2", + "poly1305", +] + +[[package]] +name = "channel" +version = "1.0.0" +dependencies = [ + "base64 0.22.1", + "cache", + "chrono", + "dashmap", + "db", + "futures", + "hmac 0.13.0", + "model", + "redis", + "serde", + "serde_json", + "sha2 0.11.0", + "socketio", + "sqlx 0.9.0", + "storage", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "uuid", ] [[package]] @@ -1798,26 +2058,25 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", "inout 0.1.4", - "zeroize", ] [[package]] name = "cipher" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "block-buffer 0.12.0", - "crypto-common 0.2.1", + "crypto-common 0.2.2", "inout 0.2.2", "zeroize", ] [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -1833,13 +2092,14 @@ dependencies = [ "anstyle", "clap_lex", "strsim 0.11.1", + "terminal_size", ] [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1853,6 +2113,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "cmake" version = "0.1.58" @@ -1864,29 +2133,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" - -[[package]] -name = "coarsetime" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" -dependencies = [ - "libc", - "wasix", - "wasm-bindgen", -] - -[[package]] -name = "codepage" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" -dependencies = [ - "encoding_rs", -] +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" [[package]] name = "color_quant" @@ -1914,6 +2163,39 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "comrak" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f690706b5db081dccea6206d7f6d594bb9895599abea9d1a0539f13888781ae8" +dependencies = [ + "bon", + "caseless", + "clap", + "entities", + "memchr", + "shell-words", + "slug", + "syntect", + "typed-arena", + "unicode_categories", + "xdg", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1925,7 +2207,7 @@ dependencies = [ [[package]] name = "config" -version = "0.2.9" +version = "1.0.0" dependencies = [ "anyhow", "dotenvy", @@ -1967,18 +2249,6 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "convert_case" version = "0.10.0" @@ -1999,13 +2269,23 @@ dependencies = [ "hkdf 0.12.4", "hmac 0.12.1", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "sha2 0.10.9", "subtle", "time", "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -2022,20 +2302,11 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpubits" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" [[package]] name = "cpufeatures" @@ -2057,18 +2328,28 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin 0.10.0", +] [[package]] name = "crc16" @@ -2148,16 +2429,16 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.7.0-rc.28" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96dacf199529fb801ae62a9aafdc01b189e9504c0d1ee1512a4c16bcd8666a93" +checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3" dependencies = [ "cpubits", "ctutils", "getrandom 0.4.2", "hybrid-array", "num-traits", - "rand_core 0.10.0", + "rand_core 0.10.1", "serdect", "subtle", "zeroize", @@ -2176,76 +2457,26 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "getrandom 0.4.2", "hybrid-array", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] name = "crypto-primes" -version = "0.7.0-pre.9" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6081ce8b60c0e533e2bba42771b94eb6149052115f4179744d5779883dc98583" +checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a" dependencies = [ - "crypto-bigint 0.7.0-rc.28", + "crypto-bigint 0.7.3", "libm", - "rand_core 0.10.0", + "rand_core 0.10.1", ] -[[package]] -name = "cssparser" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "csv" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde_core", -] - -[[package]] -name = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "ct-codecs" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" - [[package]] name = "ctr" version = "0.9.2" @@ -2257,18 +2488,18 @@ dependencies = [ [[package]] name = "ctr" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17469f8eb9bdbfad10f71f4cfddfd38b01143520c0e717d8796ccb4d44d44e42" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ - "cipher 0.5.1", + "cipher 0.5.2", ] [[package]] name = "ctutils" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ "cmov", "subtle", @@ -2298,7 +2529,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest 0.11.2", + "digest 0.11.3", "fiat-crypto 0.3.0", "rustc_version", "subtle", @@ -2316,42 +2547,14 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "daemonize" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" -dependencies = [ - "libc", -] - -[[package]] -name = "daggy" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70def8d72740e44d9f676d8dab2c933a236663d86dd24319b57a2bed4d694774" -dependencies = [ - "petgraph", -] - [[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core", + "darling_macro", ] [[package]] @@ -2368,75 +2571,46 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] [[package]] name = "dashmap" -version = "7.0.0-rc2" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a1e35a65fe0538a60167f0ada6e195ad5d477f6ddae273943596d4a1a5730b" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", - "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.14.5", "lock_api", + "once_cell", "parking_lot_core", ] [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "db" -version = "0.2.9" +version = "1.0.0" dependencies = [ "anyhow", - "async-trait", - "chrono", "config", - "deadpool-redis", - "redis", "sea-orm", - "serde_json", - "tokio", - "uuid", + "sqlparser", + "sqlx 0.9.0", ] [[package]] @@ -2469,12 +2643,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - [[package]] name = "delegate" version = "0.13.5" @@ -2549,20 +2717,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom 7.1.3", - "num-bigint", - "num-traits", - "rusticata-macros", -] - [[package]] name = "deranged" version = "0.5.8" @@ -2573,28 +2727,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "derive_builder" version = "0.20.2" @@ -2610,7 +2742,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -2626,19 +2758,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case 0.4.0", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -2654,7 +2773,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -2663,14 +2782,31 @@ dependencies = [ ] [[package]] -name = "des" -version = "0.9.0-rc.3" +name = "derive_utils" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3214053e68a813b9c06ef61075c844f3a1cdeb307d8998ea8555c063caa52fa9" +checksum = "362f47930db19fe7735f527e6595e4900316b893ebf6d48ad3d31be928d57dd6" dependencies = [ - "cipher 0.5.1", + "proc-macro2", + "quote", + "syn 2.0.117", ] +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "digest" version = "0.10.7" @@ -2685,47 +2821,16 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", - "crypto-common 0.2.1", + "crypto-common 0.2.2", "ctutils", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.11.0", - "objc2", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -2744,19 +2849,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "dtoa" -version = "1.0.11" +name = "duct" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" +checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684" +dependencies = [ + "libc", + "os_pipe", + "shared_child", + "shared_thread", +] [[package]] -name = "dtoa-short" -version = "0.3.5" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dyn-clone" @@ -2780,30 +2888,19 @@ dependencies = [ [[package]] name = "ecdsa" -version = "0.17.0-rc.16" +version = "0.17.0-rc.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91bbdd377139884fafcad8dc43a760a3e1e681aa26db910257fa6535b70e1829" +checksum = "54fb064faabbee66e1fc8e5c5a9458d4269dc2d8b638fe86a425adb2510d1a96" dependencies = [ "der 0.8.0", - "digest 0.11.2", - "elliptic-curve 0.14.0-rc.28", + "digest 0.11.3", + "elliptic-curve 0.14.0-rc.32", "rfc6979 0.5.0", - "signature 3.0.0-rc.10", - "spki 0.8.0-rc.4", + "signature 3.0.0", + "spki 0.8.0", "zeroize", ] -[[package]] -name = "ece-native" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d8e2c05464ca407a32663c1f119abd2a0f8d948879c160fc6cf5b86b6c05d6" -dependencies = [ - "aes-gcm 0.10.3", - "hkdf 0.12.4", - "sha2 0.10.9", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -2815,22 +2912,12 @@ dependencies = [ [[package]] name = "ed25519" -version = "3.0.0-rc.4" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ - "pkcs8 0.11.0-rc.11", - "signature 3.0.0-rc.10", -] - -[[package]] -name = "ed25519-compact" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ce99a9e19c84beb4cc35ece85374335ccc398240712114c85038319ed709bd" -dependencies = [ - "ct-codecs", - "getrandom 0.3.4", + "pkcs8 0.11.0", + "signature 3.0.0", ] [[package]] @@ -2848,37 +2935,25 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "3.0.0-pre.6" +version = "3.0.0-pre.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" +checksum = "20449acd54b660981ae5caa2bcb56d1fe7f25f2e37a38ec507400fab034d4bb6" dependencies = [ "curve25519-dalek 5.0.0-pre.6", - "ed25519 3.0.0-rc.4", - "rand_core 0.10.0", + "ed25519 3.0.0", + "rand_core 0.10.1", "serde", "sha2 0.11.0", - "signature 3.0.0-rc.10", + "signature 3.0.0", "subtle", "zeroize", ] -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] @@ -2895,7 +2970,6 @@ dependencies = [ "ff", "generic-array 0.14.7", "group", - "hkdf 0.12.4", "pem-rfc7468 0.7.0", "pkcs8 0.10.2", "rand_core 0.6.4", @@ -2906,20 +2980,20 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.14.0-rc.28" +version = "0.14.0-rc.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde7860544606d222fd6bd6d9f9a0773321bf78072a637e1d560a058c0031978" +checksum = "cda94f31325c4275e9706adecbb6f0650dee2f904c915a98e3d81adaaaa757aa" dependencies = [ "base16ct 1.0.0", - "crypto-bigint 0.7.0-rc.28", - "crypto-common 0.2.1", - "digest 0.11.2", + "crypto-bigint 0.7.3", + "crypto-common 0.2.2", + "digest 0.11.3", "hkdf 0.13.0", "hybrid-array", "once_cell", "pem-rfc7468 1.0.0", - "pkcs8 0.11.0-rc.11", - "rand_core 0.10.0", + "pkcs8 0.11.0", + "rand_core 0.10.1", "rustcrypto-ff", "rustcrypto-group", "sec1 0.8.1", @@ -2929,14 +3003,15 @@ dependencies = [ [[package]] name = "email" -version = "0.2.9" +version = "1.0.0" dependencies = [ "anyhow", + "async-trait", "config", "lettre", - "metrics 0.24.5", - "regex", + "queue", "serde", + "serde_json", "tokio", "tracing", ] @@ -2951,26 +3026,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "email-server" -version = "0.2.9" -dependencies = [ - "anyhow", - "chrono", - "clap", - "config", - "db", - "hyper 0.14.32", - "metrics 0.22.4", - "metrics-exporter-prometheus 0.13.1", - "observability", - "sea-orm", - "serde_json", - "service", - "tokio", - "tracing", -] - [[package]] name = "email_address" version = "0.2.9" @@ -2987,24 +3042,10 @@ dependencies = [ ] [[package]] -name = "enum-ordinalize" -version = "4.3.2" +name = "entities" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" [[package]] name = "enum_dispatch" @@ -3018,29 +3059,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equator" version = "0.4.2" @@ -3067,15 +3085,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" -dependencies = [ - "serde", -] - [[package]] name = "errno" version = "0.3.14" @@ -3097,6 +3106,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -3129,17 +3148,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "evmap" -version = "11.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8" -dependencies = [ - "hashbag", - "left-right", - "smallvec", -] - [[package]] name = "exr" version = "1.74.0" @@ -3157,9 +3165,9 @@ dependencies = [ [[package]] name = "fancy-regex" -version = "0.17.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ "bit-set", "regex-automata", @@ -3167,58 +3175,26 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "2.3.0" +name = "faster-hex" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "fctool" -version = "0.2.9" -dependencies = [ - "agent", - "ammonia", - "base64 0.22.1", - "chrono", - "csv", - "db", - "git", - "git2", - "models", - "pulldown-cmark", - "queue", - "quick-xml 0.37.5", - "redis", - "regex", - "reqwest 0.13.2", - "sea-orm", - "serde", - "serde_json", - "sqlparser", - "tokio", - "tracing", - "uuid", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -3253,13 +3229,12 @@ checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -3281,7 +3256,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-ng-sys", "miniz_oxide", "zlib-rs", ] @@ -3294,7 +3268,18 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin", + "spin 0.9.8", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", ] [[package]] @@ -3339,22 +3324,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.32" @@ -3439,9 +3420,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -3460,21 +3441,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link", - "windows-result", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3497,15 +3463,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -3540,25 +3497,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 6.0.0", - "rand_core 0.10.0", + "rand_core 0.10.1", "wasip2", "wasip3", - "wasm-bindgen", -] - -[[package]] -name = "getset" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", ] [[package]] @@ -3582,204 +3525,666 @@ dependencies = [ [[package]] name = "gif" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" dependencies = [ "color_quant", "weezl", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "gingress" -version = "0.2.9" -dependencies = [ - "anyhow", - "clap", - "dashmap", - "futures", - "futures-util", - "gingress-proxy", - "k8s-openapi", - "kube", - "observability", - "rustls-pemfile", - "serde", - "serde_json", - "serde_yaml", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", - "x509-parser", -] - -[[package]] -name = "gingress-proxy" -version = "0.2.9" -dependencies = [ - "anyhow", - "async-trait", - "dashmap", - "futures-util", - "http 1.4.0", - "observability", - "once_cell", - "pingora", - "pingora-cache", - "pingora-load-balancing", - "pingora-proxy", - "rustls", - "rustls-pemfile", - "serde", - "serde_json", - "serde_yaml", - "thiserror 2.0.18", - "tokio", - "tracing", -] - [[package]] name = "git" -version = "0.2.9" +version = "1.0.0" dependencies = [ "actix-web", "anyhow", - "argon2", + "argon2 0.5.3", "async-stream", "async-trait", "base64 0.22.1", + "cache", "chrono", "config", + "dashmap", "db", "deadpool-redis", - "flate2", - "futures", + "duct", "futures-util", - "git2", - "git2-ext", - "git2-hooks", - "globset", + "gix", + "gix-archive", + "gix-worktree-stream", "hex", - "metrics 0.24.5", - "models", + "hmac 0.13.0", + "juniper", + "miette", + "model", "num_cpus", + "parsefile", "password-hash 0.6.1", + "prost 0.14.3", "redis", - "reqwest 0.13.2", - "rsa 0.9.10", + "reqwest 0.13.3", "russh", - "sea-orm", "serde", "serde_json", "serde_yaml", - "sha1 0.11.0", "sha2 0.11.0", - "ssh-key", - "tar", + "sqlx 0.9.0", + "storage", + "thiserror 2.0.18", "tokio", + "tokio-stream", "tokio-util", + "tonic 0.14.6", + "tonic-build", + "tonic-prost", + "tonic-prost-build", "tracing", "uuid", - "zip 8.4.0", ] [[package]] -name = "git-hook" -version = "0.2.9" -dependencies = [ - "agent", - "anyhow", - "async-trait", - "chrono", - "clap", - "config", - "db", - "git", - "hyper 0.14.32", - "metrics 0.22.4", - "metrics-exporter-prometheus 0.13.1", - "models", - "observability", - "reqwest 0.13.2", - "sea-orm", - "serde_json", - "tokio", - "tokio-util", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "git2" -version = "0.20.4" +name = "gix" +version = "0.83.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "6ce52001b946a6249d5d0d3011df0a042ac3f8a4d013460db6476577b0b9c567" dependencies = [ - "bitflags 2.11.0", - "libc", - "libgit2-sys", - "log", - "openssl-probe 0.1.6", - "openssl-sys", - "url", -] - -[[package]] -name = "git2-ext" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b351b3ef9a04bbd24822138469eddbc8db7c85ae81a13acb6cbb803053de19b" -dependencies = [ - "bstr", - "git2", - "itertools", - "log", - "pkg-config", - "shlex", - "tempfile", - "which", -] - -[[package]] -name = "git2-hooks" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e21a2c5eee3085f2b622805d4f4c878d9519bf70fa76bed0923b82a25f24199" -dependencies = [ - "git2", + "gix-actor", + "gix-archive", + "gix-attributes", + "gix-blame", + "gix-command", + "gix-commitgraph", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-dir", + "gix-discover", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-ignore", + "gix-index", + "gix-lock", + "gix-mailmap", + "gix-merge", + "gix-negotiate", + "gix-object", + "gix-odb", + "gix-pack", "gix-path", - "log", - "shellexpand", + "gix-pathspec", + "gix-prompt", + "gix-protocol", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-revwalk", + "gix-sec", + "gix-shallow", + "gix-status", + "gix-submodule", + "gix-tempfile", + "gix-trace", + "gix-transport", + "gix-traverse", + "gix-url", + "gix-utils", + "gix-validate", + "gix-worktree", + "gix-worktree-state", + "gix-worktree-stream", + "nonempty", + "parking_lot", + "regex", + "serde", + "signal-hook 0.4.4", + "smallvec", "thiserror 2.0.18", ] [[package]] -name = "gitserver" -version = "0.2.9" +name = "gix-actor" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272916673b83714734b15d4ef3c8b5f1ccddb15fea8ff548430b97c1ab7b7ed8" dependencies = [ - "anyhow", - "chrono", - "clap", - "config", - "db", - "git", - "observability", - "tokio", - "tracing", + "bstr", + "gix-date", + "gix-error", + "serde", +] + +[[package]] +name = "gix-archive" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a20ec244b733338d4cb60e5e05eac700dab7fcc689647b1d1daa9396b119342" +dependencies = [ + "bstr", + "flate2", + "gix-date", + "gix-error", + "gix-object", + "gix-path", + "gix-worktree-stream", + "rawzip", + "tar", +] + +[[package]] +name = "gix-attributes" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe17c5a1c0b6f2ef1476aa1d3222ea50cdff67608016613a58bfc3e078046000" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "serde", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-bitmap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecbfc77ec6852294e341ecc305a490b59f2813e6ca42d79efda5099dcab1894" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-blame" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dab9a942ab54a9661ded7397c3bf927274e7afa94494db0d75cfcbde02ca0a" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-diff", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "gix-traverse", + "gix-worktree", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-chunk" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf288be9b60fe7231de03771faa292be1493d84786f68727e33ad1f91764320" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86335306511abe43d75c866d4b1f3d90932fe202edcd43e1314036333e7384d8" +dependencies = [ + "bstr", + "gix-path", + "gix-quote", + "gix-trace", + "shell-words", +] + +[[package]] +name = "gix-commitgraph" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3b5aa0f24e19028c261d229aeeedafcaaa52ebd71021cc15184620fc9d32eb" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", + "serde", +] + +[[package]] +name = "gix-config" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c01848aebd21c67f6ba41f1de8efd46ae96df21f001954a3c9e1517e514d410" +dependencies = [ + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", + "smallvec", + "thiserror 2.0.18", + "unicode-bom", +] + +[[package]] +name = "gix-config-value" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b39ed39ee4c10a3b157f9fb94bac8098d9f8e56201f0cf7dee6c187416c4b2" +dependencies = [ + "bitflags", + "bstr", + "gix-path", + "libc", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-credentials" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ca11598b70811d7b16ff90945a6e57dfe521e85b744e51636965fe39cc8f60" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-date", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-trace", + "gix-url", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-date" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94cdae4eb4b0f4136e3d9b3aa2d2cd03cfb5bb9b636b31263aea2df86d41543" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", + "serde", + "smallvec", +] + +[[package]] +name = "gix-diff" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc08e0fa1a91ff5f24affeab052f198056645e1de004910bde7b82b50ea5982a" +dependencies = [ + "bstr", + "gix-attributes", + "gix-command", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-dir" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a0fc06e9e1e430cbf0a313666976d90f822f461a6525320427aa9b8af5236c" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-discover" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17852e6a501e688a1702b24ebe5b3761d4719455bc869fd29f38b0b859bcad34" +dependencies = [ + "bstr", + "dunce", + "gix-fs", + "gix-path", + "gix-ref", + "gix-sec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-error" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e207b971746ab724fccdfced2e4e19e854744611904a0195d3aa8fda8a110613" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-features" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af375693ad5333d0a2c66b4c5b2cbe9ccc38e34f8e8bf24e4ae42c12307fdc4f" +dependencies = [ + "bytes", + "bytesize", + "crc32fast", + "crossbeam-channel", + "gix-path", + "gix-trace", + "gix-utils", + "libc", + "once_cell", + "parking_lot", + "prodash", + "thiserror 2.0.18", + "walkdir", + "zlib-rs", +] + +[[package]] +name = "gix-filter" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac917dbe9653c9b615d248db91907a365bd779750c9e1b457a9d9fdeece3a08" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-fs" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e1967daac9848757c47c2aef0c57bcadc1a897347f559778249bf286a536c86" +dependencies = [ + "bstr", + "fastrand", + "gix-features", + "gix-path", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-glob" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bf29249a069bf2507f5964f80997f37b134d320ea348d66527726b9be2c38c" +dependencies = [ + "bitflags", + "bstr", + "gix-features", + "gix-path", + "serde", +] + +[[package]] +name = "gix-hash" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf70d1e252337eed16360f8b8ebb71865ece58eab7954b39ce38b420de703d2" +dependencies = [ + "faster-hex", + "gix-features", + "serde", + "sha1-checked", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-hashtable" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d33b455e07b3c16d3b2eeebc7b38d2dafcbf8a653de1138ef55d4c2a1fd0b08b" +dependencies = [ + "gix-hash", + "hashbrown 0.16.1", + "parking_lot", +] + +[[package]] +name = "gix-ignore" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb13fbbeeafee943e52b61fcc88dfddf6a452fcaf0c4d0cdc8f218fa25bbec5" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "serde", + "unicode-bom", +] + +[[package]] +name = "gix-imara-diff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39eb0623e15e4cb83c02ce6a959e48fadd1ae3b715b36b5acc01816e01388c82" +dependencies = [ + "bstr", + "hashbrown 0.16.1", +] + +[[package]] +name = "gix-index" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c3ef97ad08121e4327a6226bd63fed6b9e3c6b976d48bddd4356d9d41191db" +dependencies = [ + "bitflags", + "bstr", + "filetime", + "fnv", + "gix-bitmap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", + "gix-utils", + "gix-validate", + "hashbrown 0.16.1", + "itoa", + "libc", + "memmap2", + "rustix", + "serde", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-lock" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3bc074e5723027b482dcd9ab99d95804a53742f6de812d0172fbba4a186c1" +dependencies = [ + "gix-tempfile", + "gix-utils", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-mailmap" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023d3a6561cbebe45b89e0764d48928ad970667076f16fa5889e6f86d8432086" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-error", + "serde", +] + +[[package]] +name = "gix-merge" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74bbcdcc52b70a32f0a151b024dff9d0fcf56ee48f00d9503e735af9d99ea881" +dependencies = [ + "bstr", + "gix-command", + "gix-diff", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-imara-diff", + "gix-index", + "gix-object", + "gix-path", + "gix-quote", + "gix-revision", + "gix-revwalk", + "gix-tempfile", + "gix-trace", + "gix-worktree", + "nonempty", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-negotiate" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "103d42bfade1b8a96ca5005933127bdad461ce588d92422b2c2daa3ff20d780c" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-object", + "gix-revwalk", +] + +[[package]] +name = "gix-object" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38075a95d7cc5df8afd38e72c617026c1456952207a4120a7f55a3fbf93b4d7" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-utils", + "gix-validate", + "itoa", + "serde", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-odb" +version = "0.80.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeeda12a9663120418735ecdc1250d06eeab0be75700e47b3402a981331716ba" +dependencies = [ + "arc-swap", + "gix-features", + "gix-fs", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "memmap2", + "parking_lot", + "serde", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-pack" +version = "0.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf02e6f5c8f07a069c9ea5245f40d9b14856ada4086091dc99941b49002b4fa" +dependencies = [ + "clru", + "gix-chunk", + "gix-error", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "memmap2", + "serde", + "smallvec", + "thiserror 2.0.18", + "uluru", +] + +[[package]] +name = "gix-packetline" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362246df440ee691699f0664cbf7006a6ece477db6734222be95e4198e5656e6" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.18", ] [[package]] name = "gix-path" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c31d4373bda7fab9eb01822927b55185a378d6e1bf737e0a54c743ad806658" +checksum = "671a6059e8a4c1b7f406e24716499cefa3926e060876fb1959ef225efeee346e" dependencies = [ "bstr", "gix-trace", @@ -3788,20 +4193,345 @@ dependencies = [ ] [[package]] -name = "gix-trace" -version = "0.1.18" +name = "gix-pathspec" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69a13643b8437d4ca6845e08143e847a36ca82903eed13303475d0ae8b162e0" +checksum = "2a84a4f083dd70fb49f4377e13afa6d90df2daaa1c705c49d6ff1331fc7e8855" +dependencies = [ + "bitflags", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-prompt" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e041a626c64cb69e4117fcdf80da8d0e454fba3b1f420412792d191f52251aee" +dependencies = [ + "gix-command", + "gix-config-value", + "parking_lot", + "rustix", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-protocol" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4bee82db63ec635996b96efae71cf467c155fa3f34a556184373224a26c4fd" +dependencies = [ + "bstr", + "gix-date", + "gix-features", + "gix-hash", + "gix-ref", + "gix-shallow", + "gix-transport", + "gix-utils", + "maybe-async", + "nonempty", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-quote" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e97b73791a64bc0fa7dd2c5b3e551136115f97750b876ed1c952c7a7dbaf8be" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ba9cc15f558b274c99349b83130f5ec83459660828fde9718bbbb43a726167" +dependencies = [ + "gix-actor", + "gix-features", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-refspec" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61755b27d57edc8940a1b1593c8c61548ca8e4c02da1ed8d5bfeda9eb2a6b761" +dependencies = [ + "bstr", + "gix-error", + "gix-glob", + "gix-hash", + "gix-revision", + "gix-validate", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-revision" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb5288fac706d3ea3e4e2ba9ec38b78743b8c02f422e18cb342299cfd6ab7e8" +dependencies = [ + "bitflags", + "bstr", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "gix-trace", + "nonempty", + "serde", +] + +[[package]] +name = "gix-revwalk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313813706b073a12ff7f9b2896bf3e6504cdac7cfbc97b1920114724705069f0" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-sec" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3a2d3e504a238136751e646a6c028252286a0ea64ea9974bf0498633407c6" +dependencies = [ + "bitflags", + "gix-path", + "libc", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "gix-shallow" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29187305521bfacf4aefd284ab28dbfa9fb74abd39a5e63dd313b1baa5808c27" +dependencies = [ + "bstr", + "gix-hash", + "gix-lock", + "nonempty", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-status" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c6d2a8c521ffa205fe7e268c82e6d1378ba37cd826ca10ab6129fdc29a4b65" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-submodule" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd5fc8692890bd71a596e540fd4c364f8460eaa82c4eaaedebde6e1e3eb4d91" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-tempfile" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "691ea1e31435c7e7d4d04705ec9d1c0d9482c46b2acf512bc723939d8f0af7fb" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "parking_lot", + "signal-hook 0.4.4", + "signal-hook-registry", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f23569e55f2ffaf958617353b9734a7d52a7c19c439eeaa5e3efc217fd2270e" + +[[package]] +name = "gix-transport" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd6a5c676b92d4ead5f5a2b2935024415dec69edc997b6090ca9cac010a3018" +dependencies = [ + "bstr", + "gix-command", + "gix-features", + "gix-packetline", + "gix-quote", + "gix-sec", + "gix-url", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-traverse" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14b7052c0786676c03e71fcfde7d7f0f8e8316e642b5cec6bb3998719b2ce5c" +dependencies = [ + "bitflags", + "gix-commitgraph", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-revwalk", + "smallvec", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-url" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35842d099e813f6f6bba529e88d4670572149c3df79b7a412952259887721ece" +dependencies = [ + "bstr", + "gix-path", + "percent-encoding", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-utils" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e477b4f07a6e8da4ba791c53c858102959703c60d70f199932010d5b94adb2c" +dependencies = [ + "bstr", + "fastrand", + "unicode-normalization", +] [[package]] name = "gix-validate" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec1eff98d91941f47766367cba1be746bab662bad761d9891ae6f7882f7840b" +checksum = "e26ac2602b43eadfdca0560b81d3341944162a3c9f64ccdeef8fc501ad80dad5" dependencies = [ "bstr", ] +[[package]] +name = "gix-worktree" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69955eb5e2910832f88d041964b809eee01dadd579237e0b55efec58fd406fd" +dependencies = [ + "bstr", + "gix-attributes", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", + "serde", +] + +[[package]] +name = "gix-worktree-state" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a96dccbcf9e8fe0291c55f06e08da93ebb2e691c1311276f541eefcc6d70800" +dependencies = [ + "bstr", + "gix-features", + "gix-filter", + "gix-fs", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", + "thiserror 2.0.18", +] + +[[package]] +name = "gix-worktree-stream" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8444b8ed4662e1a0c97f3eceda29630001a1bbb2632201e50312623e594213" +dependencies = [ + "gix-attributes", + "gix-error", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-object", + "gix-path", + "gix-traverse", + "parking_lot", +] + [[package]] name = "glam" version = "0.14.0" @@ -3916,31 +4646,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "globset" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "group" version = "0.13.0" @@ -3964,7 +4669,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -3973,9 +4678,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -3983,7 +4688,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -4003,10 +4708,13 @@ dependencies = [ ] [[package]] -name = "hashbag" -version = "0.1.13" +name = "hash32" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] [[package]] name = "hashbrown" @@ -4022,9 +4730,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", -] [[package]] name = "hashbrown" @@ -4048,6 +4753,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "hashlink" version = "0.10.0" @@ -4057,6 +4768,25 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -4120,31 +4850,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.2", -] - -[[package]] -name = "hmac-sha1-compact" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823" - -[[package]] -name = "hmac-sha256" -version = "1.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac-sha512" -version = "1.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019ece39bbefc17f13f677a690328cb978dbf6790e141a3c24e66372cb38588b" -dependencies = [ - "digest 0.10.7", + "digest 0.11.3", ] [[package]] @@ -4167,17 +4873,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "html5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" -dependencies = [ - "log", - "markup5ever", - "match_token", -] - [[package]] name = "http" version = "0.2.12" @@ -4233,12 +4928,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" - [[package]] name = "httparse" version = "1.10.1" @@ -4252,10 +4941,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] -name = "hybrid-array" -version = "0.4.8" +name = "human_format" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "ctutils", "subtle", @@ -4273,6 +4968,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -4288,22 +4984,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.13", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -4311,21 +5006,34 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", - "log", - "rustls", + "rustls 0.23.40", "rustls-native-certs", - "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] @@ -4334,42 +5042,13 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -4382,15 +5061,17 @@ dependencies = [ "futures-util", "http 1.4.0", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.9.0", "ipnet", "libc", "percent-encoding", "pin-project-lite", "socket2 0.6.3", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -4419,12 +5100,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -4432,9 +5114,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -4445,9 +5127,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -4459,15 +5141,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -4479,15 +5161,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -4523,9 +5205,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -4573,9 +5255,9 @@ dependencies = [ [[package]] name = "imageproc" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8046da590889acc65f5880004580ebb269bbef84d6c0f5c543ec2dece46638" +checksum = "645329c490783f3ea465d2b6c7c08286fece97f15e714fd533b6c70a3ead2252" dependencies = [ "ab_glyph", "approx", @@ -4584,7 +5266,7 @@ dependencies = [ "itertools", "nalgebra", "num", - "rand 0.9.2", + "rand 0.9.4", "rand_distr", "rayon", "rustdct", @@ -4592,9 +5274,9 @@ dependencies = [ [[package]] name = "imgref" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" [[package]] name = "impl-more" @@ -4614,12 +5296,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -4664,35 +5346,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "internal-russh-forked-ssh-key" -version = "0.6.18+upstream-0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f8a978272e3cbdf4768f7363eb1c8e1e6ba63c52a3ed05e29e222da4aec7cb" -dependencies = [ - "argon2", - "bcrypt-pbkdf", - "crypto-bigint 0.7.0-rc.28", - "ecdsa 0.17.0-rc.16", - "ed25519-dalek 3.0.0-pre.6", - "hex", - "hmac 0.13.0", - "num-bigint-dig", - "p256 0.14.0-rc.7", - "p384 0.14.0-rc.7", - "p521", - "rand_core 0.10.0", - "rsa 0.10.0-rc.16", - "sec1 0.8.1", - "sha1 0.11.0", - "sha2 0.11.0", - "signature 3.0.0-rc.10", - "ssh-cipher 0.2.0", - "ssh-encoding 0.2.0", - "subtle", - "zeroize", -] - [[package]] name = "internal-russh-num-bigint" version = "0.5.0" @@ -4702,7 +5355,7 @@ dependencies = [ "num-integer", "num-traits", "rand 0.10.1", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -4716,28 +5369,32 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "io-close" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "issues" +version = "1.0.0" + [[package]] name = "itertools" version = "0.14.0" @@ -4755,28 +5412,94 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", + "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde_core", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4789,9 +5512,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -4800,90 +5523,37 @@ dependencies = [ ] [[package]] -name = "json-patch" -version = "4.2.0" +name = "juniper" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +checksum = "118749c965e171dcccffb7ca0dff6ab00451b08ba2710914caaaebc3686a7c3f" dependencies = [ - "jsonptr", + "arcstr", + "async-trait", + "auto_enums", + "compact_str", + "derive_more", + "fnv", + "futures", + "indexmap 2.14.0", + "itertools", + "juniper_codegen", + "ref-cast", "serde", - "serde_json", - "thiserror 2.0.18", + "static_assertions", ] [[package]] -name = "jsonpath-rust" -version = "1.0.4" +name = "juniper_codegen" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633a7320c4bb672863a3782e89b9094ad70285e097ff6832cddd0ec615beadfa" +checksum = "8634f500d6d2ec5c91c115b83e15d998d9ea05645aaa43f7afec09e660c483ba" dependencies = [ - "pest", - "pest_derive", - "regex", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "jsonptr" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "jwt-simple" -version = "0.12.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3991f54af4b009bb6efe01aa5a4fcce9ca52f3de7a104a3f6b6e2ad36c852c48" -dependencies = [ - "anyhow", - "binstring", - "blake2b_simd", - "coarsetime", - "ct-codecs", - "ed25519-compact", - "hmac-sha1-compact", - "hmac-sha256", - "hmac-sha512", - "k256", - "p256 0.13.2", - "p384 0.13.1", - "rand 0.8.5", - "serde", - "serde_json", - "superboring", - "thiserror 2.0.18", - "zeroize", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "once_cell", - "sha2 0.10.9", - "signature 2.2.0", -] - -[[package]] -name = "k8s-openapi" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b326f5219dd55872a72c1b6ddd1b830b8334996c667449c29391d657d78d5e" -dependencies = [ - "base64 0.22.1", - "jiff", - "serde", - "serde_json", + "derive_more", + "proc-macro2", + "quote", + "syn 2.0.117", + "url", ] [[package]] @@ -4902,116 +5572,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" dependencies = [ - "crypto-common 0.2.1", - "rand_core 0.10.0", + "crypto-common 0.2.2", + "rand_core 0.10.1", ] [[package]] -name = "kube" -version = "3.1.0" +name = "kstring" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc5a6a69da2975ed9925d56b5dcfc9cc739b66f37add06785b7c9f6d1e88741" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" dependencies = [ - "k8s-openapi", - "kube-client", - "kube-core", - "kube-derive", - "kube-runtime", -] - -[[package]] -name = "kube-client" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcaf2d1f1a91e1805d4cd82e8333c022767ae8ffd65909bbef6802733a7dd40" -dependencies = [ - "base64 0.22.1", - "bytes", - "either", - "futures", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.8.1", - "hyper-rustls", - "hyper-timeout", - "hyper-util", - "jiff", - "jsonpath-rust", - "k8s-openapi", - "kube-core", - "pem", - "rustls", - "secrecy", "serde", - "serde_json", - "serde_yaml", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tower 0.5.3", - "tower-http", - "tracing", -] - -[[package]] -name = "kube-core" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f126d2db7a8b532ec1d839ece2a71e2485dc3bbca6cc3c3f929becaa810e719e" -dependencies = [ - "derive_more 2.1.1", - "form_urlencoded", - "http 1.4.0", - "jiff", - "json-patch", - "k8s-openapi", - "schemars", - "serde", - "serde-value", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "kube-derive" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b9b97e121fce957f9cafc6da534abc4276983ab03190b76c09361e2df849fa" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn 2.0.117", -] - -[[package]] -name = "kube-runtime" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c072737075826ee74d3e615e80334e41e617ca3d14fb46ef7cdfda822d6f15f2" -dependencies = [ - "ahash 0.8.12", - "async-broadcast", - "async-stream", - "backon", - "educe", - "futures", - "hashbrown 0.16.1", - "hostname", - "json-patch", - "k8s-openapi", - "kube-client", - "parking_lot", - "pin-project", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "tracing", + "static_assertions", ] [[package]] @@ -5026,7 +5598,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -5041,42 +5613,28 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" -[[package]] -name = "left-right" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a" -dependencies = [ - "crossbeam-utils", - "loom", - "slab", -] - [[package]] name = "lettre" -version = "0.11.20" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471816f3e24b85e820dee02cde962379ea1a669e5242f19c61bcbcffedf4c4fb" +checksum = "0da65617f6cb926332d039cb578aad56178da86e128db6a1b09f4c94fa5b3349" dependencies = [ - "async-trait", "base64 0.22.1", "email-encoding", "email_address", "fastrand", - "futures-io", "futures-util", + "hostname", "httpdate", "idna", "mime", + "native-tls", "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls", "socket2 0.6.3", "tokio", - "tokio-rustls", "url", - "webpki-roots 1.0.6", ] [[package]] @@ -5136,12 +5694,6 @@ dependencies = [ "lexical-util", ] -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - [[package]] name = "libc" version = "0.2.186" @@ -5158,20 +5710,6 @@ dependencies = [ "cc", ] -[[package]] -name = "libgit2-sys" -version = "0.18.3+1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" -dependencies = [ - "cc", - "libc", - "libssh2-sys", - "libz-sys", - "openssl-sys", - "pkg-config", -] - [[package]] name = "libm" version = "0.2.16" @@ -5180,14 +5718,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.0", + "bitflags", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.5", ] [[package]] @@ -5202,40 +5740,10 @@ dependencies = [ ] [[package]] -name = "libssh2-sys" -version = "0.3.1" +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-ng-sys" -version = "1.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be734b33b7bc6a42d92d23e25e69758f866cf564a88d0bf80866fcf5a52c2255" -dependencies = [ - "cmake", - "libc", -] - -[[package]] -name = "libz-sys" -version = "1.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" @@ -5245,9 +5753,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-channel" @@ -5260,17 +5768,6 @@ dependencies = [ "local-waker", ] -[[package]] -name = "local-ip-address" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b0187df4e614e42405b49511b82ff7a1774fbd9a816060ee465067847cac22" -dependencies = [ - "libc", - "neli", - "windows-sys 0.61.2", -] - [[package]] name = "local-waker" version = "0.1.4" @@ -5292,19 +5789,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "loop9" version = "0.1.5" @@ -5314,35 +5798,6 @@ dependencies = [ "imgref", ] -[[package]] -name = "lopdf" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c8ecfc6c72051981c0459f75ccc585e7ff67c70829560cda8e647882a9abff" -dependencies = [ - "chrono", - "encoding_rs", - "flate2", - "indexmap 2.13.0", - "itoa", - "log", - "md-5", - "nom 7.1.3", - "rangemap", - "rayon", - "time", - "weezl", -] - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru" version = "0.16.4" @@ -5358,21 +5813,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rust2" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" -dependencies = [ - "sha2 0.10.9", -] - -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "mac_address" version = "1.1.8" @@ -5384,34 +5824,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "markup5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "matchers" version = "0.2.0" @@ -5427,6 +5839,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "matrixmultiply" version = "0.3.10" @@ -5437,6 +5855,17 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "maybe-async" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -5457,6 +5886,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + [[package]] name = "md5" version = "0.7.0" @@ -5470,12 +5909,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "memoffset" -version = "0.6.5" +name = "memmap2" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ - "autocfg", + "libc", ] [[package]] @@ -5488,150 +5927,38 @@ dependencies = [ ] [[package]] -name = "metrics" -version = "0.22.4" +name = "miette" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d05972e8cbac2671e85aa9d04d9160d193f8bebd1a5c1a2f4542c62e65d1d0" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" dependencies = [ - "ahash 0.8.12", - "portable-atomic", + "cfg-if", + "miette-derive", + "unicode-width", ] [[package]] -name = "metrics" -version = "0.24.5" +name = "miette-derive" +version = "7.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ - "portable-atomic", - "rapidhash", -] - -[[package]] -name = "metrics-aggregator" -version = "0.2.9" -dependencies = [ - "actix-rt", - "actix-web", - "anyhow", - "awc", - "chrono", - "clap", - "config", - "futures", - "metrics 0.24.5", - "metrics-exporter-prometheus 0.18.3", - "observability", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", - "reqwest 0.13.2", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tokio-util", - "tower 0.5.3", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", - "url", -] - -[[package]] -name = "metrics-exporter-prometheus" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf4e7146e30ad172c42c39b3246864bd2d3c6396780711a1baf749cfe423e21" -dependencies = [ - "base64 0.21.7", - "hyper 0.14.32", - "hyper-tls 0.5.0", - "indexmap 2.13.0", - "ipnet", - "metrics 0.22.4", - "metrics-util 0.16.3", - "quanta", - "thiserror 1.0.69", - "tokio", - "tracing", -] - -[[package]] -name = "metrics-exporter-prometheus" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108" -dependencies = [ - "base64 0.22.1", - "evmap", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "indexmap 2.13.0", - "ipnet", - "metrics 0.24.5", - "metrics-util 0.20.3", - "quanta", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "metrics-util" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b07a5eb561b8cbc16be2d216faf7757f9baf3bfb94dbb0fae3df8387a5bb47f" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "hashbrown 0.14.5", - "metrics 0.22.4", - "num_cpus", - "quanta", - "sketches-ddsketch 0.2.2", -] - -[[package]] -name = "metrics-util" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e56997f084e57b045edf17c3ed8ba7f9f779c670df8206dfd1c736f4c02dc4a" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "hashbrown 0.16.1", - "metrics 0.24.5", - "quanta", - "rand 0.9.2", - "rand_xoshiro", - "rapidhash", - "sketches-ddsketch 0.3.1", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] name = "migrate" -version = "0.2.9" -dependencies = [ - "async-trait", - "models", - "sea-orm", - "sea-orm-migration", - "sea-query", -] - -[[package]] -name = "migrate-cli" -version = "0.2.9" +version = "1.0.0" dependencies = [ "anyhow", "clap", - "config", - "dotenvy", - "migrate", - "sea-orm", + "serde", + "sqlx 0.9.0", "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -5650,19 +5977,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "mime_guess2" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" -dependencies = [ - "mime", - "phf", - "phf_codegen", - "phf_shared", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -5693,27 +6007,28 @@ dependencies = [ [[package]] name = "ml-kem" -version = "0.3.0-rc.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8198b5db27ac9773534c371751a59dc18aec8b80aa141e69abfdd1dec2e3f78c" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" dependencies = [ "hybrid-array", "kem", "module-lattice", - "rand_core 0.10.0", + "pkcs8 0.11.0", + "rand_core 0.10.1", "sha3", ] [[package]] -name = "models" -version = "0.2.9" +name = "model" +version = "1.0.0" dependencies = [ "chrono", + "db", "rust_decimal", - "sea-orm", "serde", "serde_json", - "utoipa", + "sqlx 0.9.0", "uuid", ] @@ -5758,6 +6073,12 @@ dependencies = [ "pxfm", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "nalgebra" version = "0.34.2" @@ -5797,7 +6118,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" dependencies = [ - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -5809,7 +6130,7 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.2.1", + "openssl-probe", "openssl-sys", "schannel", "security-framework", @@ -5817,64 +6138,23 @@ dependencies = [ "tempfile", ] -[[package]] -name = "neli" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" -dependencies = [ - "bitflags 2.11.0", - "byteorder", - "derive_builder", - "getset", - "libc", - "log", - "neli-proc-macros", - "parking_lot", -] - -[[package]] -name = "neli-proc-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609" -dependencies = [ - "either", - "proc-macro2", - "quote", - "serde", - "syn 2.0.117", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -5883,7 +6163,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -5900,10 +6180,19 @@ dependencies = [ "ed25519-dalek 2.2.0", "getrandom 0.2.17", "log", - "rand 0.8.5", + "rand 0.8.6", "signatory", ] +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -5923,21 +6212,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" +dependencies = [ + "serde", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -5953,7 +6242,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" dependencies = [ - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -5990,8 +6279,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", - "serde", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -6007,9 +6295,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -6073,63 +6361,6 @@ dependencies = [ "libc", ] -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.0", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.11.0", - "objc2", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-open-directory" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" -dependencies = [ - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - [[package]] name = "object" version = "0.37.3" @@ -6139,42 +6370,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "observability" -version = "0.2.9" -dependencies = [ - "actix-web", - "anyhow", - "chrono", - "futures", - "hostname", - "metrics 0.22.4", - "metrics-exporter-prometheus 0.13.1", - "once_cell", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-otlp", - "opentelemetry_sdk", - "reqwest 0.13.2", - "serde", - "serde_json", - "sysinfo", - "thiserror 2.0.18", - "tokio", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", -] - -[[package]] -name = "oid-registry" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" -dependencies = [ - "asn1-rs", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -6187,6 +6382,28 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "opaque-debug" version = "0.3.1" @@ -6195,15 +6412,14 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "foreign-types", "libc", - "once_cell", "openssl-macros", "openssl-sys", ] @@ -6219,12 +6435,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -6233,9 +6443,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -6243,97 +6453,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "opentelemetry" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "opentelemetry-http" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" -dependencies = [ - "async-trait", - "bytes", - "http 1.4.0", - "opentelemetry", - "reqwest 0.12.28", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" -dependencies = [ - "http 1.4.0", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost 0.14.3", - "reqwest 0.12.28", - "thiserror 2.0.18", - "tokio", - "tonic 0.14.5", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost 0.14.3", - "tonic 0.14.5", - "tonic-prost", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" -dependencies = [ - "futures-channel", - "futures-executor", - "futures-util", - "opentelemetry", - "percent-encoding", - "rand 0.9.2", - "thiserror 2.0.18", - "tokio", - "tokio-stream", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-float" version = "4.6.0" @@ -6352,6 +6471,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "ouroboros" version = "0.18.5" @@ -6376,6 +6505,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -6399,71 +6534,60 @@ dependencies = [ [[package]] name = "p256" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "018bfbb86e05fd70a83e985921241035ee09fcd369c4a2c3680b389a01d2ad28" +checksum = "8b97e3bf0465157ae90975ff52dbeb1362ba618924878c9f74c25baa27a65f9a" dependencies = [ - "ecdsa 0.17.0-rc.16", - "elliptic-curve 0.14.0-rc.28", + "ecdsa 0.17.0-rc.18", + "elliptic-curve 0.14.0-rc.32", "primefield", - "primeorder 0.14.0-rc.7", + "primeorder 0.14.0-rc.9", "sha2 0.11.0", ] [[package]] name = "p384" -version = "0.13.1" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +checksum = "437f30ebcb1e16ff48acead5f08bd69fbcdbc82421687bb48af5c315a0bfab03" dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", - "sha2 0.10.9", -] - -[[package]] -name = "p384" -version = "0.14.0-rc.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c91df688211f5957dbe2ab599dcbcaade8d6d3cdc15c5b350d350d7d07ce423" -dependencies = [ - "ecdsa 0.17.0-rc.16", - "elliptic-curve 0.14.0-rc.28", + "ecdsa 0.17.0-rc.18", + "elliptic-curve 0.14.0-rc.32", "fiat-crypto 0.3.0", "primefield", - "primeorder 0.14.0-rc.7", + "primeorder 0.14.0-rc.9", "sha2 0.11.0", ] [[package]] name = "p521" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6cd9451de522549d36cc78a1b45a699a3d55a872e8ea0c8f0318e502d99e2c" +checksum = "4e9fd792bab86ecf6249561752fb5a413511f999887107dd054bbda5143743d7" dependencies = [ "base16ct 1.0.0", - "ecdsa 0.17.0-rc.16", - "elliptic-curve 0.14.0-rc.28", + "ecdsa 0.17.0-rc.18", + "elliptic-curve 0.14.0-rc.32", "primefield", - "primeorder 0.14.0-rc.7", + "primeorder 0.14.0-rc.9", "sha2 0.11.0", ] [[package]] name = "pageant" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" +checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5" dependencies = [ + "base16ct 1.0.0", "byteorder", "bytes", "delegate", "futures", "log", - "rand 0.8.5", - "sha2 0.10.9", - "thiserror 1.0.69", + "rand 0.10.1", + "sha2 0.11.0", + "thiserror 2.0.18", "tokio", "windows", "windows-strings", @@ -6499,10 +6623,14 @@ dependencies = [ ] [[package]] -name = "parse-size" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" +name = "parsefile" +version = "1.0.0" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_yaml", + "thiserror 2.0.18", +] [[package]] name = "password-hash" @@ -6520,6 +6648,10 @@ name = "password-hash" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "getrandom 0.4.2", + "phc", +] [[package]] name = "paste" @@ -6533,36 +6665,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pbkdf2" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", "hmac 0.13.0", ] -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -6587,136 +6699,51 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2 0.10.9", -] - [[package]] name = "petgraph" -version = "0.7.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", - "indexmap 2.13.0", + "hashbrown 0.15.5", + "indexmap 2.14.0", ] [[package]] name = "pgvector" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" +checksum = "3673cba5b9a124916096a423b806a9f29620972c6c97b08db5f2053e9428b481" dependencies = [ "serde", ] [[package]] -name = "phf" -version = "0.11.3" +name = "phc" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", - "unicase", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", - "unicase", + "base64ct", + "ctutils", + "getrandom 0.4.2", ] [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -6735,245 +6762,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pingora" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "844a13b16e556293f4ea96dc5ac0923ac6f36855a9dfc13b640d0da183f6b5b7" -dependencies = [ - "pingora-core", - "pingora-http", - "pingora-proxy", - "pingora-timeout", -] - -[[package]] -name = "pingora-cache" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59d8c4c939a3a193a3da0e061aa7acf7432431f92ee62a26f5a9e5167a0ade2" -dependencies = [ - "ahash 0.8.12", - "async-trait", - "blake2", - "bstr", - "bytes", - "cf-rustracing", - "cf-rustracing-jaeger", - "hex", - "http 1.4.0", - "httparse", - "httpdate", - "indexmap 1.9.3", - "log", - "lru 0.16.4", - "once_cell", - "parking_lot", - "pingora-core", - "pingora-error", - "pingora-header-serde", - "pingora-http", - "pingora-lru", - "pingora-timeout", - "rand 0.8.5", - "regex", - "rmp", - "rmp-serde", - "serde", - "strum 0.26.3", - "tokio", -] - -[[package]] -name = "pingora-core" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08973c4853cef4c682f7a592907e81a32dcad69476c4846e5de079f16448b177" -dependencies = [ - "ahash 0.8.12", - "async-trait", - "brotli 3.5.0", - "bstr", - "bytes", - "chrono", - "clap", - "daemonize", - "daggy", - "derivative", - "flate2", - "futures", - "h2 0.4.13", - "http 1.4.0", - "httparse", - "httpdate", - "libc", - "log", - "nix 0.24.3", - "once_cell", - "openssl-probe 0.1.6", - "parking_lot", - "percent-encoding", - "pingora-error", - "pingora-http", - "pingora-pool", - "pingora-runtime", - "pingora-timeout", - "prometheus", - "rand 0.8.5", - "regex", - "serde", - "serde_yaml", - "sfv", - "socket2 0.6.3", - "strum 0.26.3", - "strum_macros", - "tokio", - "tokio-stream", - "tokio-test", - "unicase", - "windows-sys 0.59.0", - "zstd", -] - -[[package]] -name = "pingora-error" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9fa97a500e7e5c27a7b8609b9294c8922c9656322285268bfad9520f12feb38" - -[[package]] -name = "pingora-header-serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2705feb8b50d4e734e0c7d3879aa040e655a45656276323ff530e254585dd816" -dependencies = [ - "bytes", - "http 1.4.0", - "httparse", - "pingora-error", - "pingora-http", - "thread_local", - "zstd", - "zstd-safe", -] - -[[package]] -name = "pingora-http" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb52d4651b687fab6abf669539cfd97b7cd94b301fde8f57c63354f9c9cc5e2" -dependencies = [ - "bytes", - "http 1.4.0", - "pingora-error", -] - -[[package]] -name = "pingora-ketama" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0286fb5a0359dca1e2e137dfe14ca4d94f676635a5eae4616bb3d8d4ce06d120" -dependencies = [ - "crc32fast", -] - -[[package]] -name = "pingora-load-balancing" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2606e9e22e72927a69772cefe56b0d41d251c3ffdfcd548a6020fe157fb79ad" -dependencies = [ - "arc-swap", - "async-trait", - "derivative", - "fnv", - "futures", - "http 1.4.0", - "log", - "pingora-core", - "pingora-error", - "pingora-http", - "pingora-ketama", - "pingora-runtime", - "rand 0.8.5", - "tokio", -] - -[[package]] -name = "pingora-lru" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91bb5030596a3d442c0866ac68afe29c14ba558e77c726dcdf7016b0dbb359d9" -dependencies = [ - "arrayvec", - "hashbrown 0.16.1", - "parking_lot", - "rand 0.8.5", -] - -[[package]] -name = "pingora-pool" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67f034be36772f318370d058913db43dbd22c3763ad974c995ba2e4afb2bb52a" -dependencies = [ - "crossbeam-queue", - "log", - "lru 0.16.4", - "parking_lot", - "pingora-timeout", - "thread_local", - "tokio", -] - -[[package]] -name = "pingora-proxy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1e070a98a70d0d05f2fdcfb706237e06a043b2fbc9261e8772a3459cc2175e" -dependencies = [ - "async-trait", - "bytes", - "clap", - "futures", - "h2 0.4.13", - "http 1.4.0", - "log", - "once_cell", - "pingora-cache", - "pingora-core", - "pingora-error", - "pingora-http", - "rand 0.8.5", - "regex", - "tokio", -] - -[[package]] -name = "pingora-runtime" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e371315b1c44c2e5a8788fdc61577527b785e121e6ff49144755f40d86511430" -dependencies = [ - "once_cell", - "rand 0.8.5", - "thread_local", - "tokio", -] - -[[package]] -name = "pingora-timeout" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a853fee5ce510a7f5db2561f99c752724112ed13fc3820e70d462d278d704ea" -dependencies = [ - "once_cell", - "parking_lot", - "pin-project-lite", - "thread_local", - "tokio", -] - [[package]] name = "pkcs1" version = "0.7.5" @@ -6992,24 +6780,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" dependencies = [ "der 0.8.0", - "spki 0.8.0-rc.4", + "spki 0.8.0", ] [[package]] name = "pkcs5" -version = "0.8.0-rc.13" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a777c6e26664bc9504b3ce3f6133f8f20d9071f130a4f9fcbd3186959d8dd6" +checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" dependencies = [ "aes 0.9.0", - "aes-gcm 0.11.0-rc.3", - "cbc 0.2.0", + "cbc", "der 0.8.0", - "pbkdf2 0.13.0", - "rand_core 0.10.0", + "pbkdf2", + "rand_core 0.10.1", "scrypt", "sha2 0.11.0", - "spki 0.8.0-rc.4", + "spki 0.8.0", ] [[package]] @@ -7024,21 +6811,21 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.11.0-rc.11" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ "der 0.8.0", "pkcs5", - "rand_core 0.10.0", - "spki 0.8.0-rc.4", + "rand_core 0.10.1", + "spki 0.8.0", ] [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -7046,6 +6833,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + [[package]] name = "pluralizer" version = "0.5.0" @@ -7062,7 +6862,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", + "bitflags", "crc32fast", "fdeflate", "flate2", @@ -7071,20 +6871,9 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash 0.5.1", -] - -[[package]] -name = "poly1305" -version = "0.9.0-rc.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19feddcbdf17fad33f40041c7f9e768faf19455f32a6d52ba1b8b65ffc7b1cae" +checksum = "a00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2e" dependencies = [ "cpufeatures 0.3.0", "universal-hash 0.6.1", @@ -7122,18 +6911,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -7144,12 +6933,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppmd-rust" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -7159,12 +6942,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "prettyplease" version = "0.2.37" @@ -7186,13 +6963,13 @@ dependencies = [ [[package]] name = "primefield" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93401c13cc7ff24684571cfca9d3cf9ebabfaf3d4b7b9963ade41ec54da196b5" +checksum = "1b52e6ee42db392378a95622b463c9740631171d1efce43fa445a569c1600cb6" dependencies = [ - "crypto-bigint 0.7.0-rc.28", - "crypto-common 0.2.1", - "rand_core 0.10.0", + "crypto-bigint 0.7.3", + "crypto-common 0.2.2", + "rand_core 0.10.1", "rustcrypto-ff", "subtle", "zeroize", @@ -7209,11 +6986,11 @@ dependencies = [ [[package]] name = "primeorder" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c5c8a39bcd764bfedf456e8d55e115fe86dda3e0f555371849f2a41cbc9706" +checksum = "0556580e42c19833f5d232aca11a7687a503ee41f937b54f5ae1d50fc2a6a36a" dependencies = [ - "elliptic-curve 0.14.0-rc.28", + "elliptic-curve 0.14.0-rc.32", ] [[package]] @@ -7232,7 +7009,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.8+spec-1.1.0", + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] @@ -7280,39 +7057,35 @@ dependencies = [ ] [[package]] -name = "profiling" -version = "1.0.17" +name = "prodash" +version = "31.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "962200e2d7d551451297d9fdce85138374019ada198e30ea9ede38034e27604c" +dependencies = [ + "bytesize", + "human_format", + "parking_lot", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" dependencies = [ "quote", "syn 2.0.117", ] -[[package]] -name = "prometheus" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" -dependencies = [ - "cfg-if", - "fnv", - "lazy_static", - "memchr", - "parking_lot", - "protobuf", - "thiserror 1.0.69", -] - [[package]] name = "prost" version = "0.13.5" @@ -7333,6 +7106,27 @@ dependencies = [ "prost-derive 0.14.3", ] +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck 0.5.0", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost 0.14.3", + "prost-types 0.14.3", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.117", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.13.5" @@ -7369,16 +7163,19 @@ dependencies = [ ] [[package]] -name = "protobuf" -version = "2.28.0" +name = "prost-types" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost 0.14.3", +] [[package]] name = "psm" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" dependencies = [ "ar_archive_writer", "cc", @@ -7406,34 +7203,35 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.12.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags 2.11.0", - "getopts", + "bitflags", "memchr", - "pulldown-cmark-escape", "unicase", ] [[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" +name = "pulldown-cmark-to-cmark" +version = "22.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +checksum = "50793def1b900256624a709439404384204a5dc3a6ec580281bfaac35e882e90" +dependencies = [ + "pulldown-cmark", +] [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "qdrant-client" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d0a9b168ecf8f30a3eb7e8f4766e3050701242ffbe99838b58e6c4251e7211" +checksum = "82cef4e669bcf9c07471463adab5ee080dd9bc9381f3652ea4981f6030b2c309" dependencies = [ "anyhow", "derive_builder", @@ -7441,7 +7239,7 @@ dependencies = [ "futures-util", "parking_lot", "prost 0.13.5", - "prost-types", + "prost-types 0.13.5", "reqwest 0.12.28", "semver", "serde", @@ -7460,41 +7258,19 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi", - "web-sys", - "winapi", -] - [[package]] name = "queue" -version = "0.2.9" +version = "1.0.0" dependencies = [ "anyhow", "async-nats", - "chrono", + "async-trait", "config", - "deadpool-redis", - "futures", "futures-util", - "metrics 0.24.5", - "redis", "serde", "serde_json", - "thiserror 2.0.18", "tokio", - "tokio-stream", "tracing", - "uuid", ] [[package]] @@ -7505,22 +7281,11 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" -dependencies = [ - "encoding_rs", - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", - "serde", ] [[package]] @@ -7534,8 +7299,8 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.2", - "rustls", + "rustc-hash", + "rustls 0.23.40", "socket2 0.6.3", "thiserror 2.0.18", "tokio", @@ -7549,13 +7314,14 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", - "rustc-hash 2.1.2", - "rustls", + "rustc-hash", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -7575,7 +7341,7 @@ dependencies = [ "once_cell", "socket2 0.6.3", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -7605,6 +7371,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "radium" version = "0.7.0" @@ -7613,9 +7390,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -7624,9 +7401,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -7638,9 +7415,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "chacha20 0.10.0", + "chacha20", "getrandom 0.4.2", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -7663,6 +7440,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -7683,9 +7470,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rand_distr" @@ -7694,31 +7481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.2", -] - -[[package]] -name = "rand_xoshiro" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rangemap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" - -[[package]] -name = "rapidhash" -version = "4.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" -dependencies = [ - "rustversion", + "rand 0.9.4", ] [[package]] @@ -7748,7 +7511,7 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha 0.9.0", "simd_helpers", "thiserror 2.0.18", @@ -7771,15 +7534,6 @@ dependencies = [ "rgb", ] -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.11.0", -] - [[package]] name = "rawpointer" version = "0.2.1" @@ -7787,10 +7541,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] -name = "rayon" -version = "1.11.0" +name = "rawzip" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "9d9575f44c8cf85bc843ad666dcdf20d05a7753772bef56eb2a5140282b32150" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -7828,9 +7588,9 @@ dependencies = [ [[package]] name = "redis" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e41a79ae5cbb41257d84cf4cf0db0bb5a95b11bf05c62c351de4fe748620d" +checksum = "72d32a1ac9123f0d84fda64bfc02a271d9868483162dd2d9099b5c362ece064c" dependencies = [ "arc-swap", "arcstr", @@ -7844,10 +7604,12 @@ dependencies = [ "futures-util", "itoa", "log", + "lru", "num-bigint", "percent-encoding", "pin-project-lite", - "rand 0.9.2", + "r2d2", + "rand 0.9.4", "ryu", "sha1_smol", "socket2 0.6.3", @@ -7863,27 +7625,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.18", + "bitflags", ] [[package]] @@ -7958,29 +7709,28 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "futures-channel", "futures-core", "futures-util", - "h2 0.4.13", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", - "hyper-rustls", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.40", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", "tower 0.5.3", "tower-http", @@ -7990,37 +7740,42 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams 0.4.2", "web-sys", - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", - "hyper-tls 0.6.0", + "hyper 1.9.0", + "hyper-rustls 0.27.9", "hyper-util", "js-sys", "log", + "mime", "mime_guess", - "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls 0.23.40", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", - "tokio-native-tls", + "tokio-rustls 0.26.4", "tokio-util", "tower 0.5.3", "tower-http", @@ -8079,7 +7834,7 @@ dependencies = [ "nanoid", "ordered-float 5.3.0", "pin-project-lite", - "reqwest 0.13.2", + "reqwest 0.13.3", "rig-derive", "schemars", "serde", @@ -8093,13 +7848,14 @@ dependencies = [ [[package]] name = "rig-derive" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1a6de0e69cfc929772efa85c19602a341b5837aba5eafe8339ccb26f185116" +checksum = "5ba9149c63403a49ddfd5d373860487e6feef0021bfe5329812d1c4e72ee207c" dependencies = [ - "convert_case 0.10.0", + "convert_case", "deluxe", "indoc", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "serde_json", @@ -8116,7 +7872,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -8149,61 +7905,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "room" -version = "0.2.9" -dependencies = [ - "agent", - "ammonia", - "anyhow", - "async-nats", - "chrono", - "config", - "dashmap", - "db", - "deadpool-redis", - "fctool", - "futures", - "hostname", - "lru 0.12.5", - "metrics 0.22.4", - "models", - "observability", - "queue", - "redis", - "regex-lite", - "sea-orm", - "serde", - "serde_json", - "session", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "utoipa", - "uuid", -] - [[package]] name = "rsa" version = "0.9.10" @@ -8218,7 +7919,6 @@ dependencies = [ "pkcs1 0.7.5", "pkcs8 0.10.2", "rand_core 0.6.4", - "sha2 0.10.9", "signature 2.2.0", "spki 0.7.3", "subtle", @@ -8227,62 +7927,58 @@ dependencies = [ [[package]] name = "rsa" -version = "0.10.0-rc.16" +version = "0.10.0-rc.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb9fd8c1edd9e6a2693623baf0fe77ff05ce022a5d7746900ffc38a15c233de" +checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" dependencies = [ "const-oid 0.10.2", - "crypto-bigint 0.7.0-rc.28", + "crypto-bigint 0.7.3", "crypto-primes", - "digest 0.11.2", + "digest 0.11.3", "pkcs1 0.8.0-rc.4", - "pkcs8 0.11.0-rc.11", - "rand_core 0.10.0", + "pkcs8 0.11.0", + "rand_core 0.10.1", "sha2 0.11.0", - "signature 3.0.0-rc.10", - "spki 0.8.0-rc.4", + "signature 3.0.0", + "spki 0.8.0", "zeroize", ] [[package]] name = "russh" -version = "0.60.2" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e358980fe9b079b99da387117864ee6f0a3fd02f39e5b5fde6af9c2895374" +checksum = "f67013f080c226e5a34db1c71f2567f44d95a6300005bb6cd4e2c8fe3c326d1b" dependencies = [ - "aead 0.6.0-rc.10", - "aes 0.8.4", "aes 0.9.0", - "aes-gcm 0.11.0-rc.3", - "bitflags 2.11.0", - "block-padding 0.3.3", + "async-trait", + "aws-lc-rs", + "bitflags", + "block-padding 0.4.2", "byteorder", "bytes", - "cbc 0.1.2", - "cbc 0.2.0", - "cipher 0.5.1", - "crypto-bigint 0.7.0-rc.28", - "ctr 0.10.0", - "ctr 0.9.2", + "cbc", + "cipher 0.5.2", + "crypto-bigint 0.7.3", + "ctr 0.10.1", "curve25519-dalek 5.0.0-pre.6", "data-encoding", "delegate", "der 0.8.0", - "digest 0.10.7", - "ecdsa 0.17.0-rc.16", - "ed25519-dalek 3.0.0-pre.6", - "elliptic-curve 0.14.0-rc.28", + "digest 0.11.3", + "ecdsa 0.17.0-rc.18", + "ed25519-dalek 3.0.0-pre.7", + "elliptic-curve 0.14.0-rc.32", "enum_dispatch", + "flate2", "futures", "generic-array 1.4.1", "getrandom 0.2.17", "ghash 0.6.0", "hex-literal", "hkdf 0.13.0", - "hmac 0.12.1", "hmac 0.13.0", "inout 0.1.4", - "internal-russh-forked-ssh-key", "internal-russh-num-bigint", "keccak", "log", @@ -8290,33 +7986,30 @@ dependencies = [ "ml-kem", "module-lattice", "num-bigint", - "p256 0.14.0-rc.7", - "p384 0.14.0-rc.7", + "p256 0.14.0-rc.9", + "p384", "p521", "pageant", - "pbkdf2 0.12.2", - "pbkdf2 0.13.0", + "pbkdf2", "pkcs1 0.8.0-rc.4", "pkcs5", - "pkcs8 0.11.0-rc.11", + "pkcs8 0.11.0", "polyval 0.7.1", "rand 0.10.1", - "rand_core 0.10.0", - "ring", - "rsa 0.10.0-rc.16", + "rand_core 0.10.1", + "rsa 0.10.0-rc.18", "russh-cryptovec", "russh-util", "salsa20", "scrypt", "sec1 0.8.1", - "sha1 0.10.6", "sha1 0.11.0", - "sha2 0.10.9", "sha2 0.11.0", "sha3", - "signature 3.0.0-rc.10", - "spki 0.8.0-rc.4", - "ssh-encoding 0.2.0", + "signature 3.0.0", + "spki 0.8.0", + "ssh-encoding", + "ssh-key", "subtle", "thiserror 2.0.18", "tokio", @@ -8328,13 +8021,13 @@ dependencies = [ [[package]] name = "russh-cryptovec" -version = "0.59.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36140e8a20297bc2e8338807c3d9ca911f7fa49d7539cbcd6d48d3befd70efd8" +checksum = "443f6bbcfacb34a1aab2b12b99bf08e0c63abdc5a0db261901365df9d57fff51" dependencies = [ "log", "nix 0.31.3", - "ssh-encoding 0.2.0", + "ssh-encoding", "windows-sys 0.61.2", ] @@ -8352,33 +8045,21 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.41.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" +checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995" dependencies = [ "arrayvec", "borsh", "bytes", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "serde", "serde_json", "wasm-bindgen", ] -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.2" @@ -8400,7 +8081,7 @@ version = "0.14.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6" dependencies = [ - "rand_core 0.10.0", + "rand_core 0.10.1", "subtle", ] @@ -8410,7 +8091,7 @@ version = "0.14.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" dependencies = [ - "rand_core 0.10.0", + "rand_core 0.10.1", "rustcrypto-ff", "subtle", ] @@ -8438,22 +8119,13 @@ dependencies = [ "transpose", ] -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -8462,15 +8134,28 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -8481,7 +8166,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", "security-framework", @@ -8498,23 +8183,61 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", ] [[package]] -name = "rustls-webpki" -version = "0.103.10" +name = "rustls-platform-verifier" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.40", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.13", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -8545,7 +8268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ "cfg-if", - "cipher 0.5.1", + "cipher 0.5.2", ] [[package]] @@ -8566,6 +8289,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "schemars" version = "1.2.1" @@ -8591,12 +8323,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -8610,11 +8336,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" dependencies = [ "cfg-if", - "pbkdf2 0.13.0", + "pbkdf2", "salsa20", "sha2 0.11.0", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + [[package]] name = "sea-bae" version = "0.2.1" @@ -8638,7 +8374,7 @@ dependencies = [ "async-trait", "bigdecimal", "chrono", - "derive_more 2.1.1", + "derive_more", "futures-util", "itertools", "log", @@ -8653,8 +8389,8 @@ dependencies = [ "sea-schema", "serde", "serde_json", - "sqlx", - "strum 0.28.0", + "sqlx 0.8.6", + "strum", "thiserror 2.0.18", "time", "tracing", @@ -8673,23 +8409,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "sea-orm-cli" -version = "2.0.0-rc.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9b34d4c8e615079c04eb7863a429c2d2a8bf9c934eb9eeb580f51f36367124" -dependencies = [ - "chrono", - "clap", - "dotenvy", - "glob", - "indoc", - "regex", - "tracing", - "tracing-subscriber", - "url", -] - [[package]] name = "sea-orm-macros" version = "2.0.0-rc.38" @@ -8706,22 +8425,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sea-orm-migration" -version = "2.0.0-rc.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ceb928aac8be83332d34d1fdbc827d43696135a800723ffeb2e0b33b7b495e" -dependencies = [ - "async-trait", - "clap", - "dotenvy", - "sea-orm", - "sea-orm-cli", - "sea-schema", - "tracing", - "tracing-subscriber", -] - [[package]] name = "sea-query" version = "1.0.0-rc.33" @@ -8730,7 +8433,6 @@ checksum = "b04cdb0135c16e829504e93fbe7880513578d56f07aaea152283526590111828" dependencies = [ "chrono", "inherent", - "itoa", "ordered-float 4.6.0", "rust_decimal", "sea-query-derive", @@ -8745,7 +8447,7 @@ version = "1.0.0-rc.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d88ad44b6ad9788c8b9476b6b91f94c7461d1e19d39cd8ea37838b1e6ff5aa8" dependencies = [ - "darling 0.20.11", + "darling", "heck 0.4.1", "proc-macro2", "quote", @@ -8760,7 +8462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a04aeecfe00614fece56336fd35dc385bb9ffed0c75660695ba925e42a3991ef" dependencies = [ "sea-query", - "sqlx", + "sqlx 0.8.6", ] [[package]] @@ -8773,7 +8475,7 @@ dependencies = [ "sea-query", "sea-query-sqlx", "sea-schema-derive", - "sqlx", + "sqlx 0.8.6", ] [[package]] @@ -8822,23 +8524,14 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "zeroize", -] - [[package]] name = "security-framework" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", - "core-foundation", + "bitflags", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -8856,9 +8549,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -8870,16 +8563,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-value" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" -dependencies = [ - "ordered-float 2.10.1", - "serde", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -8913,9 +8596,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -8933,15 +8616,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_plain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" -dependencies = [ - "serde", -] - [[package]] name = "serde_repr" version = "0.1.20" @@ -8971,7 +8645,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -8990,99 +8664,67 @@ dependencies = [ [[package]] name = "service" -version = "0.2.9" +version = "1.0.0" dependencies = [ - "agent", - "anyhow", - "argon2", - "avatar", + "ai", + "argon2 0.6.0-rc.8", + "async-trait", "base64 0.22.1", - "base64ct", - "calamine", + "cache", "captcha-rs", "chacha20poly1305", "chrono", + "comrak", "config", - "csv", "db", "deadpool-redis", "email", - "fctool", - "flate2", - "futures", "git", - "git2", "hex", "hkdf 0.13.0", "hmac 0.13.0", - "http 0.2.12", - "http 1.4.0", - "jwt-simple", - "lopdf", - "mime_guess2", - "models", - "moka", - "observability", - "p256 0.13.2", - "pulldown-cmark", + "model", "queue", - "quick-xml 0.37.5", "rand 0.10.1", + "rand_chacha 0.10.0", "redis", - "regex", - "reqwest 0.13.2", - "room", - "rsa 0.9.10", + "rig-core", + "rsa 0.10.0-rc.18", "rust_decimal", - "sea-orm", "serde", "serde_json", - "serde_yaml", "session", "sha1 0.11.0", "sha2 0.11.0", - "slog", - "sqlparser", - "tempfile", + "sqlx 0.9.0", + "storage", "tokio", - "tokio-stream", + "tokio-util", + "tonic 0.14.6", "tracing", "utoipa", "uuid", - "walkdir", - "web-push-native", - "zip 8.4.0", ] [[package]] name = "session" -version = "0.2.9" +version = "1.0.0" dependencies = [ "actix-service", "actix-utils", "actix-web", "anyhow", "deadpool-redis", - "derive_more 2.1.1", + "derive_more", "rand 0.10.1", "redis", "serde", "serde_json", "tokio", + "tracing", "uuid", ] -[[package]] -name = "sfv" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa1f336066b758b7c9df34ed049c0e693a426afe2b27ff7d5b14f410ab1a132" -dependencies = [ - "base64 0.22.1", - "indexmap 2.13.0", - "rust_decimal", -] - [[package]] name = "sha1" version = "0.10.6" @@ -9102,7 +8744,17 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", +] + +[[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest 0.10.7", + "sha1 0.10.6", ] [[package]] @@ -9130,7 +8782,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -9139,7 +8791,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ - "digest 0.11.2", + "digest 0.11.3", "keccak", ] @@ -9153,20 +8805,65 @@ dependencies = [ ] [[package]] -name = "shellexpand" -version = "3.1.2" +name = "shared_child" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" dependencies = [ - "dirs", + "libc", + "sigchld", + "windows-sys 0.60.2", ] +[[package]] +name = "shared_thread" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook 0.3.18", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -9201,12 +8898,12 @@ dependencies = [ [[package]] name = "signature" -version = "3.0.0-rc.10" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "digest 0.11.2", - "rand_core 0.10.0", + "digest 0.11.3", + "rand_core 0.10.1", ] [[package]] @@ -9228,6 +8925,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -9243,24 +8950,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "sketches-ddsketch" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" - -[[package]] -name = "sketches-ddsketch" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" - [[package]] name = "slab" version = "0.4.12" @@ -9268,15 +8957,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "slog" -version = "2.8.2" +name = "slug" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3b8565691b22d2bdfc066426ed48f837fc0c5f2c8cad8d9718f7f99d6995c1" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" dependencies = [ - "anyhow", - "erased-serde", - "rustversion", - "serde_core", + "deunicode", + "wasm-bindgen", ] [[package]] @@ -9308,6 +8995,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "socketio" +version = "1.0.0" +dependencies = [ + "actix-web", + "actix-ws", + "async-nats", + "async-trait", + "base64 0.22.1", + "deadpool-redis", + "futures-util", + "redis", + "serde", + "serde_json", + "session", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "spin" version = "0.9.8" @@ -9317,6 +9025,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spki" version = "0.7.3" @@ -9329,9 +9043,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.8.0-rc.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", "der 0.8.0", @@ -9339,9 +9053,9 @@ dependencies = [ [[package]] name = "sqlparser" -version = "0.55.0" +version = "0.62.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4521174166bac1ff04fe16ef4524c70144cd29682a45978978ca3d7f4e0be11" +checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec" dependencies = [ "log", "recursive", @@ -9353,11 +9067,24 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", + "sqlx-core 0.8.6", + "sqlx-macros 0.8.6", + "sqlx-mysql 0.8.6", + "sqlx-postgres 0.8.6", + "sqlx-sqlite 0.8.6", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core 0.9.0", + "sqlx-macros 0.9.0", + "sqlx-mysql 0.9.0", + "sqlx-postgres 0.9.0", + "sqlx-sqlite 0.9.0", ] [[package]] @@ -9378,8 +9105,8 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.5", - "hashlink", - "indexmap 2.13.0", + "hashlink 0.10.0", + "indexmap 2.14.0", "log", "memchr", "once_cell", @@ -9398,6 +9125,43 @@ dependencies = [ "uuid", ] +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64 0.22.1", + "bytes", + "cfg-if", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink 0.11.0", + "indexmap 2.14.0", + "log", + "memchr", + "percent-encoding", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + [[package]] name = "sqlx-macros" version = "0.8.6" @@ -9406,8 +9170,21 @@ checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", - "sqlx-core", - "sqlx-macros-core", + "sqlx-core 0.8.6", + "sqlx-macros-core 0.8.6", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core 0.9.0", + "sqlx-macros-core 0.9.0", "syn 2.0.117", ] @@ -9427,15 +9204,41 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", + "sqlx-core 0.8.6", + "sqlx-mysql 0.8.6", + "sqlx-postgres 0.8.6", + "sqlx-sqlite 0.8.6", "syn 2.0.117", "tokio", "url", ] +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core 0.9.0", + "sqlx-mysql 0.9.0", + "sqlx-postgres 0.9.0", + "sqlx-sqlite 0.9.0", + "syn 2.0.117", + "thiserror 2.0.18", + "tokio", + "url", +] + [[package]] name = "sqlx-mysql" version = "0.8.6" @@ -9444,7 +9247,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags", "byteorder", "bytes", "chrono", @@ -9462,24 +9265,53 @@ dependencies = [ "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "rsa 0.9.10", "rust_decimal", "serde", "sha1 0.10.6", "sha2 0.10.9", "smallvec", - "sqlx-core", + "sqlx-core 0.8.6", "stringprep", "thiserror 2.0.18", "time", "tracing", "uuid", - "whoami", + "whoami 1.6.1", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array 0.14.7", + "log", + "percent-encoding", + "rust_decimal", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "sqlx-core 0.9.0", + "thiserror 2.0.18", + "tracing", + "uuid", ] [[package]] @@ -9490,12 +9322,12 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags", "byteorder", "chrono", "crc", "dotenvy", - "etcetera", + "etcetera 0.8.0", "futures-channel", "futures-core", "futures-util", @@ -9505,22 +9337,60 @@ dependencies = [ "home", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "rust_decimal", "serde", "serde_json", "sha2 0.10.9", "smallvec", - "sqlx-core", + "sqlx-core 0.8.6", "stringprep", "thiserror 2.0.18", "time", "tracing", "uuid", - "whoami", + "whoami 1.6.1", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera 0.11.0", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf 0.13.0", + "hmac 0.13.0", + "itoa", + "log", + "md-5 0.11.0", + "memchr", + "rand 0.10.1", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core 0.9.0", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami 2.1.2", ] [[package]] @@ -9531,7 +9401,7 @@ checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", - "flume", + "flume 0.11.1", "futures-channel", "futures-core", "futures-executor", @@ -9542,7 +9412,7 @@ dependencies = [ "percent-encoding", "serde", "serde_urlencoded", - "sqlx-core", + "sqlx-core 0.8.6", "thiserror 2.0.18", "time", "tracing", @@ -9551,76 +9421,90 @@ dependencies = [ ] [[package]] -name = "ssh-cipher" -version = "0.2.0" +name = "sqlx-sqlite" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ - "aes 0.8.4", - "aes-gcm 0.10.3", - "cbc 0.1.2", - "chacha20 0.9.1", - "cipher 0.4.4", - "ctr 0.9.2", - "poly1305 0.8.0", - "ssh-encoding 0.2.0", - "subtle", + "atoi", + "chrono", + "flume 0.12.0", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core 0.9.0", + "thiserror 2.0.18", + "tracing", + "url", + "uuid", ] [[package]] name = "ssh-cipher" -version = "0.3.0-rc.8" +version = "0.3.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20540e2cbcf285a8e0172717b3ae77ccc2bbf63f3967263ea71e8048173b09ff" +checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36" dependencies = [ + "aead 0.6.0-rc.10", "aes 0.9.0", "aes-gcm 0.11.0-rc.3", - "chacha20 0.10.0", - "cipher 0.5.1", + "cbc", + "chacha20", + "cipher 0.5.2", + "ctr 0.10.1", + "ctutils", "des", - "poly1305 0.9.0-rc.6", - "ssh-encoding 0.3.0-rc.8", + "poly1305", + "ssh-encoding", "zeroize", ] [[package]] name = "ssh-encoding" -version = "0.2.0" +version = "0.3.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +checksum = "7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96a" dependencies = [ "base64ct", "bytes", - "pem-rfc7468 0.7.0", - "sha2 0.10.9", -] - -[[package]] -name = "ssh-encoding" -version = "0.3.0-rc.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af0ddb05d9c6034911bbdc541170b3068a2c019c7a10824e92921151563fb5af" -dependencies = [ - "base64ct", - "digest 0.11.2", + "crypto-bigint 0.7.3", + "ctutils", + "digest 0.11.3", "pem-rfc7468 1.0.0", - "subtle", "zeroize", ] [[package]] name = "ssh-key" -version = "0.7.0-rc.9" +version = "0.7.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae7221717f89c8629a83ba265a004cb864df267485656f790444fff1b69fa36" +checksum = "45735ce3dea95690e4a9e414c4cfde7f79835063c3dcd35881df85a84118e74b" dependencies = [ - "rand_core 0.10.0", + "argon2 0.6.0-rc.8", + "bcrypt-pbkdf", + "ctutils", + "ed25519-dalek 3.0.0-pre.7", + "hex", + "hmac 0.13.0", + "p256 0.14.0-rc.9", + "p384", + "p521", + "rand_core 0.10.1", + "rsa 0.10.0-rc.18", "sec1 0.8.1", + "serde", + "sha1 0.11.0", "sha2 0.11.0", - "signature 3.0.0-rc.10", - "ssh-cipher 0.3.0-rc.8", - "ssh-encoding 0.3.0-rc.8", - "subtle", + "signature 3.0.0", + "ssh-cipher", + "ssh-encoding", "zeroize", ] @@ -9632,36 +9516,15 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys 0.59.0", -] - -[[package]] -name = "static-server" -version = "0.2.9" -dependencies = [ - "actix-cors", - "actix-files", - "actix-web", - "anyhow", - "env_logger", - "futures", - "log", - "metrics-exporter-prometheus 0.13.1", - "mime", - "mime_guess2", - "observability", - "serde", - "serde_json", - "slog", - "tokio", + "windows-sys 0.61.2", ] [[package]] @@ -9670,37 +9533,26 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "storage" +version = "1.0.0" +dependencies = [ + "async-trait", + "aws-config", + "aws-sdk-s3", + "config", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "strength_reduce" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - [[package]] name = "stringprep" version = "0.1.5" @@ -9724,55 +9576,18 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - [[package]] name = "strum" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.117", -] - [[package]] name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "superboring" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d8af9125d1ea290cf5c297b94d0e518c939bbe1f45ef130c19525dae7afba99" -dependencies = [ - "aes-gcm 0.10.3", - "aes-keywrap", - "getrandom 0.2.17", - "hmac-sha256", - "hmac-sha512", - "rand 0.8.5", - "rsa 0.9.10", -] - [[package]] name = "syn" version = "1.0.109" @@ -9816,18 +9631,46 @@ dependencies = [ ] [[package]] -name = "sysinfo" -version = "0.39.1" +name = "syntect" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4deba334e1190ba7cb498327affa11e5ece10d26a30ab2f27fcf09504b8d8b6" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.18", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "objc2-open-directory", - "windows", ] [[package]] @@ -9844,9 +9687,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -9867,14 +9710,13 @@ dependencies = [ ] [[package]] -name = "tendril" -version = "0.4.3" +name = "terminal_size" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ - "futf", - "mac", - "utf-8", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -9926,16 +9768,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "thrift_codec" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d957f535b242b91aa9f47bde08080f9a6fef276477e55b0079979d002759d5" -dependencies = [ - "byteorder", - "trackable", -] - [[package]] name = "tiff" version = "0.11.3" @@ -9950,21 +9782,6 @@ dependencies = [ "zune-jpeg", ] -[[package]] -name = "tiktoken-rs" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4a168cfc1d8ed65bf17a6ee0843ad9a68f863c63c0fb2fa7eab67838782ee" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bstr", - "fancy-regex", - "lazy_static", - "regex", - "rustc-hash 1.1.0", -] - [[package]] name = "time" version = "0.3.47" @@ -9973,7 +9790,6 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "js-sys", "num-conv", "powerfmt", "serde_core", @@ -10008,9 +9824,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -10033,9 +9849,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -10060,12 +9876,12 @@ dependencies = [ ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" +name = "tokio-rustls" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "native-tls", + "rustls 0.21.12", "tokio", ] @@ -10075,7 +9891,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.40", "tokio", ] @@ -10088,18 +9904,6 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-test" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" -dependencies = [ - "futures-core", - "tokio", - "tokio-stream", ] [[package]] @@ -10113,7 +9917,6 @@ dependencies = [ "futures-sink", "futures-util", "pin-project-lite", - "slab", "tokio", ] @@ -10129,11 +9932,11 @@ dependencies = [ "futures-sink", "http 1.4.0", "httparse", - "rand 0.8.5", + "rand 0.8.6", "ring", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", "webpki-roots 0.26.11", ] @@ -10146,9 +9949,9 @@ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -10159,30 +9962,30 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "toml_datetime 0.6.11", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.25.8+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.1.0+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.3", ] [[package]] @@ -10193,15 +9996,15 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.7.9", "base64 0.22.1", "bytes", "flate2", - "h2 0.4.13", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -10211,7 +10014,7 @@ dependencies = [ "rustls-pemfile", "socket2 0.5.10", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-stream", "tower 0.4.13", "tower-layer", @@ -10221,34 +10024,70 @@ dependencies = [ [[package]] name = "tonic" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", + "axum 0.8.9", "base64 0.22.1", "bytes", + "h2 0.4.14", "http 1.4.0", "http-body 1.0.1", "http-body-util", + "hyper 1.9.0", + "hyper-timeout", + "hyper-util", "percent-encoding", "pin-project", + "socket2 0.6.3", "sync_wrapper", + "tokio", "tokio-stream", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", ] [[package]] -name = "tonic-prost" -version = "0.14.5" +name = "tonic-build" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost 0.14.3", - "tonic 0.14.5", + "tonic 0.14.6", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types 0.14.3", + "quote", + "syn 2.0.117", + "tempfile", + "tonic-build", ] [[package]] @@ -10262,7 +10101,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand 0.8.5", + "rand 0.8.6", "slab", "tokio", "tokio-util", @@ -10279,7 +10118,9 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap 2.14.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", "tokio-util", @@ -10290,23 +10131,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "base64 0.22.1", - "bitflags 2.11.0", + "bitflags", "bytes", "futures-util", "http 1.4.0", "http-body 1.0.1", - "iri-string", - "mime", "pin-project-lite", "tower 0.5.3", "tower-layer", "tower-service", - "tracing", + "url", ] [[package]] @@ -10377,22 +10215,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-opentelemetry" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" -dependencies = [ - "js-sys", - "opentelemetry", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - [[package]] name = "tracing-serde" version = "0.2.0" @@ -10424,61 +10246,6 @@ dependencies = [ "tracing-serde", ] -[[package]] -name = "trackable" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" -dependencies = [ - "trackable_derive", -] - -[[package]] -name = "trackable_derive" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "transport" -version = "0.2.9" -dependencies = [ - "actix", - "actix-web", - "actix-ws", - "async-nats", - "base64 0.22.1", - "chrono", - "config", - "dashmap", - "db", - "email", - "futures-util", - "hex", - "hmac 0.13.0", - "models", - "observability", - "queue", - "rand 0.10.1", - "redis", - "room", - "sea-orm", - "serde", - "serde_json", - "service", - "session", - "sha2 0.11.0", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", - "uuid", -] - [[package]] name = "transpose" version = "0.2.3" @@ -10512,22 +10279,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" [[package]] -name = "typed-path" -version = "0.12.3" +name = "typed-arena" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] -name = "ucd-trie" -version = "0.1.7" +name = "uluru" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "7c8a2469e56e6e5095c82ccd3afb98dad95f7af7929aab6d8ba8d6e0f73657da" +dependencies = [ + "arrayvec", +] [[package]] name = "unicase" @@ -10541,6 +10311,12 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -10570,9 +10346,9 @@ checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" -version = "0.2.2" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" @@ -10580,6 +10356,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.5.1" @@ -10596,7 +10378,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.2.1", + "crypto-common 0.2.2", "ctutils", ] @@ -10606,6 +10388,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -10625,10 +10413,10 @@ dependencies = [ ] [[package]] -name = "utf-8" -version = "0.7.6" +name = "urlencoding" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "utf8_iter" @@ -10644,11 +10432,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_json", "utoipa-gen", @@ -10656,9 +10444,9 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" dependencies = [ "proc-macro2", "quote", @@ -10676,6 +10464,7 @@ dependencies = [ "getrandom 0.4.2", "js-sys", "serde_core", + "sha1_smol", "wasm-bindgen", ] @@ -10690,12 +10479,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "v_htmlescape" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" - [[package]] name = "valuable" version = "0.1.1" @@ -10714,6 +10497,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -10741,11 +10530,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -10754,7 +10543,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -10763,20 +10552,11 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" -[[package]] -name = "wasix" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1757e0d1f8456693c7e5c6c629bdb54884e032aa0bb53c155f6a39f94440d332" -dependencies = [ - "wasi", -] - [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -10788,9 +10568,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.65" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -10798,9 +10578,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -10808,9 +10588,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -10821,9 +10601,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -10845,7 +10625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -10882,34 +10662,17 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] -[[package]] -name = "web-push-native" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2175ef28a9a693fa88f322d88484f702a340da864a449a2b6b2a1fe26db712f3" -dependencies = [ - "aes-gcm 0.10.3", - "base64ct", - "ece-native", - "hkdf 0.12.4", - "http 1.4.0", - "jwt-simple", - "p256 0.13.2", - "serde", - "sha2 0.10.9", -] - [[package]] name = "web-sys" -version = "0.3.92" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -10926,15 +10689,12 @@ dependencies = [ ] [[package]] -name = "web_atoms" -version = "0.1.3" +name = "webpki-root-certs" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", + "rustls-pki-types", ] [[package]] @@ -10943,14 +10703,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -10961,15 +10721,6 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" -[[package]] -name = "which" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" -dependencies = [ - "libc", -] - [[package]] name = "whoami" version = "1.6.1" @@ -10980,6 +10731,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + [[package]] name = "wide" version = "0.7.33" @@ -11104,6 +10861,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -11142,11 +10910,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.5", ] [[package]] @@ -11182,13 +10950,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.2.1" @@ -11210,6 +10995,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -11222,6 +11013,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -11234,12 +11031,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -11252,6 +11061,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -11264,6 +11079,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -11276,6 +11097,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -11288,6 +11115,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" @@ -11299,9 +11132,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -11315,6 +11148,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -11334,7 +11173,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -11364,8 +11203,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", - "indexmap 2.13.0", + "bitflags", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -11384,7 +11223,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -11394,15 +11233,11 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "workspace" -version = "0.2.9" - [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -11413,23 +11248,6 @@ dependencies = [ "tap", ] -[[package]] -name = "x509-parser" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom 7.1.3", - "oid-registry", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - [[package]] name = "xattr" version = "1.6.1" @@ -11440,6 +11258,18 @@ dependencies = [ "rustix", ] +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -11452,6 +11282,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "1.0.1" @@ -11470,9 +11309,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -11481,9 +11320,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -11513,18 +11352,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -11540,9 +11379,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -11551,9 +11390,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -11562,59 +11401,15 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "zip" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" -dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "displaydoc", - "flate2", - "indexmap 2.13.0", - "memchr", - "thiserror 2.0.18", - "zopfli", -] - -[[package]] -name = "zip" -version = "8.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9" -dependencies = [ - "aes 0.8.4", - "bzip2", - "constant_time_eq", - "crc32fast", - "deflate64", - "flate2", - "getrandom 0.4.2", - "hmac 0.12.1", - "indexmap 2.13.0", - "lzma-rust2", - "memchr", - "pbkdf2 0.12.2", - "ppmd-rust", - "sha1 0.10.6", - "time", - "typed-path", - "zeroize", - "zopfli", - "zstd", -] - [[package]] name = "zlib-rs" version = "0.6.3" @@ -11627,18 +11422,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index b4628dc..1ce3711 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,192 +1,5 @@ -[workspace] -members = [ - # "libs/frontend", - "libs/models", - "libs/session", - "libs/git", - "libs/email", - "libs/queue", - "libs/room", - "libs/config", - "libs/service", - "libs/db", - "libs/api", - "libs/transport", - "libs/observability", - "libs/avatar", - "libs/agent", - "libs/migrate", - "libs/fctool", - "libs/gingress-proxy", - "apps/migrate", - "apps/app", - "apps/git-hook", - "apps/gitserver", - "apps/email", - "apps/static", - "apps/metrics", - "apps/gingress", -] - -resolver = "3" - -[workspace.dependencies] -models = { path = "libs/models" } -session = { path = "libs/session" } -git = { path = "libs/git" } -email = { path = "libs/email" } -queue = { path = "libs/queue" } -room = { path = "libs/room" } -config = { path = "libs/config" } -service = { path = "libs/service" } -db = { path = "libs/db" } -api = { path = "libs/api" } -agent = { path = "libs/agent" } -observability = { path = "libs/observability" } -avatar = { path = "libs/avatar" } -migrate = { path = "libs/migrate" } -fctool = { path = "libs/fctool" } -transport = { path = "libs/transport" } -metrics-aggregator = { path = "apps/metrics" } -gingress-proxy = { path = "libs/gingress-proxy" } -gingress = { path = "apps/gingress" } - -sea-query = "1.0.0-rc.33" - -actix-web = "4.13.0" -actix-files = "0.6.10" -actix-cors = "0.7.1" -actix-session = "0.11.0" -actix-ws = "0.4.0" -actix-multipart = "0.7.2" -actix-analytics = "1.2.1" -actix-jwt-session = "1.0.7" -actix-csrf = "0.8.0" -metrics = "0.24.5" - -actix-rt = "2.11.0" -actix = "0.13" -async-stream = "0.3" - -actix-service = "2.0.3" -actix-utils = "3.0.1" -redis = "1.1.0" -anyhow = "1.0.102" -derive_more = "2.1.1" -blake3 = "1.8.3" -argon2 = "0.5.3" -thiserror = "2.0.18" -password-hash = "0.6.0" -awc = "3.8.2" -bstr = "1.12.1" -captcha-rs = "0.5.0" -deadpool-redis = "0.23.0" -deadpool = "0.13.0" -dotenv = "0.15.0" -env_logger = "0.11.10" -brotli = "7.0" -flate2 = "1.1.9" -git2 = "0.20.4" -slog = "2.8.2" -git2-ext = "1.0.0" -git2-hooks = "0.7.0" -futures = "0.3.32" -futures-util = "0.3.32" -globset = "0.4.18" -hex = "0.4.3" -lettre = { version = "0.11.19", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder", "pool"] } -mime = "0.3.17" -mime_guess2 = "2.3.1" -opentelemetry = "0.31.0" -opentelemetry-otlp = { version = "0.31.0", features = ["http-proto", "trace"] } -opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio"] } -opentelemetry-http = "0.31.0" -prost = "0.14.3" -prost-build = "0.14.3" -qdrant-client = "1.17.0" -prost-types = "0.14.3" -rand = "0.10.0" -russh = { version = "0.60.2", default-features = false, features = ["ring", "rsa"] } -hmac = { version = "0.13" } -hkdf = "0.13.0" -sha1_smol = "1.0.1" -rsa = { version = "0.9.7", package = "rsa" } -reqwest = { version = "0.13.2", default-features = false } -dotenvy = "0.15.7" -# aws-lc-sys requires NASM on Windows, so we use local filesystem storage instead of S3 -# aws-sdk-s3 = "1.127.0" -sea-orm = "2.0.0-rc.37" -sea-orm-migration = "2.0.0-rc.37" -sha1 = "0.11" -sha2 = "0.11" -sysinfo = "0.39.1" -ssh-key = "0.7.0-rc.9" -tar = "0.4.45" -zip = "8.3.1" -tokenizer = "0.1.2" -tiktoken-rs = "0.11.0" -regex = "1.12.3" -jsonwebtoken = "10.3.0" -once_cell = "1.21.4" -async-trait = "0.1.89" -fs2 = "0.4.3" -image = "0.25.10" -tokio = "1.50.0" -tokio-util = "0.7.18" -tokio-stream = { version = "0.1.18", features = ["sync"] } -url = "2.5.8" -tower = "0.5" -num_cpus = "1.17.0" -ring = "0.17" -rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } -clap = "4.6.0" -time = "0.3.47" -chrono = "0.4.44" -tracing = "0.1.44" -tracing-subscriber = { version = "0.3.23", features = ["env-filter", "json", "tracing-log"] } -tracing-opentelemetry = "0.32.1" -tonic = "0.14.5" -tonic-build = "0.14.5" -uuid = "1.22.0" -hostname = "0.4" -utoipa = { version = "5.4.0", features = ["chrono", "uuid"] } -rust_decimal = "1.40.0" -walkdir = "2.5.0" -calamine = "0.26" -csv = "1.3" -lopdf = "0.34" -pulldown-cmark = "0.12" -quick-xml = { version = "0.37", features = ["serialize"] } -sqlparser = "0.55" -lazy_static = "1.5" -chacha20poly1305 = "0.10" -md5 = "0.7" -moka = "0.12.15" -dashmap = "7.0.0-rc2" -serde = "1.0.228" -serde_json = "1.0.149" -serde_yaml = "0.9.33" -serde_bytes = "0.11.19" -phf = "0.13.1" -phf_codegen = "0.13.1" -base64 = "0.22.1" -base64ct = "1" -p256 = { version = "0.13", features = ["ecdsa", "std"] } -# http version varies per-crate (pingora needs 1.x, actix needs 0.2) -hyper = "0.14" -tempfile = "3" -rig-core = { version = "0.36.0", default-features = false } -tokio-tungstenite = { version = "0.29.0", features = [] } -async-nats = { version = "0.48.0", features = [] } -kube = { version = "3.1.0", features = ["runtime", "derive"] } -k8s-openapi = { version = "0.27", features = ["v1_31"] } -pingora = { version = "0.8", features = ["proxy"] } -pingora-proxy = "0.8" -pingora-load-balancing = "0.8" -pingora-cache = "0.8" -rustls-pemfile = "2" [workspace.package] -version = "0.2.9" +version = "1.0.0" edition = "2024" authors = [] description = "" @@ -198,6 +11,31 @@ keywords = [] categories = [] documentation = "" +[workspace] +members = [ + "app/email", + "app/gitdata", + "app/gitpod", + "app/gitsync", + "lib/ai", + "lib/api", + "lib/cache", + "lib/channel", + "lib/config", + "lib/db", + "lib/email", + "lib/git", + "lib/issues", + "lib/migrate", + "lib/model", + "lib/queue", + "lib/service", + "lib/session", + "lib/storage", + "lib/parsefile" +, "lib/socketio"] +resolver = "3" + [workspace.lints.rust] unsafe_code = "warn" @@ -205,36 +43,105 @@ unsafe_code = "warn" unwrap_used = "warn" expect_used = "warn" -[profile.dev] -debug = 1 -incremental = true -codegen-units = 256 +[workspace.dependencies] +ai = { path = "lib/ai" } +api = { path = "lib/api" } +cache = { path = "lib/cache" } +channel = { path = "lib/channel" } +config = { path = "lib/config" } +db = { path = "lib/db" } +email = { path = "lib/email" } +git = { path = "lib/git" } +issues = { path = "lib/issues" } +migrate = { path = "lib/migrate" } +model = { path = "lib/model" } +queue = { path = "lib/queue" } +service = { path = "lib/service" } +session = { path = "lib/session" } +storage = { path = "lib/storage" } +parsefile = { path = "lib/parsefile"} +socketio = { path = "lib/socketio" } -[profile.release] -lto = "thin" -codegen-units = 1 -strip = true -opt-level = 3 +leptos = "0.8.19" +leptos_actix = "0.8.7" +leptos_meta = "0.8.6" +leptos_router = "0.8.13" +server_fn = { version = "0.8.10", features = ["actix"] } +actix-http = "3.11" +actix-ws = "0.4.0" +urlencoding = "2.1" +serde_urlencoded = "0.7" - -[profile.dev.package.num-bigint-dig] -opt-level = 3 - - -[package] -name = "workspace" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[lib] -path = "lib.rs" -crate-type = ["lib"] +juniper = "0.17.1" +ractor = "0.15.13" +ractor_cluster = "0.15.13" +async-nats = "0.48.0" +petgraph = "0.8.3" +async-openai = "0.40.0" +rig-core = { version = "0.36.0", default-features = false, features = ["derive"] } +schemars = "1.2.1" +tokio-stream = "0.1.18" +duct = "1.1.1" +lettre = "0.11.22" +actix-web = "4" +jsonwebtoken = { version = "10.4.0", features = ["rust_crypto"] } +futures-util = "0.3" +futures = "0.3.32" +moka = "0.12.15" +tokio = "1.52.3" +redis = "1.2.1" +serde_json = "1.0.149" +indexmap = "2.14.0" +sea-orm-migration = "2.0.0-rc.38" +sea-orm = { version = "2.0.0-rc.38", features = ["sqlx-all","runtime-tokio","rust_decimal","uuid","chrono"]} +async-trait = "0.1.89" +aws-config = "1.8.16" +aws-sdk-s3 = "1.132.0" +rust_decimal = "1.42.0" +utoipa = "5.5.0" +dotenvy = "0.15.7" +anyhow = "1.0.102" +derive_more = "2.1.1" +serde = "1.0.228" +serde_yaml = "0.9.33" +comrak = "0.38" +sqlparser = "0.62.0" +qdrant-client = "1.18.0" +tiktoken-rs = "0.11.0" +tracing-subscriber = "0.3.23" +thiserror = "2.0.18" +uuid = "1.23.1" +git2 = "0.21.0" +gix = { version = "0.83.0", features = ["max-performance-safe", "serde", "merge", "blame", "revision", "blob-diff", "worktree-stream", "worktree-archive", "mailmap"] } +gix-archive = "0.32.0" +gix-worktree-stream = "0.32.0" +num_cpus = "1.17.0" +tracing = "0.1.44" +actix-service = "2.0.3" +actix-rt = "2.11.0" +actix-utils = "3.0.1" +toasty = "0.6.1" +chrono = "0.4.44" +argon2 = "0.5.3" +rand = "0.10.1" +rand_core = { version = "0.10.1", features = ["getrandom"] } +totp-rs = "5.7.1" +url = "2.5.7" +sha2 = "0.11.0" +base64 = "0.22" +tonic = "0.14.6" +tonic-build = "0.14.6" +prost = "0.14.3" +tonic-prost = "0.14.6" +dashmap = "6" +sqlx = "0.9.0" +russh = { version = "0.61.1", features = ["legacy-ed25519-pkcs8-parser"] } +hex = "0.4" +async-stream = "0.3" +tokio-util = "0.7" +password-hash = "0.6.1" +deadpool-redis = { version = "0.23", features = ["cluster"] } +reqwest = { version = "0.13", features = ["json", "rustls", "system-proxy"] } +hmac = "0.13" +mcpkit = "0.5" +miette = "7" diff --git a/README.md b/README.md deleted file mode 100644 index 811328a..0000000 --- a/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# React + TypeScript + Vite + shadcn/ui - -This is a template for a new Vite project with React, TypeScript, and shadcn/ui. - -## Adding components - -To add components to your app, run the following command: - -```bash -npx shadcn@latest add button -``` - -This will place the ui components in the `src/components` directory. - -## Using components - -To use the components in your app, import them as follows: - -```tsx -import { Button } from "@/components/ui/button" -``` diff --git a/apps/app/Cargo.toml b/apps/app/Cargo.toml deleted file mode 100644 index a8af107..0000000 --- a/apps/app/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "app" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[dependencies] -tokio = { workspace = true, features = ["full"] } -uuid = { workspace = true } -service = { workspace = true } -observability = { workspace = true } -room = { workspace = true } -sha2 = { workspace = true } -hkdf = { workspace = true } -hmac = { workspace = true } -api = { workspace = true } -session = { workspace = true } -config = { workspace = true } -db = { workspace = true } -migrate = { workspace = true } -actix-web = { workspace = true } -actix-cors = { workspace = true } -futures = { workspace = true } -tracing = { workspace = true } -anyhow = { workspace = true } -clap = { workspace = true } -sea-orm = { workspace = true } -serde_json = { workspace = true } -chrono = { workspace = true } -[lints] -workspace = true diff --git a/apps/app/src/args.rs b/apps/app/src/args.rs deleted file mode 100644 index 3433ce5..0000000 --- a/apps/app/src/args.rs +++ /dev/null @@ -1,12 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug)] -#[command(name = "app")] -#[command(version)] -pub struct ServerArgs { - #[arg(long, short)] - pub bind: Option, - - #[arg(long)] - pub workers: Option, -} diff --git a/apps/app/src/logging.rs b/apps/app/src/logging.rs deleted file mode 100644 index 8ac1f52..0000000 --- a/apps/app/src/logging.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! Structured HTTP request logging middleware using tracing. -//! -//! Logs every incoming request with method, path, status code, -//! response time, client IP, authenticated user ID, and trace_id. - -use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; -use futures::future::{LocalBoxFuture, Ready, ok}; -use session::SessionExt; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Instant; -use uuid::Uuid; - -/// Default log format: `{method} {path} {status} {duration_ms}ms` -pub struct RequestLogger { - trace_id_header: String, -} - -impl RequestLogger { - pub fn new(trace_id_header: String) -> Self { - Self { trace_id_header } - } -} - -impl Transform for RequestLogger -where - S: Service, Error = actix_web::Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Transform = RequestLoggerMiddleware; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ok(RequestLoggerMiddleware { - service: Arc::new(service), - trace_id_header: self.trace_id_header.clone(), - }) - } -} - -pub struct RequestLoggerMiddleware { - service: Arc, - trace_id_header: String, -} - -impl Clone for RequestLoggerMiddleware { - fn clone(&self) -> Self { - Self { - service: self.service.clone(), - trace_id_header: self.trace_id_header.clone(), - } - } -} - -impl Service for RequestLoggerMiddleware -where - S: Service, Error = actix_web::Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - let started = Instant::now(); - let trace_id_header = self.trace_id_header.clone(); - let method = req.method().to_string(); - let path = req.path().to_string(); - let query = req.query_string().to_string(); - let remote = req - .connection_info() - .realip_remote_addr() - .map(|s| s.to_string()) - .unwrap_or_else(|| "unknown".to_string()); - let user_id: Option = req.get_session().user(); - let trace_id = Uuid::now_v7().to_string(); - - let full_path = if query.is_empty() { - path.clone() - } else { - format!("{}?{}", path, query) - }; - - let service = self.service.clone(); - - Box::pin(async move { - let res = service.call(req).await?; - let elapsed = started.elapsed(); - let status = res.status(); - let status_code = status.as_u16(); - let is_health = path == "/health"; - - if !is_health { - let user_id_str = user_id - .map(|u: Uuid| u.to_string()) - .unwrap_or_else(|| "-".to_string()); - let duration_ms = elapsed.as_millis() as u64; - - let log_args = ( - method = %method, - path = %full_path, - status = status_code, - duration_ms = duration_ms, - remote = %remote, - user_id = %user_id_str, - trace_id = %trace_id, - ); - - match status_code { - 200..=299 => { - tracing::info!(log_args, "http_request"); - } - 400..=499 => { - tracing::warn!(log_args, "http_request"); - } - _ => { - tracing::error!(log_args, "http_request"); - } - } - } - Ok(res) - }) - } -} diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs deleted file mode 100644 index 469b6f1..0000000 --- a/apps/app/src/main.rs +++ /dev/null @@ -1,350 +0,0 @@ -use actix_cors::Cors; -use actix_web::cookie::time::Duration; -use actix_web::dev::{Service, ServiceRequest, ServiceResponse}; -use actix_web::{App, HttpResponse, HttpServer, cookie::Key, web}; -use api::{robots, sidemap}; -use clap::Parser; -use db::cache::AppCache; -use db::database::AppDatabase; -use futures::future::LocalBoxFuture; -use observability::{ - HttpMetrics, HttpSnapshotGuard, MetricsMiddleware, TracingSpanMiddleware, - init_tracing_subscriber, install_recorder, prometheus_handler, push::MetricsPusher, - spawn_http_metrics_poller, -}; -use sea_orm::ConnectionTrait; -use service::AppService; -use session::SessionMiddleware; -use session::config::{PersistentSession, SessionLifecycle, TtlExtensionPolicy}; -use session::storage::RedisClusterSessionStore; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Instant; - -mod args; - -use args::ServerArgs; -use config::AppConfig; -use migrate::{Migrator, MigratorTrait}; - -#[derive(Clone)] -pub struct AppState { - pub db: AppDatabase, - pub cache: AppCache, -} - -/// Custom middleware that logs requests except for noisy paths. -struct RequestLogger; - -impl actix_web::dev::Transform for RequestLogger -where - S: Service, Error = actix_web::Error>, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Transform = RequestLoggerService; - type InitError = (); - type Future = futures::future::Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - futures::future::ok(RequestLoggerService { - service, - _marker: std::marker::PhantomData, - }) - } -} - -struct RequestLoggerService { - service: S, - _marker: std::marker::PhantomData, -} - -impl actix_web::dev::Service for RequestLoggerService -where - S: actix_web::dev::Service< - ServiceRequest, - Response = ServiceResponse, - Error = actix_web::Error, - >, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - let path = req.path().to_string(); - let method = req.method().to_string(); - let should_log = !(path == "/health" - || path == "/metrics" - || path.starts_with("/ws") - || path.starts_with("/assets")); - - let start = Instant::now(); - let fut = self.service.call(req); - - Box::pin(async move { - let res = fut.await?; - if should_log { - tracing::info!( - target: "http_request", - method = %method, - path = %path, - status = res.status().as_u16(), - elapsed = ?start.elapsed(), - "{} {} {} {:?}", - method, - path, - res.status().as_u16(), - start.elapsed() - ); - } - Ok(res) - }) - } -} - -fn build_session_key(cfg: &AppConfig) -> anyhow::Result { - if let Some(secret) = cfg.env.get("APP_SESSION_SECRET") { - if secret.len() < 32 { - tracing::warn!( - secret_len = secret.len(), - "APP_SESSION_SECRET is too short (<32 bytes), using generated key instead" - ); - return Ok(Key::generate()); - } - use hkdf::Hkdf; - use sha2::Sha256; - // HKDF-SHA256: standard key derivation with info string for domain separation - let hk = Hkdf::::new(Some(b"session-cookie-key"), secret.as_bytes()); - let mut okm = [0u8; 64]; - hk.expand(b"actix-session-signing-key", &mut okm) - .map_err(|e| anyhow::anyhow!("HKDF expand failed: {}", e))?; - return Ok(Key::from(&okm)); - } - tracing::warn!( - "APP_SESSION_SECRET not set, using generated key (sessions invalidated on restart)" - ); - Ok(Key::generate()) -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let cfg = AppConfig::load(); - let log_level = cfg.log_level().unwrap_or_else(|_| "info".to_string()); - let otel_enabled = cfg.otel_enabled().unwrap_or(false); - init_tracing_subscriber(&log_level, false); - tracing::info!( - app_name = %cfg.app_name().unwrap_or_default(), - app_version = %cfg.app_version().unwrap_or_default(), - "Starting application" - ); - let db = AppDatabase::init(&cfg).await?; - tracing::info!("Database connected"); - let redis_urls = cfg.redis_urls()?; - let store: RedisClusterSessionStore = RedisClusterSessionStore::new(redis_urls).await?; - tracing::info!("Redis connected"); - let cache = AppCache::init(&cfg).await?; - tracing::info!("Cache initialized"); - run_migrations(&db).await?; - let session_key = build_session_key(&cfg)?; - let args = ServerArgs::parse(); - let service = AppService::new(cfg.clone()).await?; - tracing::info!("AppService initialized"); - let _model_sync_handle = service.clone().start_sync_task(); - // TODO: workspace module not yet wired — billing alert task pending - // let _billing_alert_handle = service.clone().start_billing_alert_task(); - - let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1); - let worker_service = service.clone(); - let worker_handle = - tokio::spawn(async move { worker_service.start_room_workers(shutdown_rx).await }); - - let _otel_guard = if otel_enabled { - let endpoint = cfg - .otel_endpoint() - .unwrap_or_else(|_| "http://localhost:4317".to_string()); - let service_name = cfg - .otel_service_name() - .unwrap_or_else(|_| "app".to_string()); - let service_version = cfg - .otel_service_version() - .unwrap_or_else(|_| "0.1.0".to_string()); - tracing::info!(endpoint = %endpoint, service = %service_name, "OTLP tracing enabled"); - let guard = - observability::init_otlp(&endpoint, &service_name, &service_version, &log_level) - .map_err(|e| anyhow::anyhow!("OTLP init failed: {}", e))?; - guard - } else { - None - }; - - let prometheus_handle = install_recorder(); - let prometheus_handle_data = web::Data::new(prometheus_handle.clone()); - - let http_metrics = std::sync::Arc::new(HttpMetrics::new()); - let http_snapshot: HttpSnapshotGuard = std::sync::Arc::new(std::sync::RwLock::new( - observability::HttpMetricsSnapshot::default(), - )); - let http_snapshot_for_poller = http_snapshot.clone(); - spawn_http_metrics_poller( - http_metrics.clone(), - http_snapshot_for_poller, - std::time::Duration::from_secs(15), - ); - let http_snapshot_data = web::Data::new(http_snapshot); - - // Metrics pusher: periodically push all metrics to apps/metrics aggregator - if let Some(push_url) = std::env::var("METRICS_PUSH_URL").ok() { - let pusher = MetricsPusher::new(&push_url, "app"); - pusher.spawn( - http_metrics.clone(), - Arc::new(prometheus_handle.clone()), - std::time::Duration::from_secs(15), - ); - tracing::info!(push_url = %push_url, "Metrics pusher started (interval 15s)"); - } - - let bind_addr = args.bind.unwrap_or_else(|| "127.0.0.1:8080".to_string()); - tracing::info!(bind_addr = %bind_addr, "Listening"); - let http_metrics_server = http_metrics.clone(); - let cors_origins: Vec = cfg - .env - .get("CORS_ORIGINS") - .map(|s| { - s.split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() - }) - .unwrap_or_else(|| vec!["http://localhost:5173".to_string()]); - let cookie_secure = cfg - .env - .get("APP_COOKIE_SECURE") - .map(|s| s != "false") - .unwrap_or(true); - tracing::info!(cookie_secure = cookie_secure, "Cookie secure mode"); - HttpServer::new(move || { - let mut cors = Cors::default(); - for origin in &cors_origins { - cors = cors.allowed_origin(origin); - } - let cors = cors - .allowed_methods(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) - .allowed_headers([ - "Content-Type", - "Authorization", - "X-Requested-With", - "Accept", - "Origin", - ]) - .supports_credentials() - .max_age(3600); - - let security_headers = actix_web::middleware::DefaultHeaders::new() - .add(("X-Content-Type-Options", "nosniff")) - .add(("X-Frame-Options", "DENY")) - .add(("Referrer-Policy", "strict-origin-when-cross-origin")); - - let session_mw = SessionMiddleware::builder(store.clone(), session_key.clone()) - .cookie_name("id".to_string()) - .cookie_path("/".to_string()) - .cookie_secure(cookie_secure) - .cookie_http_only(true) - .session_lifecycle(SessionLifecycle::PersistentSession( - PersistentSession::default() - .session_ttl(Duration::days(30)) - .session_ttl_extension_policy(TtlExtensionPolicy::OnEveryRequest), - )) - .build(); - - let metrics_mw = MetricsMiddleware::new(http_metrics_server.clone()); - - App::new() - .wrap(cors) - .wrap(security_headers) - .wrap(session_mw) - .wrap(RequestLogger) - .wrap(metrics_mw) - .wrap(TracingSpanMiddleware::new()) - .app_data(web::Data::new(AppState { - db: db.clone(), - cache: cache.clone(), - })) - .app_data(web::Data::new(service.clone())) - .app_data(web::Data::new(cfg.clone())) - .app_data(web::Data::new(db.clone())) - .app_data(web::Data::new(cache.clone())) - .app_data(http_snapshot_data.clone()) - .app_data(prometheus_handle_data.clone()) - .route("/robots.txt", web::get().to(robots::robots)) - .route("/sitemap.xml", web::get().to(sidemap::sitemap)) - .service( - web::scope("/sidemap") - .route("", web::get().to(sidemap::sitemap)) - .route("/static", web::get().to(sidemap::sitemap_static)) - .route("/users", web::get().to(sidemap::sitemap_users)) - .route("/projects", web::get().to(sidemap::sitemap_projects)) - .route("/repos", web::get().to(sidemap::sitemap_repos)), - ) - .route("/health", web::get().to(health_check)) - .route("/metrics", web::get().to(prometheus_handler)) - .configure(api::route::init_routes) - }) - .bind(&bind_addr)? - .run() - .await?; - - tracing::info!("Server stopped, shutting down room workers"); - let _ = shutdown_tx.send(()); - let _ = worker_handle.await; - tracing::info!("Room workers stopped"); - - Ok(()) -} - -async fn run_migrations(db: &AppDatabase) -> anyhow::Result<()> { - tracing::info!("Running database migrations..."); - Migrator::up(db.writer(), None) - .await - .map_err(|e| anyhow::anyhow!("Migration failed: {:?}", e))?; - tracing::info!("Migrations completed"); - Ok(()) -} - -async fn health_check(state: web::Data) -> HttpResponse { - let db_ok = db_ping(&state.db).await; - let cache_ok = cache_ping(&state.cache).await; - - let healthy = db_ok && cache_ok; - if healthy { - HttpResponse::Ok().json(serde_json::json!({ - "status": "ok", - "db": "ok", - "cache": "ok", - })) - } else { - HttpResponse::ServiceUnavailable().json(serde_json::json!({ - "status": "unhealthy", - "db": if db_ok { "ok" } else { "error" }, - "cache": if cache_ok { "ok" } else { "error" }, - })) - } -} - -async fn db_ping(db: &AppDatabase) -> bool { - let writer_ok = db.writer().execute_unprepared("SELECT 1").await.is_ok(); - let reader_ok = db.reader().execute_unprepared("SELECT 1").await.is_ok(); - writer_ok && reader_ok -} - -async fn cache_ping(cache: &AppCache) -> bool { - cache.conn().await.is_ok() -} diff --git a/apps/email/Cargo.toml b/apps/email/Cargo.toml deleted file mode 100644 index 900e792..0000000 --- a/apps/email/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "email-server" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[[bin]] -name = "email-worker" -path = "src/main.rs" - -[dependencies] -tokio = { workspace = true, features = ["full"] } -service = { workspace = true } -db = { workspace = true } -config = { workspace = true } -tracing = { workspace = true } -observability = { workspace = true } -anyhow = { workspace = true } -clap = { workspace = true, features = ["derive"] } -chrono = { workspace = true, features = ["serde"] } -hyper = { workspace = true } -serde_json = { workspace = true } -sea-orm = { workspace = true } -metrics = "0.22" -metrics-exporter-prometheus = "0.13" - -[lints] -workspace = true diff --git a/apps/email/src/main.rs b/apps/email/src/main.rs deleted file mode 100644 index 6859014..0000000 --- a/apps/email/src/main.rs +++ /dev/null @@ -1,165 +0,0 @@ -use clap::Parser; -use config::AppConfig; -use metrics::{Unit, describe_counter}; -use metrics_exporter_prometheus::PrometheusHandle; -use observability::{HttpMetrics, init_tracing_subscriber, install_recorder, push::MetricsPusher}; -use sea_orm::ConnectionTrait; -use service::AppService; -use std::sync::Arc; - -#[derive(Parser, Debug)] -#[command(name = "email-worker")] -#[command(version)] -struct Args { - #[arg(long, default_value = "info")] - log_level: String, -} - -async fn http_handler( - db: Arc, - cache: Arc, - metrics: Arc, - req: hyper::Request, -) -> Result, std::convert::Infallible> { - match req.uri().path() { - "/health" => { - let writer_ok = db.writer().execute_unprepared("SELECT 1").await.is_ok(); - let reader_ok = db.reader().execute_unprepared("SELECT 1").await.is_ok(); - let db_ok = writer_ok && reader_ok; - let cache_ok = cache.conn().await.is_ok(); - - let body = serde_json::json!({ - "status": if db_ok && cache_ok { "ok" } else { "unhealthy" }, - "db": if db_ok { "ok" } else { "error" }, - "cache": if cache_ok { "ok" } else { "error" }, - }); - - let status = if db_ok && cache_ok { 200 } else { 503 }; - let body_bytes = match serde_json::to_string(&body) { - Ok(s) => hyper::Body::from(s), - Err(e) => { - return Ok(hyper::Response::builder() - .status(500) - .body(hyper::Body::from(format!("serialize error: {}", e))) - .expect("static response")); - } - }; - Ok(hyper::Response::builder() - .status(status) - .header("content-type", "application/json") - .body(body_bytes) - .expect("static response")) - } - "/metrics" => { - let body = metrics.render(); - Ok(hyper::Response::builder() - .status(200) - .header("content-type", "text/plain; version=0.0.4; charset=utf-8") - .body(hyper::Body::from(body)) - .unwrap()) - } - _ => Ok(hyper::Response::builder() - .status(404) - .body(hyper::Body::from("not found")) - .unwrap()), - } -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let args = Args::parse(); - let cfg = AppConfig::load(); - init_tracing_subscriber(&args.log_level, false); - - // Pre-register all email/queue metrics so they appear in /metrics even before first event. - describe_counter!( - "email_queued_total", - Unit::Count, - "Emails written to Redis stream" - ); - describe_counter!( - "email_consumed_total", - Unit::Count, - "Emails consumed from queue" - ); - describe_counter!( - "email_batch_size", - Unit::Count, - "Email consumer batch sizes accumulated" - ); - describe_counter!( - "email_validation_skipped_total", - Unit::Count, - "Emails skipped due to invalid recipient" - ); - describe_counter!( - "email_build_errors_total", - Unit::Count, - "Email message build failures" - ); - describe_counter!( - "email_send_attempts_total", - Unit::Count, - "SMTP send attempts (including retries)" - ); - describe_counter!("email_sent_total", Unit::Count, "Emails sent successfully"); - describe_counter!( - "email_send_failures_total", - Unit::Count, - "Emails that failed after all retries" - ); - - let metrics_handle = Arc::new(install_recorder()); - let http_metrics = Arc::new(HttpMetrics::new()); // Worker app — HTTP section will be empty - - // Metrics pusher: periodically push all metrics to apps/metrics aggregator - if let Some(push_url) = std::env::var("METRICS_PUSH_URL").ok() { - let pusher = MetricsPusher::new(&push_url, "email"); - pusher.spawn( - http_metrics.clone(), - metrics_handle.clone(), - std::time::Duration::from_secs(15), - ); - tracing::info!(push_url = %push_url, "Metrics pusher started (interval 15s)"); - } - - tracing::info!("Starting email worker"); - let service = AppService::new(cfg).await?; - - let db = Arc::new(service.db.clone()); - let cache = Arc::new(service.cache.clone()); - - let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1); - tokio::spawn(async move { - tokio::signal::ctrl_c().await.ok(); - tracing::info!("shutting down email worker"); - let _ = shutdown_tx.send(()); - }); - - // Start health/metrics server on a dedicated port - let health_db = db.clone(); - let health_cache = cache.clone(); - let health_metrics = metrics_handle.clone(); - let health_addr: std::net::SocketAddr = ([0, 0, 0, 0], 8084).into(); - let health_service = hyper::service::make_service_fn(move |_| { - let db = health_db.clone(); - let cache = health_cache.clone(); - let metrics = health_metrics.clone(); - let service = hyper::service::service_fn(move |req| { - http_handler(db.clone(), cache.clone(), metrics.clone(), req) - }); - async move { Ok::<_, std::convert::Infallible>(service) } - }); - - let health_server = hyper::Server::bind(&health_addr).serve(health_service); - tracing::info!(port = 8084, "health/metrics server started"); - tokio::spawn(async move { - if let Err(e) = health_server.await { - tracing::error!("health check server error: {}", e); - } - }); - - service.start_email_workers(shutdown_rx).await?; - tracing::info!("email worker stopped"); - Ok(()) -} diff --git a/apps/git-hook/Cargo.toml b/apps/git-hook/Cargo.toml deleted file mode 100644 index e410a0e..0000000 --- a/apps/git-hook/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "git-hook" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[dependencies] -tokio = { workspace = true, features = ["full"] } -git = { workspace = true } -observability = { workspace = true } -db = { workspace = true } -config = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["json"] } -anyhow = { workspace = true } -clap = { workspace = true, features = ["derive"] } -tokio-util = { workspace = true } -hyper = { workspace = true } -serde_json = { workspace = true } -sea-orm = { workspace = true } -metrics = "0.22" -metrics-exporter-prometheus = "0.13" -chrono = { workspace = true, features = ["serde"] } -reqwest = { workspace = true } -agent = { workspace = true } -models = { workspace = true } -async-trait = { workspace = true } diff --git a/apps/git-hook/src/args.rs b/apps/git-hook/src/args.rs deleted file mode 100644 index d6dd9b9..0000000 --- a/apps/git-hook/src/args.rs +++ /dev/null @@ -1,10 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug)] -#[command(name = "git-hook")] -#[command(version)] -pub struct HookArgs { - /// Worker ID for this instance. Defaults to the HOOK_POOL_WORKER_ID env var or a generated UUID. - #[arg(long)] - pub worker_id: Option, -} diff --git a/apps/git-hook/src/main.rs b/apps/git-hook/src/main.rs deleted file mode 100644 index 29fc5b6..0000000 --- a/apps/git-hook/src/main.rs +++ /dev/null @@ -1,256 +0,0 @@ -use clap::Parser; -use config::AppConfig; -use db::cache::AppCache; -use db::database::AppDatabase; -use git::hook::HookService; -use git::hook::embed::TagEmbedder; -use metrics::{Unit, describe_counter}; -use metrics_exporter_prometheus::PrometheusHandle; -use observability::{HttpMetrics, init_tracing_subscriber, install_recorder, push::MetricsPusher}; -use sea_orm::ConnectionTrait; -use std::sync::Arc; -use tokio::signal; - -mod args; - -use args::HookArgs; - -/// Initialize EmbedService from config (graceful degradation). -async fn init_embed_service( - cfg: &AppConfig, - db: &AppDatabase, -) -> Result> { - let client = agent::new_embed_client(cfg).await?; - let model_name = cfg - .get_embed_model_name() - .unwrap_or_else(|_| "text-embedding-3-small".into()); - let dimensions = cfg.get_embed_model_dimensions().unwrap_or(1536); - let svc = agent::embed::EmbedService::new(client, db.writer().clone(), model_name, dimensions); - let _ = svc.ensure_collections().await; - tracing::info!("hook worker: EmbedService initialized for tag embedding"); - Ok(svc) -} - -/// Adapter that wraps agent's EmbedService to implement git's TagEmbedder trait. -struct EmbedServiceAdapter(agent::embed::EmbedService); - -#[async_trait::async_trait] -impl TagEmbedder for EmbedServiceAdapter { - async fn embed_tags_batch( - &self, - tags: Vec, - ) -> Result<(), Box> { - // Convert from models::TagEmbedInput to agent's TagEmbedInput (same struct, different path) - let agent_tags: Vec = tags - .into_iter() - .map(|t| agent::embed::TagEmbedInput { - repo_id: t.repo_id, - repo_name: t.repo_name, - project_id: t.project_id, - name: t.name, - description: t.description, - }) - .collect(); - self.0 - .embed_tags_batch(agent_tags) - .await - .map_err(|e| Box::new(e) as Box) - } -} - -async fn http_handler( - db: Arc, - cache: Arc, - metrics: Arc, - req: hyper::Request, -) -> Result, std::convert::Infallible> { - match req.uri().path() { - "/health" => { - let writer_ok = db.writer().execute_unprepared("SELECT 1").await.is_ok(); - let reader_ok = db.reader().execute_unprepared("SELECT 1").await.is_ok(); - let db_ok = writer_ok && reader_ok; - let cache_ok = cache.conn().await.is_ok(); - - let body = serde_json::json!({ - "status": if db_ok && cache_ok { "ok" } else { "unhealthy" }, - "db": if db_ok { "ok" } else { "error" }, - "cache": if cache_ok { "ok" } else { "error" }, - }); - - let status = if db_ok && cache_ok { 200 } else { 503 }; - let body_bytes = match serde_json::to_string(&body) { - Ok(s) => hyper::Body::from(s), - Err(e) => { - return Ok(hyper::Response::builder() - .status(500) - .body(hyper::Body::from(format!("serialize error: {}", e))) - .expect("static response")); - } - }; - Ok(hyper::Response::builder() - .status(status) - .header("content-type", "application/json") - .body(body_bytes) - .expect("static response")) - } - "/metrics" => { - let body = metrics.render(); - Ok(hyper::Response::builder() - .status(200) - .header("content-type", "text/plain; version=0.0.4; charset=utf-8") - .body(hyper::Body::from(body)) - .expect("static response")) - } - _ => Ok(hyper::Response::builder() - .status(404) - .body(hyper::Body::from("not found")) - .expect("static response")), - } -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let cfg = AppConfig::load(); - - let log_level = cfg.log_level().unwrap_or_else(|_| "info".to_string()); - init_tracing_subscriber(&log_level, false); - - // Pre-register all hook metrics so they appear in /metrics even before first increment. - describe_counter!("hook_tasks_total", Unit::Count, "Total hook tasks dequeued"); - describe_counter!( - "hook_tasks_success_total", - Unit::Count, - "Hook tasks completed successfully" - ); - describe_counter!( - "hook_tasks_failed_total", - Unit::Count, - "Hook tasks that failed" - ); - describe_counter!( - "hook_tasks_locked_total", - Unit::Count, - "Hook tasks re-queued due to repo lock" - ); - describe_counter!( - "hook_tasks_retried_total", - Unit::Count, - "Hook tasks that entered retry" - ); - describe_counter!( - "hook_tasks_exhausted_total", - Unit::Count, - "Hook tasks that exhausted retries" - ); - describe_counter!( - "hook_sync_branches_changed_total", - Unit::Count, - "Branches changed during sync" - ); - describe_counter!( - "hook_sync_tags_changed_total", - Unit::Count, - "Tags changed during sync" - ); - - let metrics_handle = Arc::new(install_recorder()); - let http_metrics = Arc::new(HttpMetrics::new()); // Worker app — HTTP section will be empty - - // Metrics pusher: periodically push all metrics to apps/metrics aggregator - if let Some(push_url) = std::env::var("METRICS_PUSH_URL").ok() { - let pusher = MetricsPusher::new(&push_url, "git-hook"); - pusher.spawn( - http_metrics.clone(), - metrics_handle.clone(), - std::time::Duration::from_secs(15), - ); - tracing::info!(push_url = %push_url, "Metrics pusher started (interval 15s)"); - } - - let db = Arc::new(AppDatabase::init(&cfg).await?); - tracing::info!("database connected"); - - // 4. Connect to Redis cache (also provides the cluster pool for hook queue) - let cache = Arc::new(AppCache::init(&cfg).await?); - tracing::info!("cache connected"); - - // 5. Parse CLI args - let _args = HookArgs::parse(); - - tracing::info!("git-hook worker starting"); - - // 6. Build and start git hook service - let mut hooks = HookService::new( - (*db).clone(), - (*cache).clone(), - cache.redis_pool().clone(), - cfg.clone(), - ); - - // Optionally initialize tag embedding - if let Ok(embed_svc) = init_embed_service(&cfg, &db).await { - let adapter = EmbedServiceAdapter(embed_svc); - hooks = hooks.with_tag_embedder(Arc::new(adapter)); - } - - let cancel = hooks.start_worker().await; - let cancel_signal = cancel.clone(); - - // 7. Start health/metrics server on a dedicated port - let health_db = db.clone(); - let health_cache = cache.clone(); - let health_metrics = metrics_handle.clone(); - let health_addr: std::net::SocketAddr = ([0, 0, 0, 0], 8083).into(); - let health_service = hyper::service::make_service_fn(move |_| { - let db = health_db.clone(); - let cache = health_cache.clone(); - let metrics = health_metrics.clone(); - let service = hyper::service::service_fn(move |req| { - http_handler(db.clone(), cache.clone(), metrics.clone(), req) - }); - async move { Ok::<_, std::convert::Infallible>(service) } - }); - - let health_server = hyper::Server::bind(&health_addr).serve(health_service); - tracing::info!(port = 8083, "health/metrics server started"); - tokio::spawn(async move { - if let Err(e) = health_server.await { - tracing::error!("health check server error: {}", e); - } - }); - - // Spawn signal handler that cancels on SIGINT/SIGTERM - tokio::spawn(async move { - let ctrl_c = async { - signal::ctrl_c() - .await - .expect("failed to install CTRL+C handler"); - }; - - #[cfg(unix)] - let term = async { - use tokio::signal::unix::{SignalKind, signal}; - let mut sig = - signal(SignalKind::terminate()).expect("failed to install SIGTERM handler"); - sig.recv().await; - }; - - #[cfg(not(unix))] - let term = std::future::pending::<()>(); - - tokio::select! { - _ = ctrl_c => { - tracing::info!("received SIGINT, initiating shutdown"); - } - _ = term => { - tracing::info!("received SIGTERM, initiating shutdown"); - } - } - cancel_signal.cancel(); - }); - - // Wait until the worker is cancelled (by signal handler or otherwise) - cancel.cancelled().await; - tracing::info!("git-hook worker stopped"); - Ok(()) -} diff --git a/apps/gitserver/Cargo.toml b/apps/gitserver/Cargo.toml deleted file mode 100644 index df2c734..0000000 --- a/apps/gitserver/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "gitserver" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[[bin]] -name = "gitserver" -path = "src/main.rs" - -[dependencies] -tokio = { workspace = true, features = ["full"] } -git = { workspace = true } -observability = { workspace = true } -tracing = { workspace = true } -db = { workspace = true } -config = { workspace = true } -anyhow = { workspace = true } -clap = { workspace = true, features = ["derive"] } -chrono = { workspace = true, features = ["serde"] } - -[lints] -workspace = true diff --git a/apps/gitserver/src/main.rs b/apps/gitserver/src/main.rs deleted file mode 100644 index 89e75c6..0000000 --- a/apps/gitserver/src/main.rs +++ /dev/null @@ -1,59 +0,0 @@ -use clap::Parser; -use config::AppConfig; -use observability::{HttpMetrics, init_tracing_subscriber, install_recorder, push::MetricsPusher}; -use std::sync::Arc; - -#[derive(Parser, Debug)] -#[command(name = "gitserver")] -#[command(version)] -struct Args { - #[arg(long, default_value = "info")] - log_level: String, -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let args = Args::parse(); - let cfg = AppConfig::load(); - init_tracing_subscriber(&args.log_level, false); - - let prometheus_handle = Arc::new(install_recorder()); - let http_metrics = Arc::new(HttpMetrics::new()); - - // Metrics pusher: periodically push all metrics to apps/metrics aggregator - if let Some(push_url) = std::env::var("METRICS_PUSH_URL").ok() { - let pusher = MetricsPusher::new(&push_url, "gitserver"); - pusher.spawn( - http_metrics.clone(), - prometheus_handle.clone(), - std::time::Duration::from_secs(15), - ); - tracing::info!(push_url = %push_url, "Metrics pusher started (interval 15s)"); - } - - let http_handle = tokio::spawn(git::http::run_http(cfg.clone())); - let ssh_handle = tokio::spawn(git::ssh::run_ssh(cfg)); - - tokio::select! { - result = http_handle => { - match result { - Ok(Ok(())) => tracing::info!("HTTP server stopped"), - Ok(Err(e)) => tracing::error!("HTTP server error: {}", e), - Err(e) => tracing::error!("HTTP server task panicked: {}", e), - } - } - result = ssh_handle => { - match result { - Ok(Ok(())) => tracing::info!("SSH server stopped"), - Ok(Err(e)) => tracing::error!("SSH server error: {}", e), - Err(e) => tracing::error!("SSH server task panicked: {}", e), - } - } - _ = tokio::signal::ctrl_c() => { - tracing::info!("received shutdown signal"); - } - } - - tracing::info!("shutting down"); - Ok(()) -} diff --git a/apps/metrics/Cargo.toml b/apps/metrics/Cargo.toml deleted file mode 100644 index 214e441..0000000 --- a/apps/metrics/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "metrics-aggregator" -version.workspace = true -edition.workspace = true -authors.workspace = true -description = "Unified observability aggregator: scrapes metrics, forwards traces, collects logs" -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[[bin]] -name = "metrics-aggregator" -path = "src/main.rs" - -[dependencies] -tokio = { workspace = true, features = ["full"] } -config = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } -observability = { workspace = true } -anyhow = { workspace = true } -clap = { workspace = true, features = ["derive", "env"] } -serde_json = { workspace = true } -chrono = { workspace = true, features = ["serde"] } -serde = { workspace = true, features = ["derive"] } - -# HTTP server -actix-web = "4.13.0" -actix-rt = "2.11.0" - -# HTTP client for scraping (uses awc = actix-web client, no extra TLS deps) -awc = { workspace = true } - -# HTTP client for Loki (reqwest is Send+Sync, unlike awc::Client) -reqwest = { workspace = true, features = ["json"] } - -# Metrics -metrics = { workspace = true } -metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener", "tokio"] } - -# Observability -opentelemetry = { workspace = true } -opentelemetry_sdk = { workspace = true } -opentelemetry-otlp = { version = "0.31.0", default-features = false, features = ["http-proto", "tokio", "trace", "tonic"] } -tracing-opentelemetry = "0.32.1" - -tokio-util = { workspace = true } -tokio-stream = { workspace = true } -futures = { workspace = true } -url = { workspace = true } -tower = { workspace = true } - -[lints] -workspace = true \ No newline at end of file diff --git a/apps/metrics/src/args.rs b/apps/metrics/src/args.rs deleted file mode 100644 index 4e8e4b3..0000000 --- a/apps/metrics/src/args.rs +++ /dev/null @@ -1,35 +0,0 @@ -use clap::Parser; - -#[derive(Parser, Debug)] -#[command(name = "metrics-aggregator")] -#[command(version)] -pub struct Args { - #[arg(long, default_value = "9090", env = "METRICS_AGGREGATOR_PORT")] - pub port: u16, - - #[arg(long, env = "OTEL_EXPORTER_OTLP_ENDPOINT")] - pub otel_endpoint: Option, - - #[arg(long, env = "LOKI_URL")] - pub loki_url: Option, - - #[arg(long, default_value = "15", env = "SCRAPE_INTERVAL_SECS")] - pub scrape_interval_secs: u64, - - /// JSON file with scrape targets. - #[arg(long, env = "SCRAPE_TARGETS_FILE")] - pub targets_file: Option, - - #[arg(long, default_value = "info", env = "LOG_LEVEL")] - pub log_level: String, - - /// Comma-separated list of app names to scrape. - #[arg(long, env = "SCRAPE_APPS")] - pub scrape_apps: Option, - - #[arg(long)] - pub no_otel: bool, - - #[arg(long)] - pub no_loki: bool, -} diff --git a/apps/metrics/src/hotreload.rs b/apps/metrics/src/hotreload.rs deleted file mode 100644 index b19b1ee..0000000 --- a/apps/metrics/src/hotreload.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::sync::Arc; -use tokio::sync::RwLock; - -use crate::target::{ScrapeTarget, load_targets_from_file}; - -pub async fn watch_targets_file( - path: String, - targets: Arc>>, - mut shutdown: tokio::sync::broadcast::Receiver<()>, -) { - let mtime_path = path; - let mut last_mtime: Option = None; - - loop { - tokio::select! { - _ = shutdown.recv() => break, - _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => { - let metadata = match tokio::fs::metadata(&mtime_path).await { - Ok(m) => m, - Err(_) => continue, - }; - - let current_mtime = metadata.modified().ok(); - if current_mtime != last_mtime { - last_mtime = current_mtime; - match load_targets_from_file(&mtime_path).await { - Ok(new_targets) => { - let mut guard = targets.write().await; - *guard = new_targets; - tracing::info!(path = %mtime_path, "targets file reloaded"); - } - Err(e) => { - tracing::warn!(error = %e, "failed to reload targets file"); - } - } - } - } - } - } -} diff --git a/apps/metrics/src/k8s_discovery.rs b/apps/metrics/src/k8s_discovery.rs deleted file mode 100644 index 1e3f403..0000000 --- a/apps/metrics/src/k8s_discovery.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::time::Duration; - -use awc::Client; - -use crate::target::ScrapeTarget; - -pub async fn k8s_pod_discovery() -> Option> { - let pod_namespace = std::env::var("POD_NAMESPACE").ok()?; - let token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"; - let token = tokio::fs::read_to_string(token_path).await.ok()?; - - let client = Client::builder() - .timeout(Duration::from_secs(5)) - .add_default_header(( - awc::http::header::AUTHORIZATION.as_str(), - format!("Bearer {}", token), - )) - .finish(); - - let api_url = format!( - "https://kubernetes.default.svc/api/v1/namespaces/{}/pods", - pod_namespace - ); - - let mut response = client.get(api_url).send().await.ok()?; - - let body_bytes = response.body().await.ok()?; - let pod_list: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?; - - let targets: Vec = pod_list["items"] - .as_array()? - .iter() - .filter_map(|pod| { - let name = pod["metadata"]["name"].as_str()?.to_string(); - let phase = pod["status"]["phase"].as_str()?; - if phase != "Running" { - return None; - } - let pod_ip = pod["status"]["podIP"].as_str()?; - let annotations = pod["metadata"]["annotations"].as_object()?; - let port: u16 = annotations - .get("metrics.port") - .and_then(|v| v.as_str()) - .and_then(|s| s.parse().ok()) - .unwrap_or(8080); - let path = annotations - .get("metrics.path") - .and_then(|v| v.as_str()) - .unwrap_or("/metrics"); - - let labels = pod["metadata"]["labels"] - .as_object() - .map(|m| { - m.iter() - .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) - .collect() - }) - .unwrap_or_default(); - - Some(ScrapeTarget { - name, - addr: format!("{}:{}", pod_ip, port), - metrics_path: path.to_string(), - labels, - }) - }) - .collect(); - - Some(targets) -} diff --git a/apps/metrics/src/loki.rs b/apps/metrics/src/loki.rs deleted file mode 100644 index c170cd1..0000000 --- a/apps/metrics/src/loki.rs +++ /dev/null @@ -1,70 +0,0 @@ -use chrono::{DateTime, Utc}; -use reqwest::Client; -use serde::Serialize; -use std::collections::HashMap; - -#[derive(Clone)] -pub struct LokiForwarder { - url: String, - client: Client, - labels: HashMap, -} - -impl LokiForwarder { - pub fn new(url: String) -> Self { - Self { - url, - client: Client::builder() - .timeout(std::time::Duration::from_secs(5)) - .build() - .expect("valid reqwest client"), - labels: HashMap::new(), - } - } - - pub async fn push(&self, log_entries: Vec) -> anyhow::Result<()> { - if log_entries.is_empty() { - return Ok(()); - } - - let streams: Vec = vec![LokiStream { - stream: self.labels.clone(), - values: log_entries - .into_iter() - .map(|e| (format!("{}", e.timestamp), e.line)) - .collect(), - }]; - - let payload = LokiPayload { streams }; - - let resp = self - .client - .post(&self.url) - .header("Content-Type", "application/json") - .json(&payload) - .send() - .await; - - match resp { - Ok(r) if r.status().is_success() => Ok(()), - Ok(r) => anyhow::bail!("Loki push failed: {}", r.status()), - Err(e) => anyhow::bail!("Loki push error: {}", e), - } - } -} - -#[derive(Serialize)] -struct LokiPayload { - streams: Vec, -} - -#[derive(Serialize)] -struct LokiStream { - stream: HashMap, - values: Vec<(String, String)>, -} - -pub struct LokiEntry { - pub timestamp: DateTime, - pub line: String, -} diff --git a/apps/metrics/src/main.rs b/apps/metrics/src/main.rs deleted file mode 100644 index 2b01cbc..0000000 --- a/apps/metrics/src/main.rs +++ /dev/null @@ -1,633 +0,0 @@ -//! Unified observability aggregator for in-cluster deployment. -//! -//! Collects metrics from all app pods via Prometheus scrape, forwards traces -//! to OTLP endpoint, and streams logs from all pods to Loki-compatible backend. -//! -//! Usage: -//! METRICS_AGGREGATOR_PORT=9090 \ -//! OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \ -//! LOKI_URL=http://loki:3100/loki/api/v1/push \ -//! SCRAPE_INTERVAL_SECS=15 \ -//! SCRAPE_TARGETS_FILE=/etc/metrics/targets.json \ -//! metrics-aggregator - -mod args; -mod hotreload; -mod k8s_discovery; -mod loki; -mod metrics; -mod otel; -mod scrape; -mod stats_store; -mod target; - -use serde::Deserialize; -use std::collections::HashMap; -use std::fmt::Write as _; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; - -use actix_web::{HttpResponse, HttpServer, web}; -use clap::Parser; -use loki::{LokiEntry, LokiForwarder}; -use metrics::AggMetrics; -use observability::{init_tracing_subscriber, install_recorder, instance_id}; -use otel::OtelGuard; -use scrape::{HttpClient, ScrapeResult}; -use stats_store::StatsStore; -use target::ScrapeTarget; -use tokio::io::AsyncBufReadExt; -use tokio::sync::{RwLock, broadcast}; -use tokio::time::interval; - -type MetricsStore = Arc>>>; - -// StatsStore is defined in stats_store.rs — per-app aggregated data. - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let args = args::Args::parse(); - - init_tracing_subscriber(&args.log_level, false); - - let instance = instance_id(); - tracing::info!( - instance = %instance, - port = args.port, - scrape_interval = args.scrape_interval_secs, - "metrics-aggregator starting" - ); - - let prometheus_handle = install_recorder(); - metrics::init(); - - let metrics = AggMetrics::new(); - let store: MetricsStore = Arc::new(RwLock::new(HashMap::new())); - let stats_store: StatsStore = Arc::new(RwLock::new(HashMap::new())); - let targets: Arc>> = Arc::new(RwLock::new(Vec::new())); - let http = HttpClient::new(10); - - let otel_guard = init_otel_from_args(&args); - - let loki = init_loki_from_args(&args); - - let (shutdown_tx, _) = broadcast::channel::<()>(4); - - // Background task: evict push entries older than 5 minutes. - let stats_store_for_evict = stats_store.clone(); - let mut evict_shutdown = shutdown_tx.subscribe(); - tokio::spawn(async move { - let mut ticker = interval(Duration::from_secs(30)); - loop { - tokio::select! { - _ = evict_shutdown.recv() => break, - _ = ticker.tick() => { - let cutoff = chrono::Utc::now().timestamp() - 300; - let mut guard = stats_store_for_evict.write().await; - guard.retain(|_, entry| entry.last_seen >= cutoff); - } - } - } - }); - - if let Some(path) = &args.targets_file { - match target::load_targets_from_file(path).await { - Ok(initial_targets) => { - let mut guard = targets.write().await; - *guard = initial_targets; - tracing::info!(count = guard.len(), "loaded initial targets from file"); - } - Err(e) => { - tracing::warn!(error = %e, "failed to load targets file"); - } - } - - let tw = - hotreload::watch_targets_file(path.clone(), targets.clone(), shutdown_tx.subscribe()); - tokio::spawn(tw); - } else if std::env::var("KUBERNETES_SERVICE_HOST").is_ok() { - if let Some(k8s_targets) = k8s_discovery::k8s_pod_discovery().await { - let mut guard = targets.write().await; - *guard = k8s_targets.clone(); - tracing::info!(count = guard.len(), "discovered K8s pods as targets"); - } - } - - let scrape_filter = args - .scrape_apps - .as_ref() - .map(|s| s.split(',').map(|p| p.trim().to_string()).collect()); - - let scrape_targets = targets.clone(); - let scrape_store = store.clone(); - let scrape_metrics = metrics.clone(); - let scrape_http = http.clone(); - let loki_clone = loki.clone(); - let shutdown_tx_clone = shutdown_tx.clone(); - let scrape_interval = args.scrape_interval_secs; - let scrape_filter_clone = scrape_filter.clone(); - tokio::task::spawn_local(async move { - scrape_loop( - scrape_targets, - scrape_store, - scrape_metrics, - scrape_http, - scrape_interval, - scrape_filter_clone, - loki_clone, - shutdown_tx_clone.subscribe(), - ) - .await; - }); - - let log_shutdown = shutdown_tx.subscribe(); - let log_loki = loki.clone(); - tokio::task::spawn_local(async move { - log_collector(log_loki, log_shutdown).await; - }); - - let bind_addr: SocketAddr = ([0, 0, 0, 0], args.port).into(); - tracing::info!(addr = %bind_addr, "HTTP server starting"); - - let app_targets = targets.clone(); - let app_store = store.clone(); - let app_handle = prometheus_handle.clone(); - let loki_for_push: Option> = loki.map(Arc::new); - let app_stats = stats_store.clone(); - - let server = HttpServer::new(move || { - let targets = app_targets.clone(); - let store = app_store.clone(); - let handle = app_handle.clone(); - let stats_store = app_stats.clone(); - let loki_for_push: Option> = loki_for_push.clone(); - actix_web::App::new() - .app_data(web::Data::new(targets)) - .app_data(web::Data::new(store)) - .app_data(web::Data::new(handle)) - .app_data(web::Data::new(stats_store)) - .app_data(web::Data::new(loki_for_push)) - .route("/metrics", web::get().to(handle_metrics)) - .route("/api/v1/metrics", web::get().to(handle_metrics)) - .route("/api/v1/push", web::post().to(handle_push)) - .route("/api/v1/dashboard", web::get().to(handle_dashboard)) - .route("/api/v1/stats", web::get().to(handle_stats)) - .route("/health", web::get().to(handle_health)) - .route("/api/v1/health", web::get().to(handle_health)) - .route("/api/v1/targets", web::get().to(handle_targets)) - }) - .bind(&bind_addr)? - .run(); - - let server_handle = server.handle(); - - tokio::spawn(server); - - tokio::signal::ctrl_c().await.ok(); - tracing::info!("received Ctrl+C, shutting down"); - - let _ = shutdown_tx.send(()); - server_handle.stop(true).await; - - if let Some(guard) = otel_guard { - guard.shutdown().await; - } - - tracing::info!("metrics-aggregator stopped"); - Ok(()) -} - -fn init_otel_from_args(args: &args::Args) -> Option { - if args.no_otel { - return None; - } - - let endpoint = args - .otel_endpoint - .clone() - .or_else(|| std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok())?; - - match otel::init_otel(&endpoint, "metrics-aggregator") { - Ok(guard) => { - tracing::info!(endpoint = %endpoint, "OTLP tracing enabled"); - Some(guard) - } - Err(e) => { - tracing::warn!(error = %e, "OTLP init failed, continuing without traces"); - None - } - } -} - -fn init_loki_from_args(args: &args::Args) -> Option { - if args.no_loki { - return None; - } - - let url = args - .loki_url - .clone() - .or_else(|| std::env::var("LOKI_URL").ok())?; - - tracing::info!("Loki log forwarding enabled"); - Some(LokiForwarder::new(url)) -} - -async fn handle_metrics( - store: web::Data, - stats_store: web::Data, - handle: web::Data, -) -> HttpResponse { - let extra = vec![("aggregator_instance".to_string(), "default".to_string())]; - let scraped = render_aggregated_metrics(store, extra.clone()).await; - let pushed = render_pushed_metrics(stats_store).await; - let combined = format!("{}{}{}", handle.render(), scraped, pushed); - HttpResponse::Ok() - .content_type("text/plain; version=0.0.4; charset=utf-8") - .body(combined) -} - -async fn handle_health() -> HttpResponse { - HttpResponse::Ok() - .content_type("application/json") - .body(r#"{"status":"ok"}"#) -} - -async fn handle_targets(targets: web::Data>>>) -> HttpResponse { - let guard = targets.read().await; - let json = serde_json::to_string(&*guard).unwrap_or_default(); - HttpResponse::Ok() - .content_type("application/json") - .body(json) -} - -// ── Push endpoint payload ──────────────────────────────────────────────────── - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PushPayload { - app: String, - #[serde(default)] - instance: String, - timestamp: i64, - #[serde(default)] - http: Option, - #[serde(default)] - system: Option, - #[serde(default)] - business: HashMap, - #[serde(default)] - token_usage: Option, - #[serde(default)] - tasks: Option, - #[serde(default)] - latency: HashMap, - #[serde(default)] - logs: Vec, -} - -async fn handle_push( - stats_store: web::Data, - loki: web::Data>>, - payload: web::Json, -) -> HttpResponse { - let app = payload.app.clone(); - - stats_store::merge_push_payload( - &stats_store, - &app, - &payload.instance, - payload.timestamp, - payload.http.as_ref(), - payload.system.as_ref(), - &payload.business, - payload.token_usage.as_ref(), - payload.tasks.as_ref(), - &payload.latency, - &payload.logs, - ) - .await; - - // Forward logs to Loki if configured - if !payload.logs.is_empty() { - if let Some(loki_fwd) = loki.as_ref() { - let entries: Vec = payload - .logs - .iter() - .map(|l| LokiEntry { - timestamp: chrono::DateTime::from_timestamp(l.timestamp, 0) - .unwrap_or_else(chrono::Utc::now), - line: format!("[{}] {}", l.level.to_lowercase(), l.message), - }) - .collect(); - if let Err(e) = loki_fwd.push(entries).await { - tracing::warn!(error = %e, "loki push on /push failed"); - } - } - } - - HttpResponse::Ok().body("ok") -} - -async fn scrape_loop( - targets: Arc>>, - store: MetricsStore, - metrics: AggMetrics, - http: HttpClient, - interval_secs: u64, - scrape_apps_filter: Option>, - _loki: Option, - mut shutdown: broadcast::Receiver<()>, -) { - let mut ticker = interval(Duration::from_secs(interval_secs)); - loop { - tokio::select! { - _ = shutdown.recv() => break, - _ = ticker.tick() => { - let targets_snapshot = targets.read().await.clone(); - let count = targets_snapshot.len() as u64; - metrics.targets_total.set(count as f64); - - let mut healthy_count = 0u64; - - for target in &targets_snapshot { - if let Some(ref filter) = scrape_apps_filter { - if !filter.contains(&target.name) { - continue; - } - } - - metrics.scrape_total.increment(1); - - match http.scrape(target).await { - ScrapeResult::Success(body, duration_ms) => { - metrics.scrape_success.increment(1); - metrics.scrape_duration.record(duration_ms); - - let parsed = scrape::parse_prometheus(&body); - update_store(store.clone(), &target.name, parsed).await; - healthy_count += 1; - } - ScrapeResult::Timeout => { - metrics.scrape_failures.increment(1); - metrics.scrape_errors_timeout.increment(1); - tracing::warn!(target = %target.name, "scrape timeout"); - } - ScrapeResult::ConnectionError(e) => { - metrics.scrape_failures.increment(1); - metrics.scrape_errors_connection.increment(1); - tracing::warn!(target = %target.name, error = %e, "scrape connection error"); - } - ScrapeResult::HttpError(status) => { - metrics.scrape_failures.increment(1); - tracing::warn!(target = %target.name, status = status, "scrape HTTP error"); - } - } - } - - metrics.targets_healthy.set(healthy_count as f64); - } - } - } -} - -async fn update_store(store: MetricsStore, target_name: &str, metrics: Vec) { - let mut guard = store.write().await; - guard.insert(target_name.to_string(), metrics); -} - -async fn render_aggregated_metrics( - store: web::Data, - extra_group_labels: Vec<(String, String)>, -) -> String { - let guard = store.read().await; - let mut output = String::new(); - - for (target_name, metrics) in guard.iter() { - for metric in metrics { - let mut labels = metric.labels.clone(); - labels.insert( - "aggregated_by".to_string(), - "metrics-aggregator".to_string(), - ); - labels.insert("source_target".to_string(), target_name.clone()); - for (k, v) in &extra_group_labels { - labels.insert(k.clone(), v.clone()); - } - - let label_str = if labels.is_empty() { - String::new() - } else { - let pairs: Vec = labels - .iter() - .map(|(k, v)| { - format!( - r#"{}="{}""#, - k, - v.replace('\\', "\\\\").replace('"', "\\\"") - ) - }) - .collect(); - format!("{{{}}}", pairs.join(",")) - }; - - let _ = writeln!(&mut output, "{}{} {}", metric.name, label_str, metric.value); - } - } - - output -} - -async fn render_pushed_metrics(stats_store: web::Data) -> String { - let guard = stats_store.read().await; - let mut output = String::new(); - - for (app_name, entry) in guard.iter() { - let labels = [ - format!(r#"app="{}""#, app_name), - "aggregated_by".to_string(), - "metrics-aggregator".to_string(), - "push_source=true".to_string(), - ]; - - let label_str = format!("{{{}}}", labels.join(",")); - let h = &entry; - - let _ = writeln!( - &mut output, - "push_http_requests_total{} {}", - label_str, h.requests_total - ); - let _ = writeln!( - &mut output, - "push_http_request_duration_ms_total{} {}", - label_str, h.request_duration_ms_total - ); - let _ = writeln!( - &mut output, - "push_http_requests_2xx{} {}", - label_str, h.requests_2xx - ); - let _ = writeln!( - &mut output, - "push_http_requests_4xx{} {}", - label_str, h.requests_4xx - ); - let _ = writeln!( - &mut output, - "push_http_requests_5xx{} {}", - label_str, h.requests_5xx - ); - - for (endpoint, &count) in &h.endpoints { - let sanitized = endpoint.replace([' ', '/'], "_").to_lowercase(); - let ep_labels = format!( - r#"app="{}",endpoint="{}",aggregated_by="metrics-aggregator",push_source="true""#, - app_name, sanitized - ); - let _ = writeln!( - &mut output, - "push_http_endpoint_requests_total{{{}}} {}", - ep_labels, count - ); - } - - // System metrics in Prometheus format - let sys_labels = format!(r#"app="{}",aggregated_by="metrics-aggregator""#, app_name); - let _ = writeln!( - &mut output, - "system_cpu_usage_percent{{{}}} {}", - sys_labels, h.cpu_usage_percent - ); - let _ = writeln!( - &mut output, - "system_memory_used_mb{{{}}} {}", - sys_labels, h.memory_used_mb - ); - let _ = writeln!( - &mut output, - "system_memory_total_mb{{{}}} {}", - sys_labels, h.memory_total_mb - ); - let _ = writeln!( - &mut output, - "system_uptime_secs{{{}}} {}", - sys_labels, h.uptime_secs - ); - - // Business counters - for (counter_name, value) in &h.business { - let biz_labels = format!(r#"app="{}",aggregated_by="metrics-aggregator""#, app_name); - let _ = writeln!(&mut output, "{}{{{}}} {}", counter_name, biz_labels, value); - } - - // Token usage - let ai_labels = format!(r#"app="{}",aggregated_by="metrics-aggregator""#, app_name); - let _ = writeln!( - &mut output, - "ai_input_tokens_total{{{}}} {}", - ai_labels, h.ai_input_tokens_total - ); - let _ = writeln!( - &mut output, - "ai_output_tokens_total{{{}}} {}", - ai_labels, h.ai_output_tokens_total - ); - let _ = writeln!( - &mut output, - "ai_calls_total{{{}}} {}", - ai_labels, h.ai_calls_total - ); - - // Latency per endpoint - for (endpoint, lat) in &h.latency { - let lat_labels = format!( - r#"app="{}",endpoint="{}",aggregated_by="metrics-aggregator""#, - app_name, endpoint - ); - let _ = writeln!( - &mut output, - "latency_p99_ms{{{}}} {}", - lat_labels, lat.p99_ms - ); - let _ = writeln!( - &mut output, - "latency_p90_ms{{{}}} {}", - lat_labels, lat.p90_ms - ); - let _ = writeln!( - &mut output, - "latency_p50_ms{{{}}} {}", - lat_labels, lat.p50_ms - ); - let _ = writeln!( - &mut output, - "latency_max_ms{{{}}} {}", - lat_labels, lat.max_ms - ); - } - } - - output -} - -// ── JSON API handlers ──────────────────────────────────────────────────────── - -async fn handle_dashboard(stats_store: web::Data) -> HttpResponse { - let dashboard = stats_store::build_dashboard(&stats_store).await; - let json = serde_json::to_string(&dashboard).unwrap_or_default(); - HttpResponse::Ok() - .content_type("application/json") - .body(json) -} - -async fn handle_stats(stats_store: web::Data) -> HttpResponse { - // Returns per-app stats as JSON - let guard = stats_store.read().await; - let json = serde_json::to_string(&*guard).unwrap_or_default(); - HttpResponse::Ok() - .content_type("application/json") - .body(json) -} - -async fn log_collector(loki: Option, mut shutdown: broadcast::Receiver<()>) { - let stdin = tokio::io::stdin(); - let mut reader = tokio::io::BufReader::new(stdin); - let mut interval_tick = interval(Duration::from_secs(1)); - let mut batch: Vec = Vec::with_capacity(100); - let mut line_buf = String::new(); - - loop { - tokio::select! { - _ = shutdown.recv() => break, - _ = interval_tick.tick() => { - if !batch.is_empty() { - if let Some(ref loki) = loki { - if let Err(e) = loki.push(std::mem::take(&mut batch)).await { - tracing::warn!(error = %e, "Loki push failed"); - } - } - } - } - _ = async { line_buf.clear(); reader.read_line(&mut line_buf).await.ok() } => { - if !line_buf.is_empty() { - let line = line_buf.trim_end().to_string(); - if !line.is_empty() { - batch.push(LokiEntry { - timestamp: chrono::Utc::now(), - line, - }); - if batch.len() >= 100 { - if let Some(ref loki) = loki { - if let Err(e) = loki.push(std::mem::take(&mut batch)).await { - tracing::warn!(error = %e, "Loki push failed"); - } - } - } - } - } - } - } - } -} diff --git a/apps/metrics/src/metrics.rs b/apps/metrics/src/metrics.rs deleted file mode 100644 index 6991a67..0000000 --- a/apps/metrics/src/metrics.rs +++ /dev/null @@ -1,101 +0,0 @@ -use metrics::{ - Counter, Gauge, Histogram, Unit, describe_counter, describe_gauge, describe_histogram, -}; - -pub fn init() { - describe_gauge!( - "aggregator_targets_total", - Unit::Count, - "Total number of scrape targets known to the aggregator" - ); - describe_gauge!( - "aggregator_targets_healthy", - Unit::Count, - "Number of scrape targets that responded last scrape" - ); - describe_counter!( - "aggregator_scrape_total", - Unit::Count, - "Total number of scrape attempts" - ); - describe_counter!( - "aggregator_scrape_success", - Unit::Count, - "Successful scrapes" - ); - describe_counter!( - "aggregator_scrape_failures", - Unit::Count, - "Failed scrape attempts" - ); - describe_counter!( - "aggregator_scrape_errors_parse", - Unit::Count, - "Scrape failures due to parse errors" - ); - describe_counter!( - "aggregator_scrape_errors_timeout", - Unit::Count, - "Scrape failures due to timeout" - ); - describe_counter!( - "aggregator_scrape_errors_connection", - Unit::Count, - "Scrape failures due to connection errors" - ); - describe_counter!( - "aggregator_targets_discovered", - Unit::Count, - "Total targets discovered" - ); - describe_counter!( - "aggregator_targets_lost", - Unit::Count, - "Total targets that disappeared" - ); - describe_histogram!( - "aggregator_scrape_duration_ms", - Unit::Milliseconds, - "Scrape duration in milliseconds" - ); -} - -#[derive(Clone)] -#[allow(dead_code)] -pub struct AggMetrics { - pub targets_total: Gauge, - pub targets_healthy: Gauge, - pub scrape_total: Counter, - pub scrape_success: Counter, - pub scrape_failures: Counter, - pub scrape_errors_parse: Counter, - pub scrape_errors_timeout: Counter, - pub scrape_errors_connection: Counter, - pub targets_discovered: Counter, - pub targets_lost: Counter, - pub scrape_duration: Histogram, -} - -impl Default for AggMetrics { - fn default() -> Self { - Self { - targets_total: metrics::gauge!("aggregator_targets_total"), - targets_healthy: metrics::gauge!("aggregator_targets_healthy"), - scrape_total: metrics::counter!("aggregator_scrape_total"), - scrape_success: metrics::counter!("aggregator_scrape_success"), - scrape_failures: metrics::counter!("aggregator_scrape_failures"), - scrape_errors_parse: metrics::counter!("aggregator_scrape_errors_parse"), - scrape_errors_timeout: metrics::counter!("aggregator_scrape_errors_timeout"), - scrape_errors_connection: metrics::counter!("aggregator_scrape_errors_connection"), - targets_discovered: metrics::counter!("aggregator_targets_discovered"), - targets_lost: metrics::counter!("aggregator_targets_lost"), - scrape_duration: metrics::histogram!("aggregator_scrape_duration_ms"), - } - } -} - -impl AggMetrics { - pub fn new() -> Self { - Self::default() - } -} diff --git a/apps/metrics/src/otel.rs b/apps/metrics/src/otel.rs deleted file mode 100644 index 8c69e97..0000000 --- a/apps/metrics/src/otel.rs +++ /dev/null @@ -1,42 +0,0 @@ -use anyhow::Context; -use opentelemetry::trace::TracerProvider; -use opentelemetry_otlp::{SpanExporter, WithExportConfig}; -use opentelemetry_sdk::trace as sdktrace; -use tracing_opentelemetry::layer; -use tracing_subscriber::prelude::*; - -pub struct OtelGuard { - provider: sdktrace::SdkTracerProvider, -} - -impl OtelGuard { - pub async fn shutdown(self) { - if let Err(e) = self.provider.shutdown() { - tracing::warn!(error = %e, "OTLP shutdown error"); - } - } -} - -pub fn init_otel(endpoint: &str, service_name: &str) -> anyhow::Result { - let exporter = SpanExporter::builder() - .with_http() - .with_endpoint(endpoint) - .build() - .context("build OTLP exporter")?; - - let tracer_provider = sdktrace::SdkTracerProvider::builder() - .with_batch_exporter(exporter) - .build(); - - let tracer = tracer_provider.tracer(service_name.to_string()); - let otel_layer = layer().with_tracer(tracer); - - tracing_subscriber::registry() - .with(otel_layer) - .try_init() - .context("install OTLP tracing subscriber")?; - - Ok(OtelGuard { - provider: tracer_provider, - }) -} diff --git a/apps/metrics/src/scrape.rs b/apps/metrics/src/scrape.rs deleted file mode 100644 index 2b3cf98..0000000 --- a/apps/metrics/src/scrape.rs +++ /dev/null @@ -1,135 +0,0 @@ -use awc::Client; -use std::collections::HashMap; - -use crate::target::ScrapeTarget; - -#[derive(Clone)] -pub struct HttpClient { - client: Client, -} - -impl HttpClient { - pub fn new(timeout_secs: u64) -> Self { - let client = Client::builder() - .timeout(std::time::Duration::from_secs(timeout_secs)) - .finish(); - Self { client } - } - - pub async fn scrape(&self, target: &ScrapeTarget) -> ScrapeResult { - let start = std::time::Instant::now(); - let url = target.url(); - - let mut resp = match self.client.get(url).send().await { - Ok(resp) => resp, - Err(e) => { - let msg = e.to_string(); - if msg.contains("timeout") || msg.contains("TimedOut") || msg.contains("timed out") - { - return ScrapeResult::Timeout; - } - return ScrapeResult::ConnectionError(msg); - } - }; - - if !resp.status().is_success() { - return ScrapeResult::HttpError(resp.status().as_u16()); - } - - let body = match resp.body().await { - Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(), - Err(e) => return ScrapeResult::ConnectionError(e.to_string()), - }; - - let scrape_ms = start.elapsed().as_millis() as f64; - ScrapeResult::Success(body, scrape_ms) - } -} - -pub enum ScrapeResult { - Success(String, f64), - Timeout, - ConnectionError(String), - HttpError(u16), -} - -#[derive(Clone, Debug)] -pub struct PromMetric { - pub name: String, - pub value: f64, - pub labels: HashMap, -} - -pub fn parse_prometheus(body: &str) -> Vec { - let mut metrics = Vec::new(); - for line in body.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - - let (name_and_labels, value_str) = match line.find(' ') { - Some(pos) => (&line[..pos], &line[pos + 1..]), - None => continue, - }; - - let value: f64 = match value_str - .split_whitespace() - .next() - .and_then(|v| v.parse().ok()) - { - Some(v) => v, - None => continue, - }; - - let (metric_name, labels) = if let Some(brace) = name_and_labels.find('{') { - let name = &name_and_labels[..brace]; - let label_str = &name_and_labels[brace + 1..name_and_labels.len() - 1]; - let labels = parse_labels(label_str); - (name.to_string(), labels) - } else { - (name_and_labels.to_string(), HashMap::new()) - }; - - metrics.push(PromMetric { - name: metric_name, - value, - labels, - }); - } - metrics -} - -pub fn parse_labels(s: &str) -> HashMap { - let mut labels = HashMap::new(); - let mut remaining = s; - while !remaining.is_empty() { - if let Some(eq) = remaining.find('=') { - let key = remaining[..eq].trim().to_string(); - remaining = &remaining[eq + 1..]; - let (value, rest) = if remaining.starts_with('"') { - let end = remaining[1..] - .find('"') - .map(|p| p + 1) - .unwrap_or(remaining.len()); - (&remaining[1..end], &remaining[end + 1..]) - } else if remaining.starts_with('\'') { - let end = remaining[1..] - .find('\'') - .map(|p| p + 1) - .unwrap_or(remaining.len()); - (&remaining[1..end], &remaining[end + 1..]) - } else { - let end = remaining - .find(|c: char| !c.is_alphanumeric() && c != '_' && c != '-') - .unwrap_or(remaining.len()); - (&remaining[..end], &remaining[end..]) - }; - labels.insert(key, value.to_string()); - remaining = rest.trim_start_matches(',').trim_start(); - } else { - break; - } - } - labels -} diff --git a/apps/metrics/src/stats_store.rs b/apps/metrics/src/stats_store.rs deleted file mode 100644 index ecd4da8..0000000 --- a/apps/metrics/src/stats_store.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! Stats store: receives expanded push payloads from all apps, -//! aggregates over time, computes derived statistics (p99 etc), -//! and provides JSON API for external consumption. - -use serde::Serialize; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; - -/// Per-app, per-instance aggregated stats entry. -#[derive(Debug, Clone, Default, Serialize)] -pub struct AppStats { - /// Last seen timestamp. - pub last_seen: i64, - /// Number of push samples received. - pub sample_count: u64, - - // ── HTTP ───────────────────────────────────────────────────── - pub requests_total: u64, - pub request_duration_ms_total: u64, - pub requests_2xx: u64, - pub requests_4xx: u64, - pub requests_5xx: u64, - pub endpoints: HashMap, - - // ── System ─────────────────────────────────────────────────── - pub cpu_usage_percent: f32, - pub memory_used_mb: u64, - pub memory_total_mb: u64, - pub uptime_secs: u64, - - // ── Business counters ──────────────────────────────────────── - pub business: HashMap, - - // ── Token usage ────────────────────────────────────────────── - pub ai_input_tokens_total: i64, - pub ai_output_tokens_total: i64, - pub ai_calls_total: i64, - pub ai_calls_success: i64, - pub ai_calls_failure: i64, - pub token_by_model: HashMap, - - // ── Tasks ──────────────────────────────────────────────────── - pub tasks_queued: i64, - pub tasks_running: i64, - pub tasks_completed: i64, - pub tasks_failed: i64, - - // ── Latency ────────────────────────────────────────────────── - pub latency: HashMap, - - // ── Logs ───────────────────────────────────────────────────── - #[serde(skip_serializing)] - pub logs: Vec<(i64, String)>, -} - -#[derive(Debug, Clone, Default, Serialize)] -pub struct ModelTokenStats { - pub input_tokens: i64, - pub output_tokens: i64, - pub calls: i64, -} - -#[derive(Debug, Clone, Default, Serialize)] -pub struct LatencyStats { - pub p50_ms: f64, - pub p90_ms: f64, - pub p99_ms: f64, - pub max_ms: f64, - pub count: u64, -} - -/// The global stats store: app_name → AppStats. -pub type StatsStore = Arc>>; - -/// Merge a new push payload into the stats store. -pub async fn merge_push_payload( - store: &StatsStore, - app: &str, - _instance: &str, - timestamp: i64, - http: Option<&observability::push::HttpPayload>, - system: Option<&observability::push::SystemPayload>, - business: &HashMap, - token_usage: Option<&observability::push::TokenUsagePayload>, - tasks: Option<&observability::push::TaskStatsPayload>, - latency: &HashMap, - logs: &[observability::push::LogEntry], -) { - // Use app_name as key (merge across instances for aggregation) - let mut guard = store.write().await; - let entry = guard.entry(app.to_string()).or_default(); - entry.last_seen = timestamp; - entry.sample_count += 1; - - // HTTP — accumulate (not replace, so we get totals over time) - if let Some(http) = http { - entry.requests_total = http.requests_total; - entry.request_duration_ms_total = http.request_duration_ms_total; - entry.requests_2xx = http.requests_2xx; - entry.requests_4xx = http.requests_4xx; - entry.requests_5xx = http.requests_5xx; - for (ep, count) in &http.endpoints { - *entry.endpoints.entry(ep.clone()).or_insert(0) = *count; - } - } - - // System — replace (current snapshot, not cumulative) - if let Some(sys) = system { - entry.cpu_usage_percent = sys.cpu_usage_percent; - entry.memory_used_mb = sys.memory_used_mb; - entry.memory_total_mb = sys.memory_total_mb; - entry.uptime_secs = sys.uptime_secs; - } - - // Business — replace with latest snapshot - entry.business = business.clone(); - - // Token usage — replace with latest - if let Some(tu) = token_usage { - entry.ai_input_tokens_total = tu.ai_input_tokens_total; - entry.ai_output_tokens_total = tu.ai_output_tokens_total; - entry.ai_calls_total = tu.ai_calls_total; - entry.ai_calls_success = tu.ai_calls_success; - entry.ai_calls_failure = tu.ai_calls_failure; - for (model, usage) in &tu.by_model { - let ms = entry.token_by_model.entry(model.clone()).or_default(); - ms.input_tokens = usage.input_tokens; - ms.output_tokens = usage.output_tokens; - ms.calls = usage.calls; - } - } - - // Tasks — replace with latest - if let Some(t) = tasks { - entry.tasks_queued = t.queued; - entry.tasks_running = t.running; - entry.tasks_completed = t.completed; - entry.tasks_failed = t.failed; - } - - // Latency — replace with latest snapshots - for (endpoint, snap) in latency { - let ls = entry.latency.entry(endpoint.clone()).or_default(); - ls.p50_ms = snap.p50_ms; - ls.p90_ms = snap.p90_ms; - ls.p99_ms = snap.p99_ms; - ls.max_ms = snap.max_ms; - ls.count = snap.count; - } - - // Logs — append (keep last 300 lines) - for log in logs { - entry.logs.push(( - log.timestamp, - format!("[{}] {}", log.level.to_lowercase(), log.message), - )); - } - let cutoff = chrono::Utc::now().timestamp() - 300; - entry.logs.retain(|(ts, _)| *ts >= cutoff); -} - -/// Dashboard response combining all apps' stats. -#[derive(Debug, Serialize)] -pub struct DashboardResponse { - /// Timestamp of this snapshot. - pub timestamp: i64, - /// Total number of app instances reporting. - pub app_count: u64, - /// Per-app aggregated stats. - pub apps: HashMap, - /// Derived: average p99 latency across all apps. - pub avg_p99_ms: f64, - /// Derived: total tokens consumed across all apps. - pub total_input_tokens: i64, - pub total_output_tokens: i64, - /// Derived: total AI calls across all apps. - pub total_ai_calls: i64, -} - -/// Build the dashboard response from the stats store. -pub async fn build_dashboard(store: &StatsStore) -> DashboardResponse { - let guard = store.read().await; - - let mut avg_p99 = 0.0; - let mut p99_count = 0; - let mut total_input = 0i64; - let mut total_output = 0i64; - let mut total_calls = 0i64; - - for (_, stats) in guard.iter() { - total_input += stats.ai_input_tokens_total; - total_output += stats.ai_output_tokens_total; - total_calls += stats.ai_calls_total; - - for (_, lat) in &stats.latency { - avg_p99 += lat.p99_ms; - p99_count += 1; - } - } - - let avg_p99_ms = if p99_count > 0 { - avg_p99 / p99_count as f64 - } else { - 0.0 - }; - - DashboardResponse { - timestamp: chrono::Utc::now().timestamp(), - app_count: guard.len() as u64, - apps: guard.clone(), - avg_p99_ms, - total_input_tokens: total_input, - total_output_tokens: total_output, - total_ai_calls: total_calls, - } -} diff --git a/apps/metrics/src/target.rs b/apps/metrics/src/target.rs deleted file mode 100644 index dad1e49..0000000 --- a/apps/metrics/src/target.rs +++ /dev/null @@ -1,36 +0,0 @@ -use anyhow::Context; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ScrapeTarget { - pub name: String, - pub addr: String, - #[serde(default = "default_metrics_path")] - pub metrics_path: String, - #[serde(default)] - pub labels: HashMap, -} - -fn default_metrics_path() -> String { - "/metrics".to_string() -} - -impl ScrapeTarget { - pub fn url(&self) -> String { - if self.metrics_path.starts_with("http") { - self.metrics_path.clone() - } else { - format!("http://{}{}", self.addr, self.metrics_path) - } - } -} - -pub async fn load_targets_from_file(path: &str) -> anyhow::Result> { - let content = tokio::fs::read_to_string(path) - .await - .context("read targets file")?; - let targets: Vec = - serde_json::from_str(&content).with_context(|| format!("parse targets file {path}"))?; - Ok(targets) -} diff --git a/apps/migrate/Cargo.toml b/apps/migrate/Cargo.toml deleted file mode 100644 index 3fe1bbe..0000000 --- a/apps/migrate/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "migrate-cli" -version.workspace = true -edition.workspace = true - -[dependencies] -migrate.workspace = true -sea-orm = { workspace = true, features = ["sqlx-all", "runtime-tokio"] } -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -anyhow.workspace = true -clap.workspace = true -dotenvy.workspace = true -config = { workspace = true } \ No newline at end of file diff --git a/apps/migrate/src/main.rs b/apps/migrate/src/main.rs deleted file mode 100644 index 126c359..0000000 --- a/apps/migrate/src/main.rs +++ /dev/null @@ -1,102 +0,0 @@ -use anyhow::Context; -use clap::Command; -use migrate::MigratorTrait; -use sea_orm::{Database, DatabaseConnection}; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - dotenvy::dotenv().ok(); - config::AppConfig::load(); - - let cmd = Command::new("migrate") - .about("Database migration CLI") - .arg( - clap::Arg::new("steps") - .help("Number of migrations (for up/down)") - .required(false) - .index(1), - ) - .subcommand(Command::new("up").about("Apply pending migrations")) - .subcommand(Command::new("down").about("Revert applied migrations")) - .subcommand(Command::new("fresh").about("Drop all tables and re-apply")) - .subcommand(Command::new("refresh").about("Revert all then re-apply")) - .subcommand(Command::new("reset").about("Revert all applied migrations")) - .subcommand(Command::new("status").about("Show migration status")) - .try_get_matches() - .map_err(|e| anyhow::anyhow!("{}", e))?; - - let db_url = config::AppConfig::load().database_url()?; - - let db: DatabaseConnection = Database::connect(&db_url).await?; - - match cmd.subcommand_name() { - Some("up") => { - let steps = cmd - .get_one::("steps") - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - run_up(&db, steps).await?; - } - Some("down") => { - let steps = cmd - .get_one::("steps") - .and_then(|s| s.parse().ok()) - .unwrap_or(1); - run_down(&db, steps).await?; - } - Some("fresh") => run_fresh(&db).await?, - Some("refresh") => run_refresh(&db).await?, - Some("reset") => run_reset(&db).await?, - Some("status") => run_status(&db).await?, - _ => { - eprintln!( - "Usage: migrate \nCommands: up, down, fresh, refresh, reset, status" - ); - std::process::exit(1); - } - } - - Ok(()) -} - -async fn run_up(db: &DatabaseConnection, steps: u32) -> anyhow::Result<()> { - migrate::Migrator::up(db, if steps == 0 { None } else { Some(steps) }) - .await - .context("failed to run migrations up")?; - Ok(()) -} - -async fn run_down(db: &DatabaseConnection, steps: u32) -> anyhow::Result<()> { - migrate::Migrator::down(db, Some(steps)) - .await - .context("failed to run migrations down")?; - Ok(()) -} - -async fn run_fresh(db: &DatabaseConnection) -> anyhow::Result<()> { - migrate::Migrator::fresh(db) - .await - .context("failed to run migrations fresh")?; - Ok(()) -} - -async fn run_refresh(db: &DatabaseConnection) -> anyhow::Result<()> { - migrate::Migrator::refresh(db) - .await - .context("failed to run migrations refresh")?; - Ok(()) -} - -async fn run_reset(db: &DatabaseConnection) -> anyhow::Result<()> { - migrate::Migrator::reset(db) - .await - .context("failed to run migrations reset")?; - Ok(()) -} - -async fn run_status(db: &DatabaseConnection) -> anyhow::Result<()> { - migrate::Migrator::status(db) - .await - .context("failed to get migration status")?; - Ok(()) -} diff --git a/apps/static/Cargo.toml b/apps/static/Cargo.toml deleted file mode 100644 index 5e6c885..0000000 --- a/apps/static/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "static-server" -version.workspace = true -edition.workspace = true - -[dependencies] -actix-web = { workspace = true } -actix-files = { workspace = true } -actix-cors = { workspace = true } -observability = { workspace = true } -metrics-exporter-prometheus = "0.13" -tokio = { workspace = true, features = ["full"] } -futures = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -mime = { workspace = true } -mime_guess2 = { workspace = true } -slog = { workspace = true } -anyhow = { workspace = true } -env_logger = { workspace = true } -log = "0.4" diff --git a/apps/static/src/main.rs b/apps/static/src/main.rs deleted file mode 100644 index 68129bc..0000000 --- a/apps/static/src/main.rs +++ /dev/null @@ -1,212 +0,0 @@ -use actix_cors::Cors; -use actix_files::Files; -use actix_web::dev::{Service, ServiceRequest, ServiceResponse}; -use actix_web::{App, HttpResponse, HttpServer, http::header, web}; -use futures::future::LocalBoxFuture; -use log::info; -use observability::{HttpMetrics, init_tracing_subscriber, install_recorder, push::MetricsPusher}; -use std::path::PathBuf; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Instant; - -/// Static file server for avatar, blob, and other static files -/// Serves files from /data/{type} directories - -#[derive(Clone)] -struct StaticConfig { - root: PathBuf, - cors_enabled: bool, -} - -impl StaticConfig { - fn from_env() -> Self { - let root = std::env::var("STATIC_ROOT").unwrap_or_else(|_| "/data".to_string()); - let cors = std::env::var("STATIC_CORS").unwrap_or_else(|_| "true".to_string()); - - Self { - root: PathBuf::from(root), - cors_enabled: cors == "true" || cors == "1", - } - } - - fn ensure_dir(&self, name: &str) -> PathBuf { - let dir = self.root.join(name); - if !dir.exists() { - std::fs::create_dir_all(&dir).ok(); - } - dir - } -} - -async fn health() -> HttpResponse { - HttpResponse::Ok().json(serde_json::json!({ - "status": "ok", - "service": "static-server" - })) -} - -/// Custom middleware that logs requests except for noisy paths (health, metrics, static files). -struct RequestLogger; - -impl actix_web::dev::Transform for RequestLogger -where - S: Service, Error = actix_web::Error>, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Transform = RequestLoggerService; - type InitError = (); - type Future = futures::future::Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - futures::future::ok(RequestLoggerService { - service, - _marker: std::marker::PhantomData, - }) - } -} - -struct RequestLoggerService { - service: S, - _marker: std::marker::PhantomData, -} - -impl Service for RequestLoggerService -where - S: Service, Error = actix_web::Error>, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - let path = req.path().to_string(); - let method = req.method().to_string(); - let should_log = !(path == "/health" - || path == "/metrics" - || path.starts_with("/ws") - || path.starts_with("/avatar") - || path.starts_with("/blob") - || path.starts_with("/media") - || path.starts_with("/static")); - - let start = Instant::now(); - let fut = self.service.call(req); - - Box::pin(async move { - let res = fut.await?; - if should_log { - info!( - target: "static_server", - "{} {} {} {:?}", - method, - path, - res.status().as_u16(), - start.elapsed() - ); - } - Ok(res) - }) - } -} - -#[actix_web::main] -async fn main() -> anyhow::Result<()> { - init_tracing_subscriber("info", false); - let prometheus_handle = Arc::new(install_recorder()); - let http_metrics = Arc::new(HttpMetrics::new()); - - // Metrics pusher: periodically push all metrics to apps/metrics aggregator - if let Some(push_url) = std::env::var("METRICS_PUSH_URL").ok() { - let pusher = MetricsPusher::new(&push_url, "static"); - pusher.spawn( - http_metrics.clone(), - prometheus_handle.clone(), - std::time::Duration::from_secs(15), - ); - info!("Metrics pusher started (interval 15s, url: {})", push_url); - } - - let cfg = StaticConfig::from_env(); - let bind = std::env::var("STATIC_BIND").unwrap_or_else(|_| "0.0.0.0:8081".to_string()); - - println!("Static file server starting..."); - println!(" Root: {:?}", cfg.root); - println!(" Bind: {}", bind); - println!( - " CORS: {}", - if cfg.cors_enabled { - "enabled" - } else { - "disabled" - } - ); - - // Ensure all directories exist - for name in ["avatar", "blob", "media", "static"] { - let dir = cfg.ensure_dir(name); - println!(" {} dir: {:?}", name, dir); - } - - let root = cfg.root.clone(); - let cors_enabled = cfg.cors_enabled; - - HttpServer::new(move || { - let root = root.clone(); - - let cors = if cors_enabled { - // WARNING: allow_any_origin is intentional for static asset serving (CDN mode) - // Ensure no sensitive files are served from this directory - Cors::default() - .allow_any_origin() - .allowed_methods(vec!["GET", "HEAD", "OPTIONS"]) - .allowed_headers(vec![ - header::AUTHORIZATION, - header::ACCEPT, - header::CONTENT_TYPE, - ]) - .max_age(3600) - } else { - Cors::permissive() - }; - - App::new() - .wrap(cors) - .wrap(RequestLogger) - .route("/health", web::get().to(health)) - .service( - Files::new("/avatar", root.join("avatar")) - .prefer_utf8(true) - .index_file("index.html"), - ) - .service( - Files::new("/blob", root.join("blob")) - .prefer_utf8(true) - .index_file("index.html"), - ) - .service( - Files::new("/media", root.join("media")) - .prefer_utf8(true) - .index_file("index.html"), - ) - .service( - Files::new("/static", root.join("static")) - .prefer_utf8(true) - .index_file("index.html"), - ) - }) - .bind(&bind)? - .run() - .await?; - - Ok(()) -} diff --git a/bun.lock b/bun.lock index 8158857..9adb9dd 100644 --- a/bun.lock +++ b/bun.lock @@ -3,113 +3,124 @@ "configVersion": 1, "workspaces": { "": { - "name": "vite-app", + "name": "ddd", "dependencies": { - "@base-ui/react": "^1.4.1", - "@fontsource-variable/geist": "^5.2.8", - "@grafana/faro-react": "^1.14.0", - "@grafana/faro-web-tracing": "^1.14.0", - "@lobehub/icons": "^5.8.0", + "@base-ui/react": "^1.5.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@fontsource-variable/dm-sans": "^5.2.8", + "@fontsource-variable/geist": "^5.2.9", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@fontsource-variable/space-grotesk": "^5.2.10", + "@lobehub/icons": "^5.10.0", + "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", "@streamdown/cjk": "^1.0.3", "@streamdown/code": "^1.1.1", "@streamdown/math": "^1.0.2", "@streamdown/mermaid": "^1.0.2", - "@tailwindcss/typography": "^0.5.19", - "@tanstack/react-hotkeys": "^0.10.0", - "@tanstack/react-query": "^5.100.8", + "@tanstack/react-query": "^5.100.14", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.24", - "ai": "^6.0.177", - "axios": "^1.15.2", + "ai": "^6.0.193", + "ansi-to-react": "^6.2.6", + "axios": "^1.16.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "dayjs": "^1.11.20", - "dexie": "^4.4.2", + "date-fns": "^4.3.0", + "dexie": "^4.4.3", "dexie-react-hooks": "^4.4.0", "embla-carousel-react": "^8.6.0", - "i": "^0.3.7", - "idb": "^8.0.3", + "emoji-picker-react": "^4.19.1", "input-otp": "^1.4.2", - "jsencrypt": "^3.5.4", - "lucide-react": "^1.14.0", - "motion": "^12.38.0", + "lucide-react": "^1.17.0", + "motion": "^12.40.0", "nanoid": "^5.1.11", "next-themes": "^0.4.6", - "radix-ui": "^1.4.3", - "react": "^19.2.4", - "react-day-picker": "^9.14.0", - "react-dom": "^19.2.4", - "react-hook-form": "^7.75.0", + "react": "^19.2.6", + "react-arborist": "^3.7.0", + "react-day-picker": "^10.0.1", + "react-diff-view": "^3.3.3", + "react-dom": "^19.2.6", + "react-jsx-parser": "^2.4.1", "react-markdown": "^10.1.0", - "react-resizable-panels": "^4.10.0", - "react-router-dom": "^7.14.2", - "react-virtuoso": "^4.18.6", - "recharts": "^3.8.1", - "rehype-raw": "^7.0.0", - "rehype-sanitize": "^6.0.0", + "react-resizable-panels": "^4.11.2", + "react-router": "^7.15.1", + "react-router-dom": "^7.15.1", + "react-virtuoso": "^4.18.7", + "recharts": "3.8.0", + "rehype-highlight": "^7.0.2", "remark-gfm": "^4.0.1", - "socket.io": "^4.8.3", + "shadcn": "^4.8.0", + "shiki": "^4.1.0", + "socket.io-client": "^4.8.3", "sonner": "^2.0.7", "streamdown": "^2.5.0", "tailwind-merge": "^3.6.0", - "tailwindcss": "^4.2.1", + "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", - "uppy": "^5.2.4", "vaul": "^1.1.2", - "zod": "^4.4.2", - "zustand": "^5.0.12", + "zustand": "^5.0.13", }, "devDependencies": { - "@eslint/js": "^9.39.4", - "@tailwindcss/postcss": "^4.2.4", - "@tailwindcss/vite": "^4.2.4", - "@types/node": "^24.12.0", + "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", + "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.2.0", - "ajv": "8", - "ajv-draft-04": "1", - "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.0.1", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^16.5.0", - "orval": "^8.9.0", - "prettier": "^3.8.1", - "prettier-plugin-tailwindcss": "^0.7.2", - "shadcn": "^4.7.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.57.1", - "vite": "^7.3.1", - "vite-bundle-analyzer": "^1.3.8", + "globals": "^17.6.0", + "orval": "^8.12.3", + "tailwindcss": "^4.3.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12", }, }, }, "packages": { - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.112", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jiBao9pR4owWyjo0BnuNc7WSQBGOD0thysE4AFgZXaG+zMFbISQXUkJr7ePw/phBvePy7jE5FSA2Lf7lwqUiiQ=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.121", "https://registry.npmmirror.com/@ai-sdk/gateway/-/gateway-3.0.121.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uY248djJRxa5W68MHiyqO8WLdOeKQoRClGg7PVX/VPhVW8SJNM7/l5DcrA5WAM3YfQrLyNkgZa2VOu8T0t8LUw=="], - "@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-3.0.10.tgz", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], - "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@ant-design/colors": ["@ant-design/colors@8.0.1", "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.1.tgz", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="], - "@ant-design/colors": ["@ant-design/colors@8.0.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.0" } }, "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ=="], + "@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="], - "@ant-design/cssinjs": ["@ant-design/cssinjs@2.1.2", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1", "csstype": "^3.1.3", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ=="], + "@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@2.1.2", "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@babel/runtime": "^7.23.2", "@rc-component/util": "^1.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA=="], - "@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@2.1.2", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@babel/runtime": "^7.23.2", "@rc-component/util": "^1.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA=="], + "@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-3.0.1.tgz", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="], - "@ant-design/fast-color": ["@ant-design/fast-color@3.0.1", "", {}, "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw=="], + "@ant-design/icons": ["@ant-design/icons@6.2.5", "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.2.5.tgz", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw=="], - "@ant-design/icons": ["@ant-design/icons@6.2.2", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/icons-svg": "^4.4.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ=="], + "@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="], - "@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="], + "@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-2.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="], - "@ant-design/react-slick": ["@ant-design/react-slick@2.0.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "clsx": "^2.1.1", "json2mq": "^0.2.0", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg=="], - - "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], "@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], @@ -157,10 +168,6 @@ "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "https://registry.npmmirror.com/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], @@ -173,69 +180,67 @@ "@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@base-ui/react": ["@base-ui/react@1.4.1", "https://registry.npmmirror.com/@base-ui/react/-/react-1.4.1.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.8", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw=="], + "@base-ui/react": ["@base-ui/react@1.5.0", "https://registry.npmmirror.com/@base-ui/react/-/react-1.5.0.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.9", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-z1gSAlced1yY+iM+mHDEtIkD8UI3Ebs52MuBPxvV6f5hRutk+xvCH/wuB7hDqDzK9JG5FoMz5nhrqtSs1wjt1A=="], - "@base-ui/utils": ["@base-ui/utils@0.2.8", "https://registry.npmmirror.com/@base-ui/utils/-/utils-0.2.8.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ=="], + "@base-ui/utils": ["@base-ui/utils@0.2.9", "https://registry.npmmirror.com/@base-ui/utils/-/utils-0.2.9.tgz", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-x/PDDCYzoqPpjrdyb3VcyylTI2IjUXEtYDGi5foh7KsnmNJIIaVwA2GLgDH1dps1GgXiJbA60hM+AyuTfQzIvw=="], - "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], - "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="], - - "@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="], - - "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="], - - "@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="], - - "@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="], + "@chevrotain/types": ["@chevrotain/types@11.1.2", "https://registry.npmmirror.com/@chevrotain/types/-/types-11.1.2.tgz", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "https://registry.npmmirror.com/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="], - "@date-fns/tz": ["@date-fns/tz@1.4.1", "https://registry.npmmirror.com/@date-fns/tz/-/tz-1.4.1.tgz", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], + "@date-fns/tz": ["@date-fns/tz@1.5.0", "https://registry.npmmirror.com/@date-fns/tz/-/tz-1.5.0.tgz", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="], - "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], - "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], + "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], - "@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="], + "@dnd-kit/modifiers": ["@dnd-kit/modifiers@9.0.0", "https://registry.npmmirror.com/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw=="], - "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], + "@dnd-kit/sortable": ["@dnd-kit/sortable@10.0.0", "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz", { "dependencies": { "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.3.0", "react": ">=16.8.0" } }, "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg=="], - "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], - "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.64.0", "https://registry.npmmirror.com/@dotenvx/dotenvx/-/dotenvx-1.64.0.tgz", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.4", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-6+xRpZaWuHXEqnhBjae+VmQI9Uaqw5Uzu/ScpO+W7ww9Zp3lHSNBoNjFcUxhrCyc7pRGQzyDjhKzloqrPHERiQ=="], + "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.68.0", "https://registry.npmmirror.com/@dotenvx/dotenvx/-/dotenvx-1.68.0.tgz", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "enquirer": "^2.4.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.4", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-YGApsPyDmonlqPFGd5d9Gp5O4JQjmdQsoLYtXHTZb0bhlaCCjj69lWbXKYucCplUgvsorQqc5RRqA+tynjb1lA=="], "@ecies/ciphers": ["@ecies/ciphers@0.2.6", "https://registry.npmmirror.com/@ecies/ciphers/-/ciphers-0.2.6.tgz", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="], - "@emoji-mart/data": ["@emoji-mart/data@1.2.1", "", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@emoji-mart/react": ["@emoji-mart/react@1.1.1", "", { "peerDependencies": { "emoji-mart": "^5.2", "react": "^16.8 || ^17 || ^18" } }, "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="], + "@emoji-mart/data": ["@emoji-mart/data@1.2.1", "https://registry.npmmirror.com/@emoji-mart/data/-/data-1.2.1.tgz", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="], - "@emotion/css": ["@emotion/css@11.13.5", "", { "dependencies": { "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2" } }, "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w=="], + "@emoji-mart/react": ["@emoji-mart/react@1.1.1", "https://registry.npmmirror.com/@emoji-mart/react/-/react-1.1.1.tgz", { "peerDependencies": { "emoji-mart": "^5.2", "react": "^16.8 || ^17 || ^18" } }, "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g=="], - "@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="], + "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="], - "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], + "@emotion/cache": ["@emotion/cache@11.14.0", "https://registry.npmmirror.com/@emotion/cache/-/cache-11.14.0.tgz", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="], - "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], + "@emotion/css": ["@emotion/css@11.13.5", "https://registry.npmmirror.com/@emotion/css/-/css-11.13.5.tgz", { "dependencies": { "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2" } }, "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w=="], - "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], + "@emotion/hash": ["@emotion/hash@0.8.0", "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="], - "@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="], + "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "https://registry.npmmirror.com/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="], - "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="], + "@emotion/memoize": ["@emotion/memoize@0.9.0", "https://registry.npmmirror.com/@emotion/memoize/-/memoize-0.9.0.tgz", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="], - "@emotion/unitless": ["@emotion/unitless@0.7.5", "", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="], + "@emotion/react": ["@emotion/react@11.14.0", "https://registry.npmmirror.com/@emotion/react/-/react-11.14.0.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="], - "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="], + "@emotion/serialize": ["@emotion/serialize@1.3.3", "https://registry.npmmirror.com/@emotion/serialize/-/serialize-1.3.3.tgz", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="], - "@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="], + "@emotion/sheet": ["@emotion/sheet@1.4.0", "https://registry.npmmirror.com/@emotion/sheet/-/sheet-1.4.0.tgz", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="], - "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], + "@emotion/unitless": ["@emotion/unitless@0.7.5", "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="], + + "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "https://registry.npmmirror.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="], + + "@emotion/utils": ["@emotion/utils@1.4.2", "https://registry.npmmirror.com/@emotion/utils/-/utils-1.4.2.tgz", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="], + + "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "https://registry.npmmirror.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], @@ -293,43 +298,39 @@ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/config-array": ["@eslint/config-array@0.21.2", "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.2.tgz", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + "@eslint/config-array": ["@eslint/config-array@0.23.5", "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.23.5.tgz", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], - "@eslint/core": ["@eslint/core@0.17.0", "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + "@eslint/core": ["@eslint/core@1.2.1", "https://registry.npmmirror.com/@eslint/core/-/core-1.2.1.tgz", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + "@eslint/js": ["@eslint/js@10.0.1", "https://registry.npmmirror.com/@eslint/js/-/js-10.0.1.tgz", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], - "@eslint/js": ["@eslint/js@9.39.4", "https://registry.npmmirror.com/@eslint/js/-/js-9.39.4.tgz", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-3.0.5.tgz", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], "@floating-ui/core": ["@floating-ui/core@1.7.5", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], - "@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="], + "@floating-ui/react": ["@floating-ui/react@0.27.19", "https://registry.npmmirror.com/@floating-ui/react/-/react-0.27.19.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="], "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], - "@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.8", "https://registry.npmmirror.com/@fontsource-variable/geist/-/geist-5.2.8.tgz", {}, "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw=="], + "@fontsource-variable/dm-sans": ["@fontsource-variable/dm-sans@5.2.8", "https://registry.npmmirror.com/@fontsource-variable/dm-sans/-/dm-sans-5.2.8.tgz", {}, "sha512-AxkvMTvNWgfrmlyjiV05vlHYJa+nRQCf1EfvIrQAPBpFJW0O9VTz7oAFr9S3lvbWdmnFoBk7yFqQL86u64nl2g=="], + + "@fontsource-variable/geist": ["@fontsource-variable/geist@5.2.9", "https://registry.npmmirror.com/@fontsource-variable/geist/-/geist-5.2.9.tgz", {}, "sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ=="], + + "@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.2.8", "https://registry.npmmirror.com/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", {}, "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q=="], + + "@fontsource-variable/space-grotesk": ["@fontsource-variable/space-grotesk@5.2.10", "https://registry.npmmirror.com/@fontsource-variable/space-grotesk/-/space-grotesk-5.2.10.tgz", {}, "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w=="], "@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.23.0", "https://registry.npmmirror.com/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz", { "dependencies": { "@shikijs/engine-oniguruma": "^3.23.0", "@shikijs/langs": "^3.23.0", "@shikijs/themes": "^3.23.0", "@shikijs/types": "^3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg=="], - "@giscus/react": ["@giscus/react@3.1.0", "", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="], - - "@grafana/faro-core": ["@grafana/faro-core@1.19.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/otlp-transformer": "^0.202.0" } }, "sha512-Juo5G/aviSh3XqSGGr6D61noAC8sb+oCawBsv545ILEeOQdINyzRaoQdRpnXEY3DLS9LYtL0PYhvHZiP3rlscQ=="], - - "@grafana/faro-react": ["@grafana/faro-react@1.19.0", "", { "dependencies": { "@grafana/faro-web-sdk": "^1.19.0", "@grafana/faro-web-tracing": "^1.19.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-router-dom": "^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["react-dom", "react-router-dom"] }, "sha512-3rrqxgDefvlaZ8753Wen8AxUmCKxOLYPspJMw60u4WvRSj/ki7XN3RjuRAc2AkuHm342jTs7EU0jwDBXAUWTgg=="], - - "@grafana/faro-web-sdk": ["@grafana/faro-web-sdk@1.19.0", "", { "dependencies": { "@grafana/faro-core": "^1.19.0", "ua-parser-js": "^1.0.32", "web-vitals": "^4.0.1" } }, "sha512-3u74mV2uBWqoF6WBx71p0vtkaS1Z0QbGoZ8tuX5yiYnIybqnhKdGkApFUi7q5se0tMPIeJdMVoRFdLU8f9hfAw=="], - - "@grafana/faro-web-tracing": ["@grafana/faro-web-tracing@1.19.0", "", { "dependencies": { "@grafana/faro-web-sdk": "^1.19.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.0", "@opentelemetry/exporter-trace-otlp-http": "^0.202.0", "@opentelemetry/instrumentation": "^0.202.0", "@opentelemetry/instrumentation-fetch": "^0.202.0", "@opentelemetry/instrumentation-xml-http-request": "^0.202.0", "@opentelemetry/otlp-transformer": "^0.202.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-trace-web": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.32.0" } }, "sha512-p37kQ/n8vU53ISCMvUn42upbGKqiRZI/QspHGx6kI7nDD5CJIl/2Byzg9vGDuw9BgMEOCIS8Oz6b5+U05kfKdQ=="], + "@giscus/react": ["@giscus/react@3.1.0", "https://registry.npmmirror.com/@giscus/react/-/react-3.1.0.tgz", { "dependencies": { "giscus": "^1.6.0" }, "peerDependencies": { "react": "^16 || ^17 || ^18 || ^19", "react-dom": "^16 || ^17 || ^18 || ^19" } }, "sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg=="], "@hono/node-server": ["@hono/node-server@1.19.14", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.14.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], @@ -343,15 +344,15 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + "@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - "@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], + "@iconify/utils": ["@iconify/utils@3.1.3", "https://registry.npmmirror.com/@iconify/utils/-/utils-3.1.3.tgz", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], "@inquirer/ansi": ["@inquirer/ansi@2.0.5", "https://registry.npmmirror.com/@inquirer/ansi/-/ansi-2.0.5.tgz", {}, "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw=="], - "@inquirer/confirm": ["@inquirer/confirm@6.0.12", "https://registry.npmmirror.com/@inquirer/confirm/-/confirm-6.0.12.tgz", { "dependencies": { "@inquirer/core": "^11.1.9", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og=="], + "@inquirer/confirm": ["@inquirer/confirm@6.0.13", "https://registry.npmmirror.com/@inquirer/confirm/-/confirm-6.0.13.tgz", { "dependencies": { "@inquirer/core": "^11.1.10", "@inquirer/type": "^4.0.5" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw=="], - "@inquirer/core": ["@inquirer/core@11.1.9", "https://registry.npmmirror.com/@inquirer/core/-/core-11.1.9.tgz", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg=="], + "@inquirer/core": ["@inquirer/core@11.1.10", "https://registry.npmmirror.com/@inquirer/core/-/core-11.1.10.tgz", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/figures": "^2.0.5", "@inquirer/type": "^4.0.5", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A=="], "@inquirer/figures": ["@inquirer/figures@2.0.5", "https://registry.npmmirror.com/@inquirer/figures/-/figures-2.0.5.tgz", {}, "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ=="], @@ -367,27 +368,33 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.5.1", "", {}, "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA=="], + "@lit-labs/ssr-dom-shim": ["@lit-labs/ssr-dom-shim@1.6.0", "https://registry.npmmirror.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", {}, "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ=="], - "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], + "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "https://registry.npmmirror.com/@lit/reactive-element/-/reactive-element-2.1.2.tgz", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], - "@lobehub/emojilib": ["@lobehub/emojilib@1.0.0", "", {}, "sha512-s9KnjaPjsEefaNv150G3aifvB+J3P4eEKG+epY9zDPS2BeB6+V2jELWqAZll+nkogMaVovjEE813z3V751QwGw=="], + "@lobehub/emojilib": ["@lobehub/emojilib@1.0.0", "https://registry.npmmirror.com/@lobehub/emojilib/-/emojilib-1.0.0.tgz", {}, "sha512-s9KnjaPjsEefaNv150G3aifvB+J3P4eEKG+epY9zDPS2BeB6+V2jELWqAZll+nkogMaVovjEE813z3V751QwGw=="], - "@lobehub/fluent-emoji": ["@lobehub/fluent-emoji@4.1.0", "", { "dependencies": { "@lobehub/emojilib": "^1.0.0", "antd-style": "^4.1.0", "emoji-regex": "^10.6.0", "es-toolkit": "^1.43.0", "lucide-react": "^0.562.0", "url-join": "^5.0.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-R1MB2lfUkDvB7XAQdRzY75c1dx/tB7gEvBPaEEMarzKfCJWmXm7rheS6caVzmgwAlq5sfmTbxPL+un99sp//Yw=="], + "@lobehub/fluent-emoji": ["@lobehub/fluent-emoji@4.1.0", "https://registry.npmmirror.com/@lobehub/fluent-emoji/-/fluent-emoji-4.1.0.tgz", { "dependencies": { "@lobehub/emojilib": "^1.0.0", "antd-style": "^4.1.0", "emoji-regex": "^10.6.0", "es-toolkit": "^1.43.0", "lucide-react": "^0.562.0", "url-join": "^5.0.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-R1MB2lfUkDvB7XAQdRzY75c1dx/tB7gEvBPaEEMarzKfCJWmXm7rheS6caVzmgwAlq5sfmTbxPL+un99sp//Yw=="], - "@lobehub/icons": ["@lobehub/icons@5.8.0", "", { "dependencies": { "antd-style": "^4.1.0", "es-toolkit": "^1.45.1", "lucide-react": "^0.469.0", "polished": "^4.3.1" }, "peerDependencies": { "@lobehub/ui": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-pt06WqZdIQzagevf+aX4MlEOrBtAPHAvo2pq3sIoRikzzUCND/zhnXG8pFPjoxhnkkvUCWXWm/8wjnFasufW7A=="], + "@lobehub/icons": ["@lobehub/icons@5.10.0", "https://registry.npmmirror.com/@lobehub/icons/-/icons-5.10.0.tgz", { "dependencies": { "antd-style": "^4.1.0", "es-toolkit": "^1.45.1", "lucide-react": "^0.469.0", "polished": "^4.3.1" }, "peerDependencies": { "@lobehub/ui": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-CIpjkISCLRK7haDtSugGFd0o3odaJts8ewJOkUiEFtns3xvsqbl8i24eowBnjw+yMDQVQyNONlhqTD58YC6Ljg=="], - "@lobehub/ui": ["@lobehub/ui@5.10.2", "", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@base-ui/react": "1.0.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.19", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.1.19", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^4.0.2", "@shikijs/transformers": "^4.0.2", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.7", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", "emoji-mart": "^5.6.0", "es-toolkit": "^1.46.0", "fast-deep-equal": "^3.1.3", "immer": "^11.1.4", "katex": "^0.16.45", "leva": "^0.10.1", "lucide-react": "^1.11.0", "marked": "^17.0.6", "mermaid": "^11.14.0", "motion": "^12.38.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^15.1.0", "react-error-boundary": "^6.1.1", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.3", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^2.0.1", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.3.0", "shiki": "^4.0.2", "shiki-stream": "^0.1.4", "swr": "^2.4.1", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.49.1" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-ozLKbvOXMgTg/SXt0frPu6HM+PjTu+KpHwoSlK7uqURHHs1ENRlzm3/KuniGZ/V0U5LFIJx2ybCWZaZblgvzKA=="], + "@lobehub/ui": ["@lobehub/ui@5.15.5", "https://registry.npmmirror.com/@lobehub/ui/-/ui-5.15.5.tgz", { "dependencies": { "@ant-design/cssinjs": "^2.1.2", "@base-ui/react": "1.5.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@emotion/is-prop-valid": "^1.4.0", "@floating-ui/react": "^0.27.19", "@giscus/react": "^3.1.0", "@mdx-js/mdx": "^3.1.1", "@mdx-js/react": "^3.1.1", "@pierre/diffs": "^1.1.19", "@radix-ui/react-slot": "^1.2.4", "@shikijs/core": "^4.0.2", "@shikijs/transformers": "^4.0.2", "@splinetool/runtime": "0.9.526", "ahooks": "^3.9.7", "antd-style": "^4.1.0", "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", "emoji-mart": "^5.6.0", "es-toolkit": "^1.46.0", "fast-deep-equal": "^3.1.3", "immer": "^11.1.4", "katex": "^0.16.45", "leva": "^0.10.1", "lucide-react": "^1.11.0", "marked": "^17.0.6", "mermaid": "^11.14.0", "motion": "^12.38.0", "numeral": "^2.0.6", "polished": "^4.3.1", "query-string": "^9.3.1", "rc-collapse": "^4.0.0", "rc-footer": "^0.6.8", "rc-image": "^7.12.0", "rc-input-number": "^9.5.0", "rc-menu": "^9.16.1", "re-resizable": "^6.11.2", "react-avatar-editor": "^15.1.0", "react-error-boundary": "^6.1.1", "react-hotkeys-hook": "^5.2.4", "react-markdown": "^10.1.0", "react-merge-refs": "^3.0.2", "react-rnd": "^10.5.3", "react-zoom-pan-pinch": "^3.7.0", "rehype-github-alerts": "^4.2.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-cjk-friendly": "^2.0.1", "remark-gfm": "^4.0.1", "remark-github": "^12.0.0", "remark-math": "^6.0.0", "remend": "^1.3.0", "shiki": "^4.0.2", "shiki-stream": "^0.1.4", "swr": "^2.4.1", "ts-md5": "^2.0.1", "unified": "^11.0.5", "url-join": "^5.0.0", "use-merge-value": "^1.2.0", "uuid": "^13.0.0", "virtua": "^0.49.1" }, "peerDependencies": { "@lobehub/fluent-emoji": "^4.0.0", "@lobehub/icons": "^5.0.0", "antd": "^6.1.1", "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-epJdmEpwo6CGbUGuDLz5cpEEdZ+Yl8XdZ4ApNuL02BWEgbyGzzus+kKXqqnA6Gyy7x5myIEuDafpQsctD/NlBg=="], - "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "https://registry.npmmirror.com/@mdx-js/mdx/-/mdx-3.1.1.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], - "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + "@mdx-js/react": ["@mdx-js/react@3.1.1", "https://registry.npmmirror.com/@mdx-js/react/-/react-3.1.1.tgz", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], - "@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-1.1.1.tgz", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], - "@mswjs/interceptors": ["@mswjs/interceptors@0.41.8", "https://registry.npmmirror.com/@mswjs/interceptors/-/interceptors-0.41.8.tgz", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.7.0.tgz", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "https://registry.npmmirror.com/@monaco-editor/react/-/react-4.7.0.tgz", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.9", "https://registry.npmmirror.com/@mswjs/interceptors/-/interceptors-0.41.9.tgz", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-1.3.0.tgz", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], @@ -407,99 +414,47 @@ "@open-draft/until": ["@open-draft/until@2.1.0", "https://registry.npmmirror.com/@open-draft/until/-/until-2.1.0.tgz", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.1.tgz", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="], + "@orval/angular": ["@orval/angular@8.12.3", "https://registry.npmmirror.com/@orval/angular/-/angular-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3" } }, "sha512-NKICGEFYivnKKJYKcI1iCuG37nEn08pia91bfucSf4Pe36e2MJsfIWdM27s/ly+M0mgLz6L28SCXWaIwnvShbQ=="], - "@opentelemetry/core": ["@opentelemetry/core@2.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw=="], + "@orval/axios": ["@orval/axios@8.12.3", "https://registry.npmmirror.com/@orval/axios/-/axios-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3" } }, "sha512-zl3PNqfJCucfQXdZzkRYaj0F6ktkZOoZbCTyzo5yWAmsZqKYrx2MdIe9uJCR++0xhhSCRuEenT4ZubeFA/bOvQ=="], - "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.202.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-exporter-base": "0.202.0", "@opentelemetry/otlp-transformer": "0.202.0", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-/hKE8DaFCJuaQqE1IxpgkcjOolUIwgi3TgHElPVKGdGRBSmJMTmN/cr6vWa55pCJIXPyhKvcMrbrya7DZ3VmzA=="], + "@orval/core": ["@orval/core@8.12.3", "https://registry.npmmirror.com/@orval/core/-/core-8.12.3.tgz", { "dependencies": { "@scalar/openapi-types": "0.8.0", "acorn": "^8.15.0", "compare-versions": "^6.1.1", "debug": "^4.4.3", "esbuild": "^0.27.4", "esutils": "2.0.3", "fs-extra": "^11.3.2", "jiti": "^2.6.1", "remeda": "^2.33.6", "tinyglobby": "^0.2.16", "typedoc": "^0.28.19" }, "peerDependencies": { "@faker-js/faker": ">=10" }, "optionalPeers": ["@faker-js/faker"] }, "sha512-Y0Spgcy9EvBQfdBg0UIBsh1f5Rvu5rGFwa/fuhrZwxfXXsK8hVAYJuMKfzj97y18xqu5amx10FEpzb3DZAkfUQ=="], - "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.202.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.202.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Uz3BxZWPgDwgHM2+vCKEQRh0R8WKrd/q6Tus1vThRClhlPO39Dyz7mDrOr6KuqGXAlBQ1e5Tnymzri4RMZNaWA=="], + "@orval/fetch": ["@orval/fetch@8.12.3", "https://registry.npmmirror.com/@orval/fetch/-/fetch-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3", "@scalar/openapi-types": "0.8.0" } }, "sha512-BfY0rWpaUFQ4O5Dp5dVTfSuihzEuqDSBxRijSsQUAJa0ML/botXiVMWmqgyuszXDzNvVpxNao/vrIAoteYO9fQ=="], - "@opentelemetry/instrumentation-fetch": ["@opentelemetry/instrumentation-fetch@0.202.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/instrumentation": "0.202.0", "@opentelemetry/sdk-trace-web": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RlLgOJAKs9cQIRXPoLnS6YG8CeQt1gR+WJpzthQlqt4hdgNmfnyB7zZrg1yddECF0K2lPGBqF4s+IqjA4dy3JQ=="], + "@orval/hono": ["@orval/hono@8.12.3", "https://registry.npmmirror.com/@orval/hono/-/hono-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3", "@orval/zod": "8.12.3", "fs-extra": "^11.3.2", "remeda": "^2.33.6" } }, "sha512-ZDTWOBn4UJKJasaETXZuuja8KzUPOuqpzcbZ3KhVVwSolWqMvDQA/zqsYeXcPpQua38dO2Z4XRBThV2bKIduGQ=="], - "@opentelemetry/instrumentation-xml-http-request": ["@opentelemetry/instrumentation-xml-http-request@0.202.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/instrumentation": "0.202.0", "@opentelemetry/sdk-trace-web": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-N0wZyWpdUviscnhNKbRr2mEEfTtVi0ki7v6Lr9ZsK5mUtg12e9Mf/LsT2Msl7tvyGDGGi8Tmm8ssFrfLOADqDw=="], + "@orval/mcp": ["@orval/mcp@8.12.3", "https://registry.npmmirror.com/@orval/mcp/-/mcp-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3", "@orval/fetch": "8.12.3", "@orval/zod": "8.12.3" } }, "sha512-JN9wZEJg5QiAlk1W9wnk4x4AvMMOz9fO2IC8+Df106TpzOCK7Cw0am/JGCUmokrpRK+t1Iok9iXoUpb8jzKwnA=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.202.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.202.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nMEOzel+pUFYuBJg2znGmHJWbmvMbdX5/RhoKNKowguMbURhz0fwik5tUKplLcUtl8wKPL1y9zPnPxeBn65N0Q=="], + "@orval/mock": ["@orval/mock@8.12.3", "https://registry.npmmirror.com/@orval/mock/-/mock-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3", "remeda": "^2.33.6" } }, "sha512-2lUxUFk0ZiIiM7OrCV+wcF4F4BaZzPXcKDbLw2c0ojZO3YsoFXKyYSYIS/iGCCAsBsxQ9cV8fwwi+/s6CrggMg=="], - "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.202.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.202.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/sdk-logs": "0.202.0", "@opentelemetry/sdk-metrics": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-5XO77QFzs9WkexvJQL9ksxL8oVFb/dfi9NWQSq7Sv0Efr9x3N+nb1iklP1TeVgxqJ7m1xWiC/Uv3wupiQGevMw=="], + "@orval/query": ["@orval/query@8.12.3", "https://registry.npmmirror.com/@orval/query/-/query-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3", "@orval/fetch": "8.12.3", "remeda": "^2.33.6" } }, "sha512-aIjxqfBPfEw5CNU7qO/GY6WsgWO0h5tRAgbAG+wJ5QksD4sgB3VoX19v/0rEiSBKCGXvC5fCRcn2MTrQ2AMDCA=="], - "@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], + "@orval/solid-start": ["@orval/solid-start@8.12.3", "https://registry.npmmirror.com/@orval/solid-start/-/solid-start-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3", "@scalar/openapi-types": "0.8.0" } }, "sha512-65/WtaBLQvuzO2u/wT0/iKi7/VHF7nXeQ5j5rYHhdKL9XaAaiEKim6kbmlpiBDTdAZI9bIXa8JnW8ScBBV3DaA=="], - "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.202.0", "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-pv8QiQLQzk4X909YKm0lnW4hpuQg4zHwJ4XBd5bZiXcd9urvrJNoNVKnxGHPiDVX/GiLFvr5DMYsDBQbZCypRQ=="], + "@orval/swr": ["@orval/swr@8.12.3", "https://registry.npmmirror.com/@orval/swr/-/swr-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3", "@orval/fetch": "8.12.3" } }, "sha512-WRBYmcn++Kfcp/wHIv6WsqxYt7tl72zpoAHq9UShA8s3kW48StsAFbf8TXVvwsbh6asa5IaNuqvil3tR3dzumw=="], - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g=="], + "@orval/zod": ["@orval/zod@8.12.3", "https://registry.npmmirror.com/@orval/zod/-/zod-8.12.3.tgz", { "dependencies": { "@orval/core": "8.12.3", "remeda": "^2.33.6" } }, "sha512-DZk8ihSJWGtkbPqjgdKKLIsDyXqABXjBjHEOQWmhj5ISpyep+ZJboLHdjjiwa/6muppUE0jfoHD9UkH4X877Sw=="], - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], + "@oxc-project/types": ["@oxc-project/types@0.132.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.132.0.tgz", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="], - "@opentelemetry/sdk-trace-web": ["@opentelemetry/sdk-trace-web@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ=="], + "@pierre/diffs": ["@pierre/diffs@1.2.4", "https://registry.npmmirror.com/@pierre/diffs/-/diffs-1.2.4.tgz", { "dependencies": { "@pierre/theme": "1.0.3", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-SEuYxGpSCHVvfoLly/Q/OYpJSBLWaVLV3M3wI/VBW7aZmzYenNe4aXjOf5sIKJMWW5gbZe9WdLvtKUt6cQ1k1A=="], - "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="], + "@pierre/theme": ["@pierre/theme@1.0.3", "https://registry.npmmirror.com/@pierre/theme/-/theme-1.0.3.tgz", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="], - "@orval/angular": ["@orval/angular@8.9.0", "https://registry.npmmirror.com/@orval/angular/-/angular-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0" } }, "sha512-jYepvb8v4xHeJZskaqi8i+V7zp+rha+sK/A2Inyf10Gt7OoaX0G3P/wa3G6/K6nw4TfiLj5KKdIdG+c4X/luOw=="], - - "@orval/axios": ["@orval/axios@8.9.0", "https://registry.npmmirror.com/@orval/axios/-/axios-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0" } }, "sha512-Qumi+SsjwphVtm65I5XW7tf7itvytcz8eW6aZElaybZRBrIIC9i62jTGYIDmY/loI8jSLublxEU6CmaSUyah2A=="], - - "@orval/core": ["@orval/core@8.9.0", "https://registry.npmmirror.com/@orval/core/-/core-8.9.0.tgz", { "dependencies": { "@scalar/openapi-types": "0.8.0", "acorn": "^8.15.0", "compare-versions": "^6.1.1", "debug": "^4.4.3", "esbuild": "^0.27.4", "esutils": "2.0.3", "fs-extra": "^11.3.2", "globby": "16.1.0", "jiti": "^2.6.1", "remeda": "^2.33.6", "typedoc": "^0.28.17" }, "peerDependencies": { "@faker-js/faker": ">=10" }, "optionalPeers": ["@faker-js/faker"] }, "sha512-kcwr5ePPSaSqmFKWRI2skPd6MgPSdWk7NIfVaj61CdK5Q5agA173iUypdOwLif1xPF03nVvCdic3FZc77/XsMg=="], - - "@orval/fetch": ["@orval/fetch@8.9.0", "https://registry.npmmirror.com/@orval/fetch/-/fetch-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0", "@scalar/openapi-types": "0.8.0" } }, "sha512-nFWeKh0iExCgo/yyx8XLr+PBgVteAoePF+GiQdiiqyEuM+Jd+aFg6nzHYoD9C/WYhnxoLHDfM/57EjHBRgpNtQ=="], - - "@orval/hono": ["@orval/hono@8.9.0", "https://registry.npmmirror.com/@orval/hono/-/hono-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0", "@orval/zod": "8.9.0", "fs-extra": "^11.3.2", "remeda": "^2.33.6" } }, "sha512-HU6xbeG4CYA/gFpBoqT2x6lM81gckzjhQDE2/YP5BESjTOGcHsz/Ngg1kDh0dNyxT5Z0BrW3preqb+7imtb8Lw=="], - - "@orval/mcp": ["@orval/mcp@8.9.0", "https://registry.npmmirror.com/@orval/mcp/-/mcp-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0", "@orval/fetch": "8.9.0", "@orval/zod": "8.9.0" } }, "sha512-/jNEqnU6tny+9Y2b+rA0RwMqgAZOBEUXN/S18gnelJOe66jydCrEN89GXz9Tropmu0y4LleUwRCx1uo0pBhQ0w=="], - - "@orval/mock": ["@orval/mock@8.9.0", "https://registry.npmmirror.com/@orval/mock/-/mock-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0", "remeda": "^2.33.6" } }, "sha512-wJUG3qgORn4LfUJqoZLvMBCYAwXF2zIf+uADv42TF2wxQ80cw1uzlIb+jLTqdBiV6G4jJLGxFarP9RiWLogf8w=="], - - "@orval/query": ["@orval/query@8.9.0", "https://registry.npmmirror.com/@orval/query/-/query-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0", "@orval/fetch": "8.9.0", "remeda": "^2.33.6" } }, "sha512-lwuqA+pwDX8EAc505tOKpDU7ayN+TzzLCgk4J2EXbRPvQvqgh0G8lJVdMYLKFxcDa9wzuxoV05SV50mkd7CsgA=="], - - "@orval/solid-start": ["@orval/solid-start@8.9.0", "https://registry.npmmirror.com/@orval/solid-start/-/solid-start-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0", "@scalar/openapi-types": "0.8.0" } }, "sha512-9BtRFi5ZzB65mYc+/Ip/4NVZ+vonC+v7ikH7wPo1W0SOsvVo8eE7GwoeoGK44yZXdhDn3fKeQZvYOpa38qNI7w=="], - - "@orval/swr": ["@orval/swr@8.9.0", "https://registry.npmmirror.com/@orval/swr/-/swr-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0", "@orval/fetch": "8.9.0" } }, "sha512-z+OwrcibBDw8TEKwQd9DtlX4Ub469fxm1BrouSkkxry3b5zntGEAobOQYnCLkYDn/KUjJEJSc7fJHRWX1lV1sg=="], - - "@orval/zod": ["@orval/zod@8.9.0", "https://registry.npmmirror.com/@orval/zod/-/zod-8.9.0.tgz", { "dependencies": { "@orval/core": "8.9.0", "remeda": "^2.33.6" } }, "sha512-KZ/JSDHIlt//ErwVqzw5DxjxUYoOq4wj1MFfuHMUBTJL9cPlXbyChgWE/rEMYOJI4i1zLjNSJfcQASuQad3zDw=="], - - "@pierre/diffs": ["@pierre/diffs@1.1.20", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-lLi+3sLCm3QDd5/aLO9pw+WbF6UzhrkWm2oTZ5WZJTGemOyUNRJ4DDhcEKmVusu4C4bXx9Nssh6fF+wQcapb5w=="], - - "@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="], - - "@primer/octicons": ["@primer/octicons@19.25.0", "", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-E0eMV8nXexrs7Vro7PdS8v/JfvvYCMh8HN6CXJ9l8fk9atZaY05fVUcyiAh5KjEJu7IxdFy4URfHGpM7+iOl1A=="], - - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/inquire": ["@protobufjs/inquire@1.1.1", "", {}, "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="], + "@primer/octicons": ["@primer/octicons@19.27.0", "https://registry.npmmirror.com/@primer/octicons/-/octicons-19.27.0.tgz", { "dependencies": { "object-assign": "^4.1.1" } }, "sha512-7xC6D89f9IcoDezeKTGETbgRAoXJnbZlakavqYzD4Wo+uTC6212k0fTE/dLV8WCDOwfp//WyONftdaFRdI1VdQ=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="], - "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "https://registry.npmmirror.com/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], - "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], - "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], - "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="], - - "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "https://registry.npmmirror.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], @@ -511,8 +466,6 @@ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "https://registry.npmmirror.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="], - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], @@ -525,24 +478,12 @@ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], - "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "https://registry.npmmirror.com/@radix-ui/react-form/-/react-form-0.1.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="], - - "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "https://registry.npmmirror.com/@radix-ui/react-label/-/react-label-2.1.8.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], - "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "https://registry.npmmirror.com/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="], - - "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "https://registry.npmmirror.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], - - "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "https://registry.npmmirror.com/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="], - - "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "https://registry.npmmirror.com/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="], - "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], @@ -553,8 +494,6 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="], - "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="], "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], @@ -563,24 +502,16 @@ "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.6.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], - "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "https://registry.npmmirror.com/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="], - - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], - "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="], - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "https://registry.npmmirror.com/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], - "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], - - "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="], - "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], @@ -605,149 +536,133 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="], + "@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.1.0.tgz", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="], - "@rc-component/cascader": ["@rc-component/cascader@1.14.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ=="], + "@rc-component/cascader": ["@rc-component/cascader@1.15.0", "https://registry.npmmirror.com/@rc-component/cascader/-/cascader-1.15.0.tgz", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw=="], - "@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="], + "@rc-component/checkbox": ["@rc-component/checkbox@2.0.0", "https://registry.npmmirror.com/@rc-component/checkbox/-/checkbox-2.0.0.tgz", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ=="], - "@rc-component/collapse": ["@rc-component/collapse@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw=="], + "@rc-component/collapse": ["@rc-component/collapse@1.2.0", "https://registry.npmmirror.com/@rc-component/collapse/-/collapse-1.2.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw=="], - "@rc-component/color-picker": ["@rc-component/color-picker@3.1.1", "", { "dependencies": { "@ant-design/fast-color": "^3.0.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg=="], + "@rc-component/color-picker": ["@rc-component/color-picker@3.1.1", "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-3.1.1.tgz", { "dependencies": { "@ant-design/fast-color": "^3.0.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg=="], - "@rc-component/context": ["@rc-component/context@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="], + "@rc-component/context": ["@rc-component/context@2.0.1", "https://registry.npmmirror.com/@rc-component/context/-/context-2.0.1.tgz", { "dependencies": { "@rc-component/util": "^1.3.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw=="], - "@rc-component/dialog": ["@rc-component/dialog@1.8.4", "", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw=="], + "@rc-component/dialog": ["@rc-component/dialog@1.9.0", "https://registry.npmmirror.com/@rc-component/dialog/-/dialog-1.9.0.tgz", { "dependencies": { "@rc-component/motion": "^1.1.3", "@rc-component/portal": "^2.1.0", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A=="], - "@rc-component/drawer": ["@rc-component/drawer@1.4.2", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="], + "@rc-component/drawer": ["@rc-component/drawer@1.4.2", "https://registry.npmmirror.com/@rc-component/drawer/-/drawer-1.4.2.tgz", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.1.3", "@rc-component/util": "^1.9.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q=="], - "@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="], + "@rc-component/dropdown": ["@rc-component/dropdown@1.0.2", "https://registry.npmmirror.com/@rc-component/dropdown/-/dropdown-1.0.2.tgz", { "dependencies": { "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg=="], - "@rc-component/form": ["@rc-component/form@1.8.1", "", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.6.2", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8O7TB55Fi2mWIGvSnwZjk8jFqVNYyKDAswglwGShcbndxqzKz4cHwNtNaLjZlAeRge9wcB0LL8IWsC/Bl18raQ=="], + "@rc-component/form": ["@rc-component/form@1.8.2", "https://registry.npmmirror.com/@rc-component/form/-/form-1.8.2.tgz", { "dependencies": { "@rc-component/async-validator": "^5.1.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ZidCvOLmM9Xr+3vzk4UAoR7Aj1W/5IHyrzlBB7sNkygpTeRVrohQSo4TN7W/nARTH+nt8zSAPsn4BEl4zLEO2g=="], - "@rc-component/image": ["@rc-component/image@1.9.0", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ=="], + "@rc-component/image": ["@rc-component/image@1.9.0", "https://registry.npmmirror.com/@rc-component/image/-/image-1.9.0.tgz", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ=="], - "@rc-component/input": ["@rc-component/input@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg=="], + "@rc-component/input": ["@rc-component/input@1.3.1", "https://registry.npmmirror.com/@rc-component/input/-/input-1.3.1.tgz", { "dependencies": { "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig=="], - "@rc-component/input-number": ["@rc-component/input-number@1.6.2", "", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="], + "@rc-component/input-number": ["@rc-component/input-number@1.6.2", "https://registry.npmmirror.com/@rc-component/input-number/-/input-number-1.6.2.tgz", { "dependencies": { "@rc-component/mini-decimal": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w=="], - "@rc-component/mentions": ["@rc-component/mentions@1.6.0", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/menu": "~1.2.0", "@rc-component/textarea": "~1.1.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ=="], + "@rc-component/mentions": ["@rc-component/mentions@1.9.0", "https://registry.npmmirror.com/@rc-component/mentions/-/mentions-1.9.0.tgz", { "dependencies": { "@rc-component/input": "~1.3.0", "@rc-component/menu": "~1.3.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g=="], - "@rc-component/menu": ["@rc-component/menu@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg=="], + "@rc-component/menu": ["@rc-component/menu@1.3.1", "https://registry.npmmirror.com/@rc-component/menu/-/menu-1.3.1.tgz", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-pSZl9nBPgKgxN0aaW7NilIBEwWsc+43S+ulGdWAg9afak96dNOGWsGx0DLLBB1VQsAJvo6bQMTDzXoPlEHsBEw=="], - "@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="], + "@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="], - "@rc-component/motion": ["@rc-component/motion@1.3.2", "", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ=="], + "@rc-component/motion": ["@rc-component/motion@1.3.2", "https://registry.npmmirror.com/@rc-component/motion/-/motion-1.3.2.tgz", { "dependencies": { "@rc-component/util": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ=="], - "@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="], + "@rc-component/mutate-observer": ["@rc-component/mutate-observer@2.0.1", "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w=="], - "@rc-component/notification": ["@rc-component/notification@1.2.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA=="], + "@rc-component/notification": ["@rc-component/notification@2.0.7", "https://registry.npmmirror.com/@rc-component/notification/-/notification-2.0.7.tgz", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ=="], - "@rc-component/overflow": ["@rc-component/overflow@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA=="], + "@rc-component/overflow": ["@rc-component/overflow@1.0.1", "https://registry.npmmirror.com/@rc-component/overflow/-/overflow-1.0.1.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA=="], - "@rc-component/pagination": ["@rc-component/pagination@1.2.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="], + "@rc-component/pagination": ["@rc-component/pagination@1.2.0", "https://registry.npmmirror.com/@rc-component/pagination/-/pagination-1.2.0.tgz", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw=="], - "@rc-component/picker": ["@rc-component/picker@1.9.1", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g=="], + "@rc-component/picker": ["@rc-component/picker@1.10.0", "https://registry.npmmirror.com/@rc-component/picker/-/picker-1.10.0.tgz", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/trigger": "^3.6.15", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w=="], - "@rc-component/portal": ["@rc-component/portal@1.1.2", "", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="], + "@rc-component/portal": ["@rc-component/portal@1.1.2", "https://registry.npmmirror.com/@rc-component/portal/-/portal-1.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="], - "@rc-component/progress": ["@rc-component/progress@1.0.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ=="], + "@rc-component/progress": ["@rc-component/progress@1.0.2", "https://registry.npmmirror.com/@rc-component/progress/-/progress-1.0.2.tgz", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ=="], - "@rc-component/qrcode": ["@rc-component/qrcode@1.1.1", "", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA=="], + "@rc-component/qrcode": ["@rc-component/qrcode@1.1.1", "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.1.1.tgz", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA=="], - "@rc-component/rate": ["@rc-component/rate@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw=="], + "@rc-component/rate": ["@rc-component/rate@1.0.1", "https://registry.npmmirror.com/@rc-component/rate/-/rate-1.0.1.tgz", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw=="], - "@rc-component/resize-observer": ["@rc-component/resize-observer@1.1.2", "", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q=="], + "@rc-component/resize-observer": ["@rc-component/resize-observer@1.1.2", "https://registry.npmmirror.com/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", { "dependencies": { "@rc-component/util": "^1.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q=="], - "@rc-component/segmented": ["@rc-component/segmented@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg=="], + "@rc-component/segmented": ["@rc-component/segmented@1.3.0", "https://registry.npmmirror.com/@rc-component/segmented/-/segmented-1.3.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@rc-component/motion": "^1.1.4", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg=="], - "@rc-component/select": ["@rc-component/select@1.6.15", "", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g=="], + "@rc-component/select": ["@rc-component/select@1.6.15", "https://registry.npmmirror.com/@rc-component/select/-/select-1.6.15.tgz", { "dependencies": { "@rc-component/overflow": "^1.0.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.3.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g=="], - "@rc-component/slider": ["@rc-component/slider@1.0.1", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g=="], + "@rc-component/slider": ["@rc-component/slider@1.0.1", "https://registry.npmmirror.com/@rc-component/slider/-/slider-1.0.1.tgz", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g=="], - "@rc-component/steps": ["@rc-component/steps@1.2.2", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw=="], + "@rc-component/steps": ["@rc-component/steps@1.2.2", "https://registry.npmmirror.com/@rc-component/steps/-/steps-1.2.2.tgz", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw=="], - "@rc-component/switch": ["@rc-component/switch@1.0.3", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="], + "@rc-component/switch": ["@rc-component/switch@1.0.3", "https://registry.npmmirror.com/@rc-component/switch/-/switch-1.0.3.tgz", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw=="], - "@rc-component/table": ["@rc-component/table@1.9.1", "", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.1.0", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-FVI5ZS/GdB3BcgexfCYKi3iHhZS3Fr59EtsxORszYGrfpH1eWr33eDNSYkVfLI6tfJ7vftJDd9D5apfFWqkdJg=="], + "@rc-component/table": ["@rc-component/table@1.10.2", "https://registry.npmmirror.com/@rc-component/table/-/table-1.10.2.tgz", { "dependencies": { "@rc-component/context": "^2.0.1", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-b3PjqB9Gp25p5t/zq+9QrbXbodkptT8/zvLmwgd2FNPUUtaYyDnQqfxeD5a7ao8E8lpinLHsi2u2vdfPhyNvAw=="], - "@rc-component/tabs": ["@rc-component/tabs@1.7.0", "", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w=="], + "@rc-component/tabs": ["@rc-component/tabs@1.9.1", "https://registry.npmmirror.com/@rc-component/tabs/-/tabs-1.9.1.tgz", { "dependencies": { "@rc-component/dropdown": "~1.0.0", "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.1.3", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6mY08Fce6aNOHuGsxbzT+f2ekgL9mg1cGGHkittMlVGymjGg+kGupu5v90sRxcUd/paRU9jclLLXtF/PkK1FUA=="], - "@rc-component/textarea": ["@rc-component/textarea@1.1.2", "", { "dependencies": { "@rc-component/input": "~1.1.0", "@rc-component/resize-observer": "^1.0.0", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A=="], + "@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "https://registry.npmmirror.com/@rc-component/tooltip/-/tooltip-1.4.0.tgz", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="], - "@rc-component/tooltip": ["@rc-component/tooltip@1.4.0", "", { "dependencies": { "@rc-component/trigger": "^3.7.1", "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg=="], + "@rc-component/tour": ["@rc-component/tour@2.4.0", "https://registry.npmmirror.com/@rc-component/tour/-/tour-2.4.0.tgz", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg=="], - "@rc-component/tour": ["@rc-component/tour@2.3.0", "", { "dependencies": { "@rc-component/portal": "^2.2.0", "@rc-component/trigger": "^3.0.0", "@rc-component/util": "^1.7.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-K04K9r32kUC+auBSQfr+Fss4SpSIS9JGe56oq/ALAX0p+i2ylYOI1MgR83yBY7v96eO6ZFXcM/igCQmubps0Ow=="], + "@rc-component/tree": ["@rc-component/tree@1.3.2", "https://registry.npmmirror.com/@rc-component/tree/-/tree-1.3.2.tgz", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.11.1", "@rc-component/virtual-list": "^1.2.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA=="], - "@rc-component/tree": ["@rc-component/tree@1.2.4", "", { "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/util": "^1.8.1", "@rc-component/virtual-list": "^1.0.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-5Gli43+m4R7NhpYYz3Z61I6LOw9yI6CNChxgVtvrO6xB1qML7iE6QMLVMB3+FTjo2yF6uFdAHtqWPECz/zbX5w=="], + "@rc-component/tree-select": ["@rc-component/tree-select@1.9.0", "https://registry.npmmirror.com/@rc-component/tree-select/-/tree-select-1.9.0.tgz", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.3.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w=="], - "@rc-component/tree-select": ["@rc-component/tree-select@1.8.0", "", { "dependencies": { "@rc-component/select": "~1.6.0", "@rc-component/tree": "~1.2.0", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ=="], + "@rc-component/trigger": ["@rc-component/trigger@3.9.0", "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-3.9.0.tgz", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="], - "@rc-component/trigger": ["@rc-component/trigger@3.9.0", "", { "dependencies": { "@rc-component/motion": "^1.1.4", "@rc-component/portal": "^2.2.0", "@rc-component/resize-observer": "^1.1.1", "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg=="], + "@rc-component/upload": ["@rc-component/upload@1.1.1", "https://registry.npmmirror.com/@rc-component/upload/-/upload-1.1.1.tgz", { "dependencies": { "@rc-component/util": "^1.11.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA=="], - "@rc-component/upload": ["@rc-component/upload@1.1.0", "", { "dependencies": { "@rc-component/util": "^1.3.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw=="], + "@rc-component/util": ["@rc-component/util@1.11.1", "https://registry.npmmirror.com/@rc-component/util/-/util-1.11.1.tgz", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg=="], - "@rc-component/util": ["@rc-component/util@1.10.1", "", { "dependencies": { "is-mobile": "^5.0.0", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng=="], + "@rc-component/virtual-list": ["@rc-component/virtual-list@1.2.0", "https://registry.npmmirror.com/@rc-component/virtual-list/-/virtual-list-1.2.0.tgz", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-iavRm1Jo4GDbASQwdGa7jFyk93RvSOo9xHyBT4QL1pgFJj/Fdf1G+3RErH7/7BmAMvx2AkF62mjGYxDbXsK9TQ=="], - "@rc-component/virtual-list": ["@rc-component/virtual-list@1.0.2", "", { "dependencies": { "@babel/runtime": "^7.20.0", "@rc-component/resize-observer": "^1.0.1", "@rc-component/util": "^1.4.0", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ=="], + "@react-dnd/asap": ["@react-dnd/asap@4.0.1", "https://registry.npmmirror.com/@react-dnd/asap/-/asap-4.0.1.tgz", {}, "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg=="], - "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + "@react-dnd/invariant": ["@react-dnd/invariant@2.0.0", "https://registry.npmmirror.com/@react-dnd/invariant/-/invariant-2.0.0.tgz", {}, "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + "@react-dnd/shallowequal": ["@react-dnd/shallowequal@2.0.0", "https://registry.npmmirror.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", {}, "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="], + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="], + "@scalar/helpers": ["@scalar/helpers@0.8.0", "https://registry.npmmirror.com/@scalar/helpers/-/helpers-0.8.0.tgz", {}, "sha512-gmOC6VravNB9VDl6wnt/GOj4K/hn48tj5bpW4AM4MhH8Ubil6uu7g1DSoKHwltu8Ks79KEtR6JmOrROi9R7jaQ=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="], - - "@scalar/helpers": ["@scalar/helpers@0.5.5", "https://registry.npmmirror.com/@scalar/helpers/-/helpers-0.5.5.tgz", {}, "sha512-TqbErkqGijJJW6Ad8lpswHFTmlJaYFt9oolaWF1/Nn9171xdkK9ZDAk3pCaOCWs8WpwvcbUSxIVD8JxdBg0MFg=="], - - "@scalar/json-magic": ["@scalar/json-magic@0.12.11", "https://registry.npmmirror.com/@scalar/json-magic/-/json-magic-0.12.11.tgz", { "dependencies": { "@scalar/helpers": "0.5.5", "pathe": "^2.0.3", "yaml": "^2.8.3" } }, "sha512-oyICpNyEjBsRHM1DPbS8uyXFnU8fulFhqrMANLTTggSCH1XFNbVs7PMA8HxFLuQzJiQZzpUFf19M/Dx/TpXnpQ=="], + "@scalar/json-magic": ["@scalar/json-magic@0.12.14", "https://registry.npmmirror.com/@scalar/json-magic/-/json-magic-0.12.14.tgz", { "dependencies": { "@scalar/helpers": "0.8.0", "pathe": "^2.0.3", "yaml": "^2.8.3" } }, "sha512-dWrCy3ew1r7OQ1pu2r4ZjiKEVy0yVd66kXdmsl41bteOG2F2I2IBlPjmPV6p8ckjImQHxtNBIntFaQfNrdBhJg=="], "@scalar/openapi-parser": ["@scalar/openapi-parser@0.25.12", "https://registry.npmmirror.com/@scalar/openapi-parser/-/openapi-parser-0.25.12.tgz", { "dependencies": { "@scalar/helpers": "0.5.2", "@scalar/json-magic": "0.12.8", "@scalar/openapi-types": "0.8.0", "@scalar/openapi-upgrader": "0.2.6", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.8.0" } }, "sha512-1hajBAbc7cbEcsSZEQxaPXZyCjMf6h6hObV+SO32jkC6rrxinPXQIucDu9HTu/jm/FaaMnNhc8/XDWz5/E49cQ=="], @@ -757,21 +672,21 @@ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - "@shikijs/core": ["@shikijs/core@4.0.2", "", { "dependencies": { "@shikijs/primitive": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw=="], + "@shikijs/core": ["@shikijs/core@4.1.0", "https://registry.npmmirror.com/@shikijs/core/-/core-4.1.0.tgz", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="], - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-4.1.0.tgz", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="], - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-4.1.0.tgz", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="], - "@shikijs/langs": ["@shikijs/langs@3.23.0", "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + "@shikijs/langs": ["@shikijs/langs@4.1.0", "https://registry.npmmirror.com/@shikijs/langs/-/langs-4.1.0.tgz", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="], - "@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="], + "@shikijs/primitive": ["@shikijs/primitive@4.1.0", "https://registry.npmmirror.com/@shikijs/primitive/-/primitive-4.1.0.tgz", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="], - "@shikijs/themes": ["@shikijs/themes@3.23.0", "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + "@shikijs/themes": ["@shikijs/themes@4.1.0", "https://registry.npmmirror.com/@shikijs/themes/-/themes-4.1.0.tgz", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="], - "@shikijs/transformers": ["@shikijs/transformers@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/types": "4.0.2" } }, "sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg=="], + "@shikijs/transformers": ["@shikijs/transformers@4.1.0", "https://registry.npmmirror.com/@shikijs/transformers/-/transformers-4.1.0.tgz", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/types": "4.1.0" } }, "sha512-YbuOcAA3kwqKDU9YSt00dtFLrY5lBXjKU3dWaMATyEyPSqBm9Jqblk/uVICxz7lcjwAHzYaEvIiMWX3mTpogkA=="], - "@shikijs/types": ["@shikijs/types@3.23.0", "https://registry.npmmirror.com/@shikijs/types/-/types-3.23.0.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@shikijs/types": ["@shikijs/types@4.1.0", "https://registry.npmmirror.com/@shikijs/types/-/types-4.1.0.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "https://registry.npmmirror.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], @@ -779,193 +694,173 @@ "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], - "@splinetool/runtime": ["@splinetool/runtime@0.9.526", "", { "dependencies": { "on-change": "^4.0.0", "semver-compare": "^1.0.0" } }, "sha512-qznHbXA5aKwDbCgESAothCNm1IeEZcmNWG145p5aXj4w5uoqR1TZ9qkTHTKLTsUbHeitCwdhzmRqan1kxboLgQ=="], + "@splinetool/runtime": ["@splinetool/runtime@0.9.526", "https://registry.npmmirror.com/@splinetool/runtime/-/runtime-0.9.526.tgz", { "dependencies": { "on-change": "^4.0.0", "semver-compare": "^1.0.0" } }, "sha512-qznHbXA5aKwDbCgESAothCNm1IeEZcmNWG145p5aXj4w5uoqR1TZ9qkTHTKLTsUbHeitCwdhzmRqan1kxboLgQ=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@stitches/react": ["@stitches/react@1.2.8", "", { "peerDependencies": { "react": ">= 16.3.0" } }, "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="], + "@stitches/react": ["@stitches/react@1.2.8", "https://registry.npmmirror.com/@stitches/react/-/react-1.2.8.tgz", { "peerDependencies": { "react": ">= 16.3.0" } }, "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="], - "@streamdown/cjk": ["@streamdown/cjk@1.0.3", "", { "dependencies": { "remark-cjk-friendly": "^2.0.1", "remark-cjk-friendly-gfm-strikethrough": "^2.0.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-WRg8HR/gHbBoTgsMd91OKFUClIoDcEFVofJvluvEAyjx3KpU0aGgD9tGDqHkHj14ShoMSkX0IYetWGegTcwIJw=="], + "@streamdown/cjk": ["@streamdown/cjk@1.0.3", "https://registry.npmmirror.com/@streamdown/cjk/-/cjk-1.0.3.tgz", { "dependencies": { "remark-cjk-friendly": "^2.0.1", "remark-cjk-friendly-gfm-strikethrough": "^2.0.1", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-WRg8HR/gHbBoTgsMd91OKFUClIoDcEFVofJvluvEAyjx3KpU0aGgD9tGDqHkHj14ShoMSkX0IYetWGegTcwIJw=="], - "@streamdown/code": ["@streamdown/code@1.1.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg=="], + "@streamdown/code": ["@streamdown/code@1.1.1", "https://registry.npmmirror.com/@streamdown/code/-/code-1.1.1.tgz", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-i7HTNuDgZWb+VdrNVOam9gQhIc5MSSDXKWXgbUrn/4vSRaSMM+Rtl10MQj4wLWPNpF7p80waJsAqFP8HZfb0Jg=="], - "@streamdown/math": ["@streamdown/math@1.0.2", "", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g=="], + "@streamdown/math": ["@streamdown/math@1.0.2", "https://registry.npmmirror.com/@streamdown/math/-/math-1.0.2.tgz", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g=="], - "@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="], + "@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "https://registry.npmmirror.com/@streamdown/mermaid/-/mermaid-1.0.2.tgz", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="], - "@tabby_ai/hijri-converter": ["@tabby_ai/hijri-converter@1.0.5", "https://registry.npmmirror.com/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", {}, "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ=="], + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.3.0.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.2.4.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.3.0.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.2.4.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.3.0.tgz", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], - "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.4", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "postcss": "^8.5.6", "tailwindcss": "4.2.4" } }, "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg=="], + "@tanstack/query-core": ["@tanstack/query-core@5.100.14", "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.100.14.tgz", {}, "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew=="], - "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "https://registry.npmmirror.com/@tailwindcss/typography/-/typography-0.5.19.tgz", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - - "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.2.4.tgz", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="], - - "@tanstack/hotkeys": ["@tanstack/hotkeys@0.8.0", "https://registry.npmmirror.com/@tanstack/hotkeys/-/hotkeys-0.8.0.tgz", { "dependencies": { "@tanstack/store": "^0.11.0" } }, "sha512-vqH7X9nb0MTJ/O08++dB5bP9jgj4+BIPOUu/U+6myG86lDsirZSVSobpq5UQpE7nBuk62i8eIYeOhd+OMl/UrA=="], - - "@tanstack/query-core": ["@tanstack/query-core@5.100.8", "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.100.8.tgz", {}, "sha512-ceYwSFOqjPwET5TA6IOYxzxlGc0ekyH/gfOtWkP0PX43rzX9bxW48Iuw8KAduKCToi4rJAQ6nRy2kAe8gszdmg=="], - - "@tanstack/react-hotkeys": ["@tanstack/react-hotkeys@0.10.0", "https://registry.npmmirror.com/@tanstack/react-hotkeys/-/react-hotkeys-0.10.0.tgz", { "dependencies": { "@tanstack/hotkeys": "0.8.0", "@tanstack/react-store": "^0.11.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-GwOSndI5j3qBVYTmgP1mYyRTnlxb2MS17cwGlsavSxMQPSnmDf+m3LzMIpRMs+3zzQMjg3cYhHsFYizYlFI2tw=="], - - "@tanstack/react-query": ["@tanstack/react-query@5.100.8", "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.100.8.tgz", { "dependencies": { "@tanstack/query-core": "5.100.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-iNNEekixXU5vtAGKKZX2lx3jTooG5yNY+kv0wSgEdEYG0Mj0JM5bcuQtC35ZAP3nDopT6jciUK3xeX65U7AnfA=="], - - "@tanstack/react-store": ["@tanstack/react-store@0.11.0", "https://registry.npmmirror.com/@tanstack/react-store/-/react-store-0.11.0.tgz", { "dependencies": { "@tanstack/store": "0.11.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tX4YXh3PDkmpvGQWkWqKpzs/MSqbtuwY9dWdWhtV9Q50PmO+jOkUKIWIX4G85dwt7lxdHLXsiaEKPdKmC8F41w=="], + "@tanstack/react-query": ["@tanstack/react-query@5.100.14", "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.100.14.tgz", { "dependencies": { "@tanstack/query-core": "5.100.14" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw=="], "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "https://registry.npmmirror.com/@tanstack/react-table/-/react-table-8.21.3.tgz", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.24", "", { "dependencies": { "@tanstack/virtual-core": "3.14.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg=="], - - "@tanstack/store": ["@tanstack/store@0.11.0", "https://registry.npmmirror.com/@tanstack/store/-/store-0.11.0.tgz", {}, "sha512-WlzzCt3xi0G6pCAJu1U+2jiECwabETDpQDi3hfkFZvJii9AuZqEKbOiVarX1/bWhTNjU486yQtJCCasi/0q+Cw=="], - "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "https://registry.npmmirror.com/@tanstack/table-core/-/table-core-8.21.3.tgz", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="], + "@tokenlens/core": ["@tokenlens/core@1.3.0", "https://registry.npmmirror.com/@tokenlens/core/-/core-1.3.0.tgz", {}, "sha512-d8YNHNC+q10bVpi95fELJwJyPVf1HfvBEI18eFQxRSZTdByXrP+f/ZtlhSzkx0Jl0aEmYVeBA5tPeeYRioLViQ=="], - "@transloadit/prettier-bytes": ["@transloadit/prettier-bytes@0.3.5", "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz", {}, "sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA=="], + "@tokenlens/fetch": ["@tokenlens/fetch@1.3.0", "https://registry.npmmirror.com/@tokenlens/fetch/-/fetch-1.3.0.tgz", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-RONDRmETYly9xO8XMKblmrZjKSwCva4s5ebJwQNfNlChZoA5kplPoCgnWceHnn1J1iRjLVlrCNB43ichfmGBKQ=="], - "@transloadit/types": ["@transloadit/types@4.3.0", "https://registry.npmmirror.com/@transloadit/types/-/types-4.3.0.tgz", {}, "sha512-kWuHGp4YhWe0upPDIsoMd6LF6UPOgfelOxN0OzDrztILqRqBPShJqe47Dp7InokVTZIwl9J1t32RAPb9oq4iWw=="], + "@tokenlens/helpers": ["@tokenlens/helpers@1.3.1", "https://registry.npmmirror.com/@tokenlens/helpers/-/helpers-1.3.1.tgz", { "dependencies": { "@tokenlens/core": "1.3.0", "@tokenlens/fetch": "1.3.0" } }, "sha512-t6yL8N6ES8337E6eVSeH4hCKnPdWkZRFpupy9w5E66Q9IeqQ9IO7XQ6gh12JKjvWiRHuyyJ8MBP5I549Cr41EQ=="], + + "@tokenlens/models": ["@tokenlens/models@1.3.0", "https://registry.npmmirror.com/@tokenlens/models/-/models-1.3.0.tgz", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-9mx7ZGeewW4ndXAiD7AT1bbCk4OpJeortbjHHyNkgap+pMPPn1chY6R5zqe1ggXIUzZ2l8VOAKfPqOvpcrisJw=="], "@ts-morph/common": ["@ts-morph/common@0.27.0", "https://registry.npmmirror.com/@ts-morph/common/-/common-0.27.0.tgz", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/babel__generator": ["@types/babel__generator@7.27.0", "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], - - "@types/babel__template": ["@types/babel__template@7.4.4", "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], - - "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - - "@types/cors": ["@types/cors@2.8.19", "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], - - "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + "@types/d3": ["@types/d3@7.4.3", "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], "@types/d3-array": ["@types/d3-array@3.2.2", "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + "@types/d3-axis": ["@types/d3-axis@3.0.6", "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], - "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + "@types/d3-brush": ["@types/d3-brush@3.0.6", "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], - "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + "@types/d3-chord": ["@types/d3-chord@3.0.6", "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], "@types/d3-color": ["@types/d3-color@3.1.3", "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + "@types/d3-contour": ["@types/d3-contour@3.0.6", "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], - "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], - "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], - "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + "@types/d3-drag": ["@types/d3-drag@3.0.7", "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], - "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], "@types/d3-ease": ["@types/d3-ease@3.0.2", "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], - "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + "@types/d3-force": ["@types/d3-force@3.0.10", "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], - "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + "@types/d3-format": ["@types/d3-format@3.0.4", "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], - "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + "@types/d3-geo": ["@types/d3-geo@3.1.0", "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], - "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], "@types/d3-path": ["@types/d3-path@3.1.1", "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], - "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], - "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + "@types/d3-random": ["@types/d3-random@3.0.3", "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], "@types/d3-scale": ["@types/d3-scale@4.0.9", "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], - "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + "@types/d3-selection": ["@types/d3-selection@3.0.11", "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], "@types/d3-shape": ["@types/d3-shape@3.1.8", "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], "@types/d3-time": ["@types/d3-time@3.0.4", "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], "@types/d3-timer": ["@types/d3-timer@3.0.2", "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + "@types/d3-transition": ["@types/d3-transition@3.0.9", "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], - "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], "@types/debug": ["@types/debug@4.1.13", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], - "@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/esrecurse": ["@types/esrecurse@4.3.1", "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], + + "@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "https://registry.npmmirror.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/geojson": ["@types/geojson@7946.0.16", "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], "@types/hast": ["@types/hast@3.0.4", "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], + "@types/js-cookie": ["@types/js-cookie@3.0.6", "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-3.0.6.tgz", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], "@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + "@types/katex": ["@types/katex@0.16.8", "https://registry.npmmirror.com/@types/katex/-/katex-0.16.8.tgz", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], "@types/mdast": ["@types/mdast@4.0.4", "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + "@types/mdx": ["@types/mdx@2.0.13", "https://registry.npmmirror.com/@types/mdx/-/mdx-2.0.13.tgz", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], "@types/ms": ["@types/ms@2.1.0", "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@24.12.2", "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/node": ["@types/node@24.12.4", "https://registry.npmmirror.com/@types/node/-/node-24.12.4.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="], - "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], + "@types/parse-json": ["@types/parse-json@4.0.2", "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.2.tgz", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], - "@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + "@types/prop-types": ["@types/prop-types@15.7.15", "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@19.2.15", "https://registry.npmmirror.com/@types/react/-/react-19.2.15.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], "@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "@types/retry": ["@types/retry@0.12.2", "https://registry.npmmirror.com/@types/retry/-/retry-0.12.2.tgz", {}, "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow=="], - "@types/set-cookie-parser": ["@types/set-cookie-parser@2.4.10", "https://registry.npmmirror.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw=="], "@types/statuses": ["@types/statuses@2.0.6", "https://registry.npmmirror.com/@types/statuses/-/statuses-2.0.6.tgz", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], - "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -973,180 +868,102 @@ "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "https://registry.npmmirror.com/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], - "@types/ws": ["@types/ws@8.18.1", "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/type-utils": "8.59.4", "@typescript-eslint/utils": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/type-utils": "8.59.1", "@typescript-eslint/utils": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.4.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.1.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.4", "@typescript-eslint/types": "^8.59.4", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.1", "@typescript-eslint/types": "^8.59.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4" } }, "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1" } }, "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/utils": "8.59.4", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/utils": "8.59.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.4.tgz", {}, "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.1.tgz", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.4", "@typescript-eslint/tsconfig-utils": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.1", "@typescript-eslint/tsconfig-utils": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.4.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.1.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/types": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.4", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.4", "eslint-visitor-keys": "^5.0.0" } }, "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.1", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "https://registry.npmmirror.com/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], - "@uppy/audio": ["@uppy/audio@3.1.0", "https://registry.npmmirror.com/@uppy/audio/-/audio-3.1.0.tgz", { "dependencies": { "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-149eswgMyLA9zQzIvgDzwtA0O2cWV6UUR1ke4vNDyKC8/9Litg1m8iXjri5bmWVVjaAgNpADmucKPt3vL8a/jg=="], + "@use-gesture/core": ["@use-gesture/core@10.3.1", "https://registry.npmmirror.com/@use-gesture/core/-/core-10.3.1.tgz", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="], - "@uppy/aws-s3": ["@uppy/aws-s3@5.1.0", "https://registry.npmmirror.com/@uppy/aws-s3/-/aws-s3-5.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/utils": "^7.1.4" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-UBz+shrtDbnOf11AboDrkc9Fq2Cdf8HbFftE+gqfxig6fkv5rHpHhBCLkl8wCGAq+X/CxdqvvNhm/OM23Uzw2w=="], + "@use-gesture/react": ["@use-gesture/react@10.3.1", "https://registry.npmmirror.com/@use-gesture/react/-/react-10.3.1.tgz", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="], - "@uppy/box": ["@uppy/box@4.1.0", "https://registry.npmmirror.com/@uppy/box/-/box-4.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-pWYNBobB1QoaBaKJYTZMB2/asGkJirx+YJa6C0s5/GfgsA2kdVKnMpbzC+Kon6Ss6zBW5Cjj+1J+9rRSkih9GQ=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "https://registry.npmmirror.com/@vercel/oidc/-/oidc-3.2.0.tgz", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], - "@uppy/companion-client": ["@uppy/companion-client@5.1.1", "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-5.1.1.tgz", { "dependencies": { "@uppy/utils": "^7.1.1", "namespace-emitter": "^2.0.1", "p-retry": "^6.1.0" }, "peerDependencies": { "@uppy/core": "^5.1.1" } }, "sha512-DzrOWTbIZHvtgAFXBMYHk2wD27NjpBSVhY2tEiEIUhPd2CxbFRZjHM/N3HOt3VwZEAP471QWFLlJRWPcIY3A2Q=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], - "@uppy/compressor": ["@uppy/compressor@3.1.0", "https://registry.npmmirror.com/@uppy/compressor/-/compressor-3.1.0.tgz", { "dependencies": { "@transloadit/prettier-bytes": "^0.3.4", "@uppy/utils": "^7.1.4", "compressorjs": "^1.2.1", "preact": "^10.5.13", "promise-queue": "^2.2.5" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-EPCU1j/hvey05/ePPzuqaF7p4fORsc3PNvRGDXrT5fiZv0FCAufY5GZf4OUAiKNHkL59wwxAgMySOTJq7WBZtA=="], - - "@uppy/core": ["@uppy/core@5.2.0", "https://registry.npmmirror.com/@uppy/core/-/core-5.2.0.tgz", { "dependencies": { "@transloadit/prettier-bytes": "^0.3.4", "@uppy/store-default": "^5.0.0", "@uppy/utils": "^7.1.4", "lodash": "^4.17.21", "mime-match": "^1.0.2", "namespace-emitter": "^2.0.1", "nanoid": "^5.0.9", "preact": "^10.5.13" } }, "sha512-uvfNyz4cnaplt7LYJmEZHuqOuav0tKp4a9WKJIaH6iIj7XiqYvS2J5SEByexAlUFlzefOAyjzj4Ja2dd/8aMrw=="], - - "@uppy/dashboard": ["@uppy/dashboard@5.1.1", "https://registry.npmmirror.com/@uppy/dashboard/-/dashboard-5.1.1.tgz", { "dependencies": { "@transloadit/prettier-bytes": "^0.3.4", "@uppy/provider-views": "^5.2.2", "@uppy/thumbnail-generator": "^5.1.0", "@uppy/utils": "^7.1.5", "classnames": "^2.2.6", "lodash": "^4.17.23", "nanoid": "^5.0.9", "preact": "^10.26.10", "shallow-equal": "^3.0.0" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-6H/xVvhhdfwp1+FRMp2C+tudyaedqD5+LMDB8Iw20k9+QCL1eGzOh4wXm6MCqJtNfQ1tLaprGMG1jlo7yS/uyw=="], - - "@uppy/drag-drop": ["@uppy/drag-drop@5.1.0", "https://registry.npmmirror.com/@uppy/drag-drop/-/drag-drop-5.1.0.tgz", { "dependencies": { "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-P36E6bBc/U8wxooV9IerPpEbrl8i2N3P+twmm5nPywcKHfjS7BtyHUEs2JmWgyRRibI75c2Ge5wWchKtQclEvw=="], - - "@uppy/drop-target": ["@uppy/drop-target@4.1.0", "https://registry.npmmirror.com/@uppy/drop-target/-/drop-target-4.1.0.tgz", { "dependencies": { "@uppy/utils": "^7.1.4" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-QPOwlOfYlf4gOzdj75Mx0YxM0d6Jv1ui2aCERse1pwtIeQGQ0QPe5lmTi0o3p8dTnqACUmoM6SJOpAHQozCSkA=="], - - "@uppy/dropbox": ["@uppy/dropbox@5.1.0", "https://registry.npmmirror.com/@uppy/dropbox/-/dropbox-5.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-cCSgMIiBEmO3n7mcKMU/UmAYzUxrfKxe8KXMeIl1OPDdOjXWDtcXf08Qs46Nog8FA+4zrDheqaA+FeSzhfnE/Q=="], - - "@uppy/facebook": ["@uppy/facebook@5.1.0", "https://registry.npmmirror.com/@uppy/facebook/-/facebook-5.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-beYH0VInYNLpQ3knhCbS6IJAZGGvtZnLstT8Dfj3rWtGHhTa6clNSqfdGripgBzryLC4bXxkI2y7QonY8+h3gA=="], - - "@uppy/form": ["@uppy/form@5.1.0", "https://registry.npmmirror.com/@uppy/form/-/form-5.1.0.tgz", { "dependencies": { "@uppy/utils": "^7.1.4", "get-form-data": "^3.0.0" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-wu04XyTeWY8vPf5ERe41hp6wdTWQcM/YczQkqFhuQtj0IEAItqZxt89DcbAT8GkXmwyLjbIe0AeXo/Vz10oUpA=="], - - "@uppy/golden-retriever": ["@uppy/golden-retriever@5.2.1", "https://registry.npmmirror.com/@uppy/golden-retriever/-/golden-retriever-5.2.1.tgz", { "dependencies": { "@uppy/utils": "^7.1.5", "lodash": "^4.17.21" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-R+Lzesfmgu35ViSOxqYxwOVo4ylkIS9xP2ClnhD8k2VTbqdc6RRimaPRqxxI3Vf42qWiRBm4WaG3uHls+wOCqQ=="], - - "@uppy/google-drive": ["@uppy/google-drive@5.1.0", "https://registry.npmmirror.com/@uppy/google-drive/-/google-drive-5.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-avspyvCfQgpSvry8xXRIazUv9RhwyubSeDsg9BQaNR1BRFSw5PWVuoX7tIUyoDYtzA8TsxcYvqJuSm4HZYkQcA=="], - - "@uppy/google-drive-picker": ["@uppy/google-drive-picker@1.1.1", "https://registry.npmmirror.com/@uppy/google-drive-picker/-/google-drive-picker-1.1.1.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.2", "@uppy/utils": "^7.1.5", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-vPt8c4bcKsrtBZbbWOG73ac9xH2PMBOGt45jy+9vxV/YhXKYVimC0VzgzLSzD7Z4d2KosIod39mveGpFRBMTvQ=="], - - "@uppy/google-photos-picker": ["@uppy/google-photos-picker@1.1.0", "https://registry.npmmirror.com/@uppy/google-photos-picker/-/google-photos-picker-1.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-H7xzxzapivRaoot88fZitkBtq7a70EdTo+4RwkHvsQ/1dRmIAhS24X4lOTONORckjP9Tvr+4FHu+1oaLh1i9kQ=="], - - "@uppy/image-editor": ["@uppy/image-editor@4.2.0", "https://registry.npmmirror.com/@uppy/image-editor/-/image-editor-4.2.0.tgz", { "dependencies": { "@uppy/utils": "^7.1.5", "cropperjs": "^1.6.2", "preact": "^10.26.10" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-7Mn40hQMEwy9lufXYfNhSDuY24LUUSSmumXEoivGgUnXsNgcmppyzVjY94/Q6S6yX7Bg3L/sMvayjPXhYwt64A=="], - - "@uppy/image-generator": ["@uppy/image-generator@1.0.0", "https://registry.npmmirror.com/@uppy/image-generator/-/image-generator-1.0.0.tgz", { "dependencies": { "@uppy/provider-views": "^5.2.1", "@uppy/transloadit": "^5.4.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-Fz63Jm8EvEdKsoIO/EcYkQTmY1sVEkzkh7pOVjPIB/EpzXY4xiz7/J4x8hH0whp8wZw8DR7PLPtMqhzNhCKYfw=="], - - "@uppy/instagram": ["@uppy/instagram@5.1.0", "https://registry.npmmirror.com/@uppy/instagram/-/instagram-5.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-FtAA/24zAI/NKut43l1GTrDyUbeXl4+gWDAWNxSc9iVh1VgwILH3n2HMKE5UuVKb3Um27JYfuHVvc0N4pMM1BA=="], - - "@uppy/locales": ["@uppy/locales@5.1.1", "https://registry.npmmirror.com/@uppy/locales/-/locales-5.1.1.tgz", { "dependencies": { "@uppy/utils": "^7.1.5" } }, "sha512-zSbDU27JzfdssRJoa5/xmGOsrEtS+2Z9j41weaoCa/NoK4wqZzkFNQ0Z44etbTg3PDVFakZVDu/Z+c+vsJCfdQ=="], - - "@uppy/onedrive": ["@uppy/onedrive@5.1.0", "https://registry.npmmirror.com/@uppy/onedrive/-/onedrive-5.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-FWAYesLNl6RDu+Lt+8PBvvXAqlTEmzZrbNmcIrDWaZGCA4iyXvH8suOpdn9VYFpV3JJEI3xwpvu8Lr9vrsGg5g=="], - - "@uppy/provider-views": ["@uppy/provider-views@5.2.2", "https://registry.npmmirror.com/@uppy/provider-views/-/provider-views-5.2.2.tgz", { "dependencies": { "@uppy/utils": "^7.1.5", "classnames": "^2.2.6", "lodash": "^4.17.21", "nanoid": "^5.0.9", "p-queue": "^8.0.0", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-NAazIJ5sjrAc6++CeJ/u9dB5gDaaAOLHrYeEmWs/HqLlftlIinRZOybnyzJRXwI8jWI/FK5moluzt2HXu6dPQQ=="], - - "@uppy/remote-sources": ["@uppy/remote-sources@3.1.0", "https://registry.npmmirror.com/@uppy/remote-sources/-/remote-sources-3.1.0.tgz", { "dependencies": { "@uppy/box": "^4.1.0", "@uppy/dashboard": "^5.1.0", "@uppy/dropbox": "^5.1.0", "@uppy/facebook": "^5.1.0", "@uppy/google-drive": "^5.1.0", "@uppy/instagram": "^5.1.0", "@uppy/onedrive": "^5.1.0", "@uppy/unsplash": "^5.1.0", "@uppy/url": "^5.1.0", "@uppy/zoom": "^4.1.0" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-TG1jSQVVQVie7eIxxgLxTDVlX4ujdv4qA7w+FyaUvpleBHylT73EpyNguc097UhNZ5Ez4FKtqbjbvNn7zuJTxw=="], - - "@uppy/screen-capture": ["@uppy/screen-capture@5.1.0", "https://registry.npmmirror.com/@uppy/screen-capture/-/screen-capture-5.1.0.tgz", { "dependencies": { "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-+/AAU6Uxmup+Pzuy8/8S1ZttDAcMLnQleapxQLVpzSeddFCCeM+t6CfQ5tL/huSolz0o4v5rZWNaoUm+m5+wUw=="], - - "@uppy/status-bar": ["@uppy/status-bar@5.1.0", "https://registry.npmmirror.com/@uppy/status-bar/-/status-bar-5.1.0.tgz", { "dependencies": { "@transloadit/prettier-bytes": "^0.3.4", "@uppy/utils": "^7.1.4", "classnames": "^2.2.6", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-0rq+t/i++bbWdgXF6VfsLRF49mxtdf8PIZ62rmRbK+X3HvZdCA4RVWeZlMk+hfAP9th9g9a1rn5lkhAImr/XgQ=="], - - "@uppy/store-default": ["@uppy/store-default@5.0.0", "https://registry.npmmirror.com/@uppy/store-default/-/store-default-5.0.0.tgz", {}, "sha512-hQtCSQ1yGiaval/wVYUWquYGDJ+bpQ7e4FhUUAsRQz1x1K+o7NBtjfp63O9I4Ks1WRoKunpkarZ+as09l02cPw=="], - - "@uppy/thumbnail-generator": ["@uppy/thumbnail-generator@5.1.0", "https://registry.npmmirror.com/@uppy/thumbnail-generator/-/thumbnail-generator-5.1.0.tgz", { "dependencies": { "@uppy/utils": "^7.1.4", "exifr": "^7.0.0" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-QAKJHHkMrD/30GOyUb5U9HyJ7Ie3jiMLp4pVdw27PoA4pNV7fDQz0tyDeRPj2H+BWPEB1NsTSSfHI2pjHNI+OQ=="], - - "@uppy/transloadit": ["@uppy/transloadit@5.5.1", "https://registry.npmmirror.com/@uppy/transloadit/-/transloadit-5.5.1.tgz", { "dependencies": { "@transloadit/types": "^4.1.3", "@uppy/tus": "^5.1.1", "@uppy/utils": "^7.2.0", "component-emitter": "^2.0.0" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-nzgZ/u90hMzbsCj/44pVepUXtbOvpIbWAHLRLbSTUbQUfwR6o8o5s59IFmFUQ3uLYdunW4DDaOkEdNb7hRLmEw=="], - - "@uppy/tus": ["@uppy/tus@5.1.1", "https://registry.npmmirror.com/@uppy/tus/-/tus-5.1.1.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/utils": "^7.1.5", "tus-js-client": "^4.2.3" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-316kLQfO5H/uUJIMhBYhBrTpeN0Q+d6ykW3pomCvdTkFGCvg20rF3oH/owE3lf2UZZN7ZqBk+wHO0WlQePoklg=="], - - "@uppy/unsplash": ["@uppy/unsplash@5.1.0", "https://registry.npmmirror.com/@uppy/unsplash/-/unsplash-5.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-LGWTY54DTWxmQSet0EghRG/MoOabPoCUf1gCyxL9MXhSN+TfjFbQuVVUHqtnVN3shFAZSUv7bVTQlLrrM9hkzg=="], - - "@uppy/url": ["@uppy/url@5.1.0", "https://registry.npmmirror.com/@uppy/url/-/url-5.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/utils": "^7.1.4", "nanoid": "^5.0.9", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-MAzSboTDjU7tMP/8AvgDHt+UixxsajI/QWZ6/6d43z6G01+Js8ZNth/j6HStjM5v4K6e3/521SxAaP2hoZk6IQ=="], - - "@uppy/utils": ["@uppy/utils@7.2.0", "https://registry.npmmirror.com/@uppy/utils/-/utils-7.2.0.tgz", { "dependencies": { "lodash": "^4.17.23", "preact": "^10.26.10" } }, "sha512-6lC246qszMv6bTyl/+QyHwrudgeguWkA94ME1wHn+a6uRAvmtAEaUManIfGqTJfoKvWAiCJqdJPl5xRJjhAloQ=="], - - "@uppy/webcam": ["@uppy/webcam@5.1.0", "https://registry.npmmirror.com/@uppy/webcam/-/webcam-5.1.0.tgz", { "dependencies": { "@uppy/utils": "^7.1.4", "is-mobile": "^4.0.0", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-SHI+yiNrD4JrTFhQPyt7Mi9nL5r5YDwyXtYXuSRM3URB0etKes6u02MKY5GEn2LEpCSqSceXUi84Prig7UWYIw=="], - - "@uppy/webdav": ["@uppy/webdav@1.1.1", "https://registry.npmmirror.com/@uppy/webdav/-/webdav-1.1.1.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.1", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-QEtjLjNo5XH16aww0rX2rRNF1eboqtqfnNkRH28+jJe1gt2PH7ZcPkCw3ROFzyxzbQPC9ABo7kvWAEKL3dH72g=="], - - "@uppy/xhr-upload": ["@uppy/xhr-upload@5.2.0", "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-5.2.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/utils": "^7.2.0" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-3LV/X5Of6BINnKplP+CwUJ0a4/7cRFfzxwGyXnW+uCrNQHoo09dttcz3begWHejGvzenQHuUnMO3Fxyc71Pryg=="], - - "@uppy/zoom": ["@uppy/zoom@4.1.0", "https://registry.npmmirror.com/@uppy/zoom/-/zoom-4.1.0.tgz", { "dependencies": { "@uppy/companion-client": "^5.1.1", "@uppy/provider-views": "^5.2.0", "@uppy/utils": "^7.1.4", "preact": "^10.5.13" }, "peerDependencies": { "@uppy/core": "^5.2.0" } }, "sha512-roW/Zz0n8WDxSIg5rOO2Kp0rapE2z/U4ZVQdRFYhM3MMMQV0YW6CiUWgWOU4jtsdvAqsNisdEmbPAgSRSZK3ug=="], - - "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], - - "@use-gesture/core": ["@use-gesture/core@10.3.1", "", {}, "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw=="], - - "@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="], - - "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], - - "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], - - "accepts": ["accepts@1.3.8", "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], - "acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-base": ["agent-base@6.0.2", "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "ahooks": ["ahooks@3.9.7", "", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw=="], + "ahooks": ["ahooks@3.9.7", "https://registry.npmmirror.com/ahooks/-/ahooks-3.9.7.tgz", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw=="], - "ai": ["ai@6.0.177", "", { "dependencies": { "@ai-sdk/gateway": "3.0.112", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1xQtbeWwNcLyyM86ixZhkKvT+WRXc1lvarIKqPVtsyn8F9NDikwUMBqYu+aQKDgMht50SMXh4qboYuU8MeHZZA=="], + "ai": ["ai@6.0.193", "https://registry.npmmirror.com/ai/-/ai-6.0.193.tgz", { "dependencies": { "@ai-sdk/gateway": "3.0.121", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VQOTOse8+X8kMtg61DNSXlYJzwOW4NjMLDJNk/qxClWsFe4oiyFJDHGGG1oezfGcFzuYuQe/8Z7r4kwiZWh2YQ=="], - "ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "https://registry.npmmirror.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], "ajv-formats": ["ajv-formats@3.0.1", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "anser": ["anser@2.3.5", "https://registry.npmmirror.com/anser/-/anser-2.3.5.tgz", {}, "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ=="], + "ansi-colors": ["ansi-colors@4.1.3", "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], "ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "antd": ["antd@6.3.7", "", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.1", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.9.1", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.9.1", "@rc-component/tabs": "~1.7.0", "@rc-component/textarea": "~1.1.2", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.3.0", "@rc-component/tree": "~1.2.4", "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.10.1", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-WTHi4bHVNKpYXLHESzU0Tts7rRNQeL84Bph9dfI3Qw7mHbTulExDcYKNHny5CTXcrBBOpraXbU9miBAwUR5vaw=="], + "ansi-to-react": ["ansi-to-react@6.2.6", "https://registry.npmmirror.com/ansi-to-react/-/ansi-to-react-6.2.6.tgz", { "dependencies": { "anser": "^2.3.2", "escape-carriage": "^1.3.1", "linkify-it": "^3.0.3" }, "peerDependencies": { "react": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Eqi0iaMK5OZ3jsVFxWvU2B74UZBnGuHlkflKMX6wTOeH+luy9KE2O0gUkc2PxhIP1R4IO0xohv62UMFInQOSeg=="], - "antd-style": ["antd-style@4.1.0", "", { "dependencies": { "@ant-design/cssinjs": "^2.0.0", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=6.0.0", "react": ">=18" } }, "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ=="], + "antd": ["antd@6.4.3", "https://registry.npmmirror.com/antd/-/antd-6.4.3.tgz", { "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", "@ant-design/icons": "^6.2.3", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.29.2", "@rc-component/cascader": "~1.15.0", "@rc-component/checkbox": "~2.0.0", "@rc-component/collapse": "~1.2.0", "@rc-component/color-picker": "~3.1.1", "@rc-component/dialog": "~1.9.0", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", "@rc-component/form": "~1.8.1", "@rc-component/image": "~1.9.0", "@rc-component/input": "~1.3.0", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.9.0", "@rc-component/menu": "~1.3.0", "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~2.0.7", "@rc-component/pagination": "~1.2.0", "@rc-component/picker": "~1.10.0", "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", "@rc-component/table": "~1.10.0", "@rc-component/tabs": "~1.9.0", "@rc-component/tooltip": "~1.4.0", "@rc-component/tour": "~2.4.0", "@rc-component/tree": "~1.3.1", "@rc-component/tree-select": "~1.9.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", "@rc-component/util": "^1.11.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw=="], + + "antd-style": ["antd-style@4.1.0", "https://registry.npmmirror.com/antd-style/-/antd-style-4.1.0.tgz", { "dependencies": { "@ant-design/cssinjs": "^2.0.0", "@babel/runtime": "^7.24.1", "@emotion/cache": "^11.11.0", "@emotion/css": "^11.11.2", "@emotion/react": "^11.11.4", "@emotion/serialize": "^1.1.3", "@emotion/utils": "^1.2.1", "use-merge-value": "^1.2.0" }, "peerDependencies": { "antd": ">=6.0.0", "react": ">=18" } }, "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ=="], "argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-hidden": ["aria-hidden@1.2.6", "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "assign-symbols": ["assign-symbols@1.0.0", "", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="], + "assign-symbols": ["assign-symbols@1.0.0", "https://registry.npmmirror.com/assign-symbols/-/assign-symbols-1.0.0.tgz", {}, "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw=="], "ast-types": ["ast-types@0.16.1", "https://registry.npmmirror.com/ast-types/-/ast-types-0.16.1.tgz", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], - "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + "astring": ["astring@1.9.0", "https://registry.npmmirror.com/astring/-/astring-1.9.0.tgz", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], "asynckit": ["asynckit@0.4.0", "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], + "attr-accept": ["attr-accept@2.2.5", "https://registry.npmmirror.com/attr-accept/-/attr-accept-2.2.5.tgz", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], - "axios": ["axios@1.15.2", "https://registry.npmmirror.com/axios/-/axios-1.15.2.tgz", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A=="], + "axios": ["axios@1.16.1", "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="], - "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], + "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "https://registry.npmmirror.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], "bail": ["bail@2.0.2", "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "base64id": ["base64id@2.0.0", "https://registry.npmmirror.com/base64id/-/base64id-2.0.0.tgz", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.25", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA=="], - - "blueimp-canvas-to-blob": ["blueimp-canvas-to-blob@3.29.0", "https://registry.npmmirror.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", {}, "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="], "body-parser": ["body-parser@2.2.2", "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], - "brace-expansion": ["brace-expansion@1.1.14", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + "brace-expansion": ["brace-expansion@5.0.6", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], "braces": ["braces@3.0.3", "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bundle-name": ["bundle-name@4.1.0", "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bytes": ["bytes@3.1.2", "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -1157,11 +974,11 @@ "callsites": ["callsites@3.1.0", "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001791", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001793", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], "ccount": ["ccount@2.0.1", "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], - "chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chalk": ["chalk@5.6.2", "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "character-entities": ["character-entities@2.0.2", "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -1171,15 +988,9 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="], - - "chevrotain-allstar": ["chevrotain-allstar@0.4.3", "", { "dependencies": { "lodash-es": "^4.18.1" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ=="], - "chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "chroma-js": ["chroma-js@3.2.0", "", {}, "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw=="], - - "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + "chroma-js": ["chroma-js@3.2.0", "https://registry.npmmirror.com/chroma-js/-/chroma-js-3.2.0.tgz", {}, "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw=="], "class-variance-authority": ["class-variance-authority@0.7.1", "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], @@ -1199,15 +1010,13 @@ "code-block-writer": ["code-block-writer@13.0.3", "https://registry.npmmirror.com/code-block-writer/-/code-block-writer-13.0.3.tgz", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "https://registry.npmmirror.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color-convert": ["color-convert@2.0.1", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "colord": ["colord@2.9.3", "", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], - - "combine-errors": ["combine-errors@3.0.3", "https://registry.npmmirror.com/combine-errors/-/combine-errors-3.0.3.tgz", { "dependencies": { "custom-error-instance": "2.1.1", "lodash.uniqby": "4.5.0" } }, "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q=="], + "colord": ["colord@2.9.3", "https://registry.npmmirror.com/colord/-/colord-2.9.3.tgz", {}, "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="], "combined-stream": ["combined-stream@1.0.8", "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], @@ -1217,13 +1026,7 @@ "compare-versions": ["compare-versions@6.1.1", "https://registry.npmmirror.com/compare-versions/-/compare-versions-6.1.1.tgz", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], - "component-emitter": ["component-emitter@2.0.0", "https://registry.npmmirror.com/component-emitter/-/component-emitter-2.0.0.tgz", {}, "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw=="], - - "compressorjs": ["compressorjs@1.3.0", "https://registry.npmmirror.com/compressorjs/-/compressorjs-1.3.0.tgz", { "dependencies": { "blueimp-canvas-to-blob": "^3.29.0", "is-blob": "^2.1.0" } }, "sha512-TsvzkRgDm/6mIRUdxJbrTH7kfSW3oJzOw8b1xU60fziQSosTML5TczpO6Z4H1LGF0yRmTotk6r5UNhuRxEwA1A=="], - - "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], - - "concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], "content-disposition": ["content-disposition@1.1.0", "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], @@ -1237,77 +1040,73 @@ "cors": ["cors@2.8.6", "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + "cose-base": ["cose-base@1.0.3", "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], "cosmiconfig": ["cosmiconfig@9.0.1", "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-9.0.1.tgz", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], - "cropperjs": ["cropperjs@1.6.2", "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.6.2.tgz", {}, "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA=="], - "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "custom-error-instance": ["custom-error-instance@2.1.1", "https://registry.npmmirror.com/custom-error-instance/-/custom-error-instance-2.1.1.tgz", {}, "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="], + "cytoscape": ["cytoscape@3.33.4", "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.4.tgz", {}, "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww=="], - "cytoscape": ["cytoscape@3.33.3", "", {}, "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g=="], + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "https://registry.npmmirror.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], - "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "https://registry.npmmirror.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], - "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], - - "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + "d3": ["d3@7.9.0", "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], "d3-array": ["d3-array@3.2.4", "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + "d3-axis": ["d3-axis@3.0.0", "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], - "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + "d3-brush": ["d3-brush@3.0.0", "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], - "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + "d3-chord": ["d3-chord@3.0.1", "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], "d3-color": ["d3-color@3.1.0", "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + "d3-contour": ["d3-contour@4.0.2", "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], - "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + "d3-delaunay": ["d3-delaunay@6.0.4", "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], - "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + "d3-dispatch": ["d3-dispatch@3.0.1", "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], - "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + "d3-drag": ["d3-drag@3.0.0", "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], - "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + "d3-dsv": ["d3-dsv@3.0.1", "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], "d3-ease": ["d3-ease@3.0.1", "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + "d3-fetch": ["d3-fetch@3.0.1", "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], - "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + "d3-force": ["d3-force@3.0.0", "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], "d3-format": ["d3-format@3.1.2", "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + "d3-geo": ["d3-geo@3.1.1", "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], - "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + "d3-hierarchy": ["d3-hierarchy@3.1.2", "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], "d3-interpolate": ["d3-interpolate@3.0.1", "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], "d3-path": ["d3-path@3.1.0", "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + "d3-polygon": ["d3-polygon@3.0.1", "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], - "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + "d3-quadtree": ["d3-quadtree@3.0.1", "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], - "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + "d3-random": ["d3-random@3.0.1", "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], - "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + "d3-sankey": ["d3-sankey@0.12.3", "https://registry.npmmirror.com/d3-sankey/-/d3-sankey-0.12.3.tgz", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], "d3-scale": ["d3-scale@4.0.2", "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], - "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + "d3-selection": ["d3-selection@3.0.0", "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], "d3-shape": ["d3-shape@3.2.0", "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], @@ -1317,27 +1116,25 @@ "d3-timer": ["d3-timer@3.0.1", "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + "d3-transition": ["d3-transition@3.0.1", "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], - "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + "d3-zoom": ["d3-zoom@3.0.0", "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + "dagre-d3-es": ["dagre-d3-es@7.0.14", "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - "date-fns": ["date-fns@4.1.0", "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "date-fns": ["date-fns@4.3.0", "https://registry.npmmirror.com/date-fns/-/date-fns-4.3.0.tgz", {}, "sha512-OYcL+3N/jyWbYdFGqoMAhytDgxP9pbYPUUiRCOgn4Fewaadk9l/Wam4Avciiyp2BgkpfQyBV9B+ehnVJych+eQ=="], - "date-fns-jalali": ["date-fns-jalali@4.1.0-0", "https://registry.npmmirror.com/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="], + "dayjs": ["dayjs@1.11.21", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], - "dayjs": ["dayjs@1.11.20", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], - - "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js-light": ["decimal.js-light@2.5.1", "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], - "decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="], + "decode-uri-component": ["decode-uri-component@0.4.1", "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="], "dedent": ["dedent@1.7.2", "https://registry.npmmirror.com/dedent/-/dedent-1.7.2.tgz", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], @@ -1351,7 +1148,7 @@ "define-lazy-prop": ["define-lazy-prop@3.0.0", "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], - "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + "delaunator": ["delaunator@5.1.0", "https://registry.npmmirror.com/delaunator/-/delaunator-5.1.0.tgz", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], "delayed-stream": ["delayed-stream@1.0.0", "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], @@ -1365,13 +1162,17 @@ "devlop": ["devlop@1.1.0", "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "dexie": ["dexie@4.4.2", "", {}, "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw=="], + "dexie": ["dexie@4.4.3", "https://registry.npmmirror.com/dexie/-/dexie-4.4.3.tgz", {}, "sha512-N+3IGQ3HPlyO2YAkntGAwitm42BpBGV86MttzUMiRzWLa4NGh0pltVRcUVF4ybL/OnXjCrr9k7SDPIKkFYP2Lg=="], - "dexie-react-hooks": ["dexie-react-hooks@4.4.0", "", { "peerDependencies": { "dexie": ">=4.2.0-alpha.1 <5.0.0", "react": ">=16" } }, "sha512-ObLXBS5+4BJU8vtSvBx6b9fY6zZYgniAtwxzjCHsUQadgbqYN6935X2/1TWw4Rf2N1aZV1io5/ziox4vKuxABA=="], + "dexie-react-hooks": ["dexie-react-hooks@4.4.0", "https://registry.npmmirror.com/dexie-react-hooks/-/dexie-react-hooks-4.4.0.tgz", { "peerDependencies": { "dexie": ">=4.2.0-alpha.1 <5.0.0", "react": ">=16" } }, "sha512-ObLXBS5+4BJU8vtSvBx6b9fY6zZYgniAtwxzjCHsUQadgbqYN6935X2/1TWw4Rf2N1aZV1io5/ziox4vKuxABA=="], "diff": ["diff@8.0.4", "https://registry.npmmirror.com/diff/-/diff-8.0.4.tgz", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], - "dompurify": ["dompurify@3.4.2", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "https://registry.npmmirror.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + + "dnd-core": ["dnd-core@14.0.1", "https://registry.npmmirror.com/dnd-core/-/dnd-core-14.0.1.tgz", { "dependencies": { "@react-dnd/asap": "^4.0.0", "@react-dnd/invariant": "^2.0.0", "redux": "^4.1.1" } }, "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A=="], + + "dompurify": ["dompurify@3.2.7", "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.7.tgz", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], "dotenv": ["dotenv@17.4.2", "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], @@ -1381,7 +1182,7 @@ "ee-first": ["ee-first@1.1.1", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.349", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", {}, "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.361", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", {}, "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA=="], "embla-carousel": ["embla-carousel@8.6.0", "https://registry.npmmirror.com/embla-carousel/-/embla-carousel-8.6.0.tgz", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], @@ -1389,17 +1190,19 @@ "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "https://registry.npmmirror.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="], - "emoji-mart": ["emoji-mart@5.6.0", "", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="], + "emoji-mart": ["emoji-mart@5.6.0", "https://registry.npmmirror.com/emoji-mart/-/emoji-mart-5.6.0.tgz", {}, "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow=="], + + "emoji-picker-react": ["emoji-picker-react@4.19.1", "https://registry.npmmirror.com/emoji-picker-react/-/emoji-picker-react-4.19.1.tgz", { "dependencies": { "flairup": "1.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-BmDdqInKFVYJpv7qS9WI6L9656cDAC+FkDvUjJds56nKHbaVTBNeDmLwKBytRnzu37zWHs9Isg7gt5PT43y6xA=="], "emoji-regex": ["emoji-regex@10.6.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "encodeurl": ["encodeurl@2.0.0", "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "engine.io": ["engine.io@6.6.7", "https://registry.npmmirror.com/engine.io/-/engine.io-6.6.7.tgz", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3" } }, "sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ=="], + "engine.io-client": ["engine.io-client@6.6.5", "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.5.tgz", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.20.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg=="], "engine.io-parser": ["engine.io-parser@5.2.3", "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.21.0", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], + "enhanced-resolve": ["enhanced-resolve@5.22.0", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A=="], "enquirer": ["enquirer@2.4.1", "https://registry.npmmirror.com/enquirer/-/enquirer-2.4.1.tgz", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -1413,35 +1216,37 @@ "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-object-atoms": ["es-object-atoms@1.1.2", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], "es-toolkit": ["es-toolkit@1.46.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], - "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "https://registry.npmmirror.com/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], - "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + "esast-util-from-js": ["esast-util-from-js@2.0.1", "https://registry.npmmirror.com/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], "esbuild": ["esbuild@0.27.7", "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], "escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-carriage": ["escape-carriage@1.3.1", "https://registry.npmmirror.com/escape-carriage/-/escape-carriage-1.3.1.tgz", {}, "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw=="], + "escape-html": ["escape-html@1.0.3", "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.4", "https://registry.npmmirror.com/eslint/-/eslint-9.39.4.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "eslint": ["eslint@10.4.0", "https://registry.npmmirror.com/eslint/-/eslint-10.4.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="], - "eslint-scope": ["eslint-scope@8.4.0", "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + "eslint-scope": ["eslint-scope@9.1.2", "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-9.1.2.tgz", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "espree": ["espree@10.4.0", "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "espree": ["espree@11.2.0", "https://registry.npmmirror.com/espree/-/espree-11.2.0.tgz", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], "esprima": ["esprima@4.0.1", "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -1451,19 +1256,19 @@ "estraverse": ["estraverse@5.3.0", "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "https://registry.npmmirror.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], - "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "https://registry.npmmirror.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + "estree-util-scope": ["estree-util-scope@1.0.0", "https://registry.npmmirror.com/estree-util-scope/-/estree-util-scope-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], - "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + "estree-util-to-js": ["estree-util-to-js@2.0.0", "https://registry.npmmirror.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], - "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + "estree-util-visit": ["estree-util-visit@2.0.0", "https://registry.npmmirror.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "estree-walker": ["estree-walker@3.0.3", "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -1477,15 +1282,13 @@ "execa": ["execa@9.6.1", "https://registry.npmmirror.com/execa/-/execa-9.6.1.tgz", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "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.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], - "exifr": ["exifr@7.1.3", "https://registry.npmmirror.com/exifr/-/exifr-7.1.3.tgz", {}, "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="], - "express": ["express@5.2.1", "https://registry.npmmirror.com/express/-/express-5.2.1.tgz", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.4.1", "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.4.1.tgz", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw=="], + "express-rate-limit": ["express-rate-limit@8.5.2", "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.5.2.tgz", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], "extend": ["extend@3.0.2", "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + "extend-shallow": ["extend-shallow@2.0.1", "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1499,9 +1302,9 @@ "fast-string-width": ["fast-string-width@3.0.2", "https://registry.npmmirror.com/fast-string-width/-/fast-string-width-3.0.2.tgz", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], - "fast-uri": ["fast-uri@3.1.0", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-uri": ["fast-uri@3.1.2", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], - "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "https://registry.npmmirror.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "https://registry.npmmirror.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], "fastq": ["fastq@1.20.1", "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -1513,25 +1316,27 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "file-selector": ["file-selector@0.5.0", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="], + "file-selector": ["file-selector@0.5.0", "https://registry.npmmirror.com/file-selector/-/file-selector-0.5.0.tgz", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="], "fill-range": ["fill-range@7.1.1", "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="], + "filter-obj": ["filter-obj@5.1.0", "https://registry.npmmirror.com/filter-obj/-/filter-obj-5.1.0.tgz", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="], "finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + "find-root": ["find-root@1.1.0", "https://registry.npmmirror.com/find-root/-/find-root-1.1.0.tgz", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], "find-up": ["find-up@5.0.0", "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + "flairup": ["flairup@1.0.0", "https://registry.npmmirror.com/flairup/-/flairup-1.0.0.tgz", {}, "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA=="], + "flat-cache": ["flat-cache@4.0.1", "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], "flatted": ["flatted@3.4.2", "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], - "follow-redirects": ["follow-redirects@1.16.0", "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + "follow-redirects": ["follow-redirects@1.16.0", "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], - "for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="], + "for-in": ["for-in@1.0.2", "https://registry.npmmirror.com/for-in/-/for-in-1.0.2.tgz", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="], "form-data": ["form-data@4.0.5", "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], @@ -1539,11 +1344,11 @@ "forwarded": ["forwarded@0.2.0", "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "framer-motion": ["framer-motion@12.38.0", "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.38.0.tgz", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], + "framer-motion": ["framer-motion@12.40.0", "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.40.0.tgz", { "dependencies": { "motion-dom": "^12.40.0", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg=="], "fresh": ["fresh@2.0.0", "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fs-extra": ["fs-extra@11.3.4", "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.4.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + "fs-extra": ["fs-extra@11.3.5", "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.5.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], "fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -1555,9 +1360,7 @@ "get-caller-file": ["get-caller-file@2.0.5", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.5.0", "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], - - "get-form-data": ["get-form-data@3.0.0", "https://registry.npmmirror.com/get-form-data/-/get-form-data-3.0.0.tgz", {}, "sha512-1d53Kn08wlPuLu31/boF1tW2WRYKw3xAWae3mqcjqpDjoqVBtXolbQnudbbEFyFWL7+2SLGRAFdotxNY06V7MA=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], "get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -1569,25 +1372,25 @@ "get-stream": ["get-stream@9.0.1", "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - "giscus": ["giscus@1.6.0", "", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="], + "get-value": ["get-value@2.0.6", "https://registry.npmmirror.com/get-value/-/get-value-2.0.6.tgz", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], + + "giscus": ["giscus@1.6.0", "https://registry.npmmirror.com/giscus/-/giscus-1.6.0.tgz", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="], + + "gitdiff-parser": ["gitdiff-parser@0.3.1", "https://registry.npmmirror.com/gitdiff-parser/-/gitdiff-parser-0.3.1.tgz", {}, "sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ=="], "glob-parent": ["glob-parent@6.0.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@16.5.0", "https://registry.npmmirror.com/globals/-/globals-16.5.0.tgz", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], - - "globby": ["globby@16.1.0", "https://registry.npmmirror.com/globby/-/globby-16.1.0.tgz", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "is-path-inside": "^4.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.4.0" } }, "sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ=="], + "globals": ["globals@17.6.0", "https://registry.npmmirror.com/globals/-/globals-17.6.0.tgz", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="], "gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "graphql": ["graphql@16.13.2", "https://registry.npmmirror.com/graphql/-/graphql-16.13.2.tgz", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="], + "graphql": ["graphql@16.14.0", "https://registry.npmmirror.com/graphql/-/graphql-16.14.0.tgz", {}, "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q=="], - "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], - - "has-flag": ["has-flag@4.0.0", "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hachure-fill": ["hachure-fill@0.5.2", "https://registry.npmmirror.com/hachure-fill/-/hachure-fill-0.5.2.tgz", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], "has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -1595,35 +1398,35 @@ "hasown": ["hasown@2.0.3", "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "https://registry.npmmirror.com/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], - "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + "hast-util-from-html": ["hast-util-from-html@2.0.3", "https://registry.npmmirror.com/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], - "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "https://registry.npmmirror.com/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "https://registry.npmmirror.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], - "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + "hast-util-is-element": ["hast-util-is-element@3.0.0", "https://registry.npmmirror.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], - "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + "hast-util-raw": ["hast-util-raw@9.1.0", "https://registry.npmmirror.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], + "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "https://registry.npmmirror.com/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], - "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "https://registry.npmmirror.com/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "https://registry.npmmirror.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "https://registry.npmmirror.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], - "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + "hast-util-to-text": ["hast-util-to-text@4.0.2", "https://registry.npmmirror.com/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "hastscript": ["hastscript@9.0.1", "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], "headers-polyfill": ["headers-polyfill@5.0.1", "https://registry.npmmirror.com/headers-polyfill/-/headers-polyfill-5.0.1.tgz", { "dependencies": { "@types/set-cookie-parser": "^2.4.10", "set-cookie-parser": "^3.0.1" } }, "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA=="], @@ -1631,35 +1434,31 @@ "hermes-parser": ["hermes-parser@0.25.1", "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + "highlight.js": ["highlight.js@11.11.1", "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], - "hono": ["hono@4.12.16", "https://registry.npmmirror.com/hono/-/hono-4.12.16.tgz", {}, "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg=="], + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + + "hono": ["hono@4.12.22", "https://registry.npmmirror.com/hono/-/hono-4.12.22.tgz", {}, "sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw=="], "html-url-attributes": ["html-url-attributes@3.0.1", "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "html-void-elements": ["html-void-elements@3.0.0", "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "http-errors": ["http-errors@2.0.1", "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "human-signals": ["human-signals@8.0.1", "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - "i": ["i@0.3.7", "", {}, "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q=="], - "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "idb": ["idb@8.0.3", "https://registry.npmmirror.com/idb/-/idb-8.0.3.tgz", {}, "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="], - "ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "immer": ["immer@10.2.0", "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], "import-fresh": ["import-fresh@3.3.1", "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "import-in-the-middle": ["import-in-the-middle@1.15.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA=="], - - "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "https://registry.npmmirror.com/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], "imurmurhash": ["imurmurhash@0.1.4", "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], @@ -1671,9 +1470,9 @@ "internmap": ["internmap@2.0.3", "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "intersection-observer": ["intersection-observer@0.12.2", "", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="], + "intersection-observer": ["intersection-observer@0.12.2", "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="], - "ip-address": ["ip-address@10.1.0", "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ip-address": ["ip-address@10.2.0", "https://registry.npmmirror.com/ip-address/-/ip-address-10.2.0.tgz", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -1683,15 +1482,13 @@ "is-arrayish": ["is-arrayish@0.2.1", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - "is-blob": ["is-blob@2.1.0", "https://registry.npmmirror.com/is-blob/-/is-blob-2.1.0.tgz", {}, "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw=="], - - "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + "is-core-module": ["is-core-module@2.16.2", "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], "is-decimal": ["is-decimal@2.0.1", "https://registry.npmmirror.com/is-decimal/-/is-decimal-2.0.1.tgz", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], "is-docker": ["is-docker@3.0.0", "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - "is-extendable": ["is-extendable@1.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4" } }, "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA=="], + "is-extendable": ["is-extendable@1.0.1", "https://registry.npmmirror.com/is-extendable/-/is-extendable-1.0.1.tgz", { "dependencies": { "is-plain-object": "^2.0.4" } }, "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA=="], "is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -1707,9 +1504,7 @@ "is-interactive": ["is-interactive@2.0.0", "https://registry.npmmirror.com/is-interactive/-/is-interactive-2.0.0.tgz", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - "is-mobile": ["is-mobile@4.0.0", "https://registry.npmmirror.com/is-mobile/-/is-mobile-4.0.0.tgz", {}, "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew=="], - - "is-network-error": ["is-network-error@1.3.1", "https://registry.npmmirror.com/is-network-error/-/is-network-error-1.3.1.tgz", {}, "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw=="], + "is-mobile": ["is-mobile@5.0.0", "https://registry.npmmirror.com/is-mobile/-/is-mobile-5.0.0.tgz", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="], "is-node-process": ["is-node-process@1.2.0", "https://registry.npmmirror.com/is-node-process/-/is-node-process-1.2.0.tgz", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], @@ -1717,11 +1512,9 @@ "is-obj": ["is-obj@3.0.0", "https://registry.npmmirror.com/is-obj/-/is-obj-3.0.0.tgz", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="], - "is-path-inside": ["is-path-inside@4.0.0", "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-4.0.0.tgz", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], - "is-plain-obj": ["is-plain-obj@4.1.0", "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], + "is-plain-object": ["is-plain-object@2.0.4", "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-2.0.4.tgz", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], "is-promise": ["is-promise@4.0.0", "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], @@ -1735,37 +1528,33 @@ "isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + "isobject": ["isobject@3.0.1", "https://registry.npmmirror.com/isobject/-/isobject-3.0.1.tgz", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], - "jiti": ["jiti@2.6.1", "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "jiti": ["jiti@2.7.0", "https://registry.npmmirror.com/jiti/-/jiti-2.7.0.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "jose": ["jose@6.2.3", "https://registry.npmmirror.com/jose/-/jose-6.2.3.tgz", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], - "js-base64": ["js-base64@3.7.8", "https://registry.npmmirror.com/js-base64/-/js-base64-3.7.8.tgz", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], - - "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], + "js-cookie": ["js-cookie@3.0.8", "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.8.tgz", {}, "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw=="], "js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "jsencrypt": ["jsencrypt@3.5.4", "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.5.4.tgz", {}, "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA=="], - "jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema": ["json-schema@0.4.0", "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], - "json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-schema-typed": ["json-schema-typed@8.0.2", "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "json2mq": ["json2mq@0.2.0", "", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="], + "json2mq": ["json2mq@0.2.0", "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="], "json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -1773,19 +1562,17 @@ "jsonpointer": ["jsonpointer@5.0.1", "https://registry.npmmirror.com/jsonpointer/-/jsonpointer-5.0.1.tgz", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], + "katex": ["katex@0.16.47", "https://registry.npmmirror.com/katex/-/katex-0.16.47.tgz", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], "keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "khroma": ["khroma@2.1.0", "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], "kleur": ["kleur@4.1.5", "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - "langium": ["langium@4.2.3", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.3", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-sOPIi4hISFnY7twwV97ca1TsxpBtXq0URu/LL1AvxwccPG/RIBBlKS7a/f/EL6w8lTNaS0EFs/F+IdSOaqYpng=="], + "layout-base": ["layout-base@1.0.2", "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - - "leva": ["leva@0.10.1", "", { "dependencies": { "@radix-ui/react-portal": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.8", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA=="], + "leva": ["leva@0.10.1", "https://registry.npmmirror.com/leva/-/leva-0.10.1.tgz", { "dependencies": { "@radix-ui/react-portal": "^1.1.4", "@radix-ui/react-tooltip": "^1.1.8", "@stitches/react": "^1.2.8", "@use-gesture/react": "^10.2.5", "colord": "^2.9.2", "dequal": "^2.0.2", "merge-value": "^1.0.0", "react-colorful": "^5.5.1", "react-dropzone": "^12.0.0", "v8n": "^1.3.3", "zustand": "^3.6.9" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-BcjnfUX8jpmwZUz2L7AfBtF9vn4ggTH33hmeufDULbP3YgNZ/C+ss/oO3stbrqRQyaOmRwy70y7BGTGO81S3rA=="], "leven": ["leven@4.1.0", "https://registry.npmmirror.com/leven/-/leven-4.1.0.tgz", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="], @@ -1817,63 +1604,45 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - "linkify-it": ["linkify-it@5.0.0", "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + "linkify-it": ["linkify-it@3.0.3", "https://registry.npmmirror.com/linkify-it/-/linkify-it-3.0.3.tgz", { "dependencies": { "uc.micro": "^1.0.1" } }, "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ=="], - "lit": ["lit@3.3.2", "", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ=="], + "lit": ["lit@3.3.3", "https://registry.npmmirror.com/lit/-/lit-3.3.3.tgz", { "dependencies": { "@lit/reactive-element": "^2.1.0", "lit-element": "^4.2.0", "lit-html": "^3.3.0" } }, "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw=="], - "lit-element": ["lit-element@4.2.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w=="], + "lit-element": ["lit-element@4.2.2", "https://registry.npmmirror.com/lit-element/-/lit-element-4.2.2.tgz", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0", "@lit/reactive-element": "^2.1.0", "lit-html": "^3.3.0" } }, "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w=="], - "lit-html": ["lit-html@3.3.2", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw=="], + "lit-html": ["lit-html@3.3.3", "https://registry.npmmirror.com/lit-html/-/lit-html-3.3.3.tgz", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA=="], "locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash": ["lodash@4.18.1", "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - - "lodash._baseiteratee": ["lodash._baseiteratee@4.7.0", "https://registry.npmmirror.com/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz", { "dependencies": { "lodash._stringtopath": "~4.8.0" } }, "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ=="], - - "lodash._basetostring": ["lodash._basetostring@4.12.0", "https://registry.npmmirror.com/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz", {}, "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw=="], - - "lodash._baseuniq": ["lodash._baseuniq@4.6.0", "https://registry.npmmirror.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz", { "dependencies": { "lodash._createset": "~4.0.0", "lodash._root": "~3.0.0" } }, "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A=="], - - "lodash._createset": ["lodash._createset@4.0.3", "https://registry.npmmirror.com/lodash._createset/-/lodash._createset-4.0.3.tgz", {}, "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA=="], - - "lodash._root": ["lodash._root@3.0.1", "https://registry.npmmirror.com/lodash._root/-/lodash._root-3.0.1.tgz", {}, "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ=="], - - "lodash._stringtopath": ["lodash._stringtopath@4.8.0", "https://registry.npmmirror.com/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz", { "dependencies": { "lodash._basetostring": "~4.12.0" } }, "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ=="], - - "lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "lodash.throttle": ["lodash.throttle@4.1.1", "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], - - "lodash.uniqby": ["lodash.uniqby@4.5.0", "https://registry.npmmirror.com/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz", { "dependencies": { "lodash._baseiteratee": "~4.7.0", "lodash._baseuniq": "~4.6.0" } }, "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ=="], + "lodash-es": ["lodash-es@4.18.1", "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], "log-symbols": ["log-symbols@6.0.0", "https://registry.npmmirror.com/log-symbols/-/log-symbols-6.0.0.tgz", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "longest-streak": ["longest-streak@3.1.0", "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lowlight": ["lowlight@3.3.0", "https://registry.npmmirror.com/lowlight/-/lowlight-3.3.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], "lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + "lru_map": ["lru_map@0.4.1", "https://registry.npmmirror.com/lru_map/-/lru_map-0.4.1.tgz", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], - "lucide-react": ["lucide-react@1.14.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.14.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA=="], + "lucide-react": ["lucide-react@1.17.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.17.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], "lunr": ["lunr@2.3.9", "https://registry.npmmirror.com/lunr/-/lunr-2.3.9.tgz", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="], "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + "markdown-extensions": ["markdown-extensions@2.0.0", "https://registry.npmmirror.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], - "markdown-it": ["markdown-it@14.1.1", "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.1.tgz", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], + "markdown-it": ["markdown-it@14.2.0", "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.2.0.tgz", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="], "markdown-table": ["markdown-table@3.0.4", "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + "marked": ["marked@17.0.6", "https://registry.npmmirror.com/marked/-/marked-17.0.6.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -1893,9 +1662,9 @@ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + "mdast-util-math": ["mdast-util-math@3.0.0", "https://registry.npmmirror.com/mdast-util-math/-/mdast-util-math-3.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], - "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "https://registry.npmmirror.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "https://registry.npmmirror.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], @@ -1903,7 +1672,7 @@ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "https://registry.npmmirror.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], - "mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="], + "mdast-util-newline-to-break": ["mdast-util-newline-to-break@2.0.0", "https://registry.npmmirror.com/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0" } }, "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog=="], "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], @@ -1917,25 +1686,27 @@ "media-typer": ["media-typer@1.1.0", "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + "memoize-one": ["memoize-one@5.2.1", "https://registry.npmmirror.com/memoize-one/-/memoize-one-5.2.1.tgz", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "merge-stream": ["merge-stream@2.0.0", "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - "merge-value": ["merge-value@1.0.0", "", { "dependencies": { "get-value": "^2.0.6", "is-extendable": "^1.0.0", "mixin-deep": "^1.2.0", "set-value": "^2.0.0" } }, "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ=="], + "merge-value": ["merge-value@1.0.0", "https://registry.npmmirror.com/merge-value/-/merge-value-1.0.0.tgz", { "dependencies": { "get-value": "^2.0.6", "is-extendable": "^1.0.0", "mixin-deep": "^1.2.0", "set-value": "^2.0.0" } }, "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ=="], "merge2": ["merge2@1.4.1", "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="], + "mermaid": ["mermaid@11.15.0", "https://registry.npmmirror.com/mermaid/-/mermaid-11.15.0.tgz", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="], "micromark": ["micromark@4.0.2", "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="], + "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "https://registry.npmmirror.com/micromark-extension-cjk-friendly/-/micromark-extension-cjk-friendly-2.0.1.tgz", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="], - "micromark-extension-cjk-friendly-gfm-strikethrough": ["micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "get-east-asian-width": "^1.4.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-character": "^2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-wVC0zwjJNqQeX+bb07YTPu/CvSAyCTafyYb7sMhX1r62/Lw5M/df3JyYaANyp8g15c1ypJRFSsookTqA1IDsUg=="], + "micromark-extension-cjk-friendly-gfm-strikethrough": ["micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1", "https://registry.npmmirror.com/micromark-extension-cjk-friendly-gfm-strikethrough/-/micromark-extension-cjk-friendly-gfm-strikethrough-2.0.1.tgz", { "dependencies": { "devlop": "^1.1.0", "get-east-asian-width": "^1.4.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-character": "^2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-wVC0zwjJNqQeX+bb07YTPu/CvSAyCTafyYb7sMhX1r62/Lw5M/df3JyYaANyp8g15c1ypJRFSsookTqA1IDsUg=="], - "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="], + "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "https://registry.npmmirror.com/micromark-extension-cjk-friendly-util/-/micromark-extension-cjk-friendly-util-3.0.1.tgz", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark-util-types": "*" }, "optionalPeers": ["micromark-util-types"] }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="], "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "https://registry.npmmirror.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], @@ -1951,23 +1722,23 @@ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + "micromark-extension-math": ["micromark-extension-math@3.1.0", "https://registry.npmmirror.com/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], - "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "https://registry.npmmirror.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], - "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "https://registry.npmmirror.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], - "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "https://registry.npmmirror.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], - "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "https://registry.npmmirror.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], - "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "https://registry.npmmirror.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "https://registry.npmmirror.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], "micromark-factory-label": ["micromark-factory-label@2.0.1", "https://registry.npmmirror.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "https://registry.npmmirror.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], "micromark-factory-space": ["micromark-factory-space@2.0.1", "https://registry.npmmirror.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], @@ -1989,7 +1760,7 @@ "micromark-util-encode": ["micromark-util-encode@2.0.1", "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "https://registry.npmmirror.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "https://registry.npmmirror.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], @@ -2009,41 +1780,37 @@ "mime-db": ["mime-db@1.52.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "mime-match": ["mime-match@1.0.2", "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz", { "dependencies": { "wildcard": "^1.1.0" } }, "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg=="], - "mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "mimic-fn": ["mimic-fn@2.1.0", "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "mimic-function": ["mimic-function@5.0.1", "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "mixin-deep": ["mixin-deep@1.3.2", "", { "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" } }, "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA=="], + "mixin-deep": ["mixin-deep@1.3.2", "https://registry.npmmirror.com/mixin-deep/-/mixin-deep-1.3.2.tgz", { "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" } }, "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA=="], - "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], + "monaco-editor": ["monaco-editor@0.55.1", "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.55.1.tgz", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], - "motion": ["motion@12.38.0", "https://registry.npmmirror.com/motion/-/motion-12.38.0.tgz", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="], + "motion": ["motion@12.40.0", "https://registry.npmmirror.com/motion/-/motion-12.40.0.tgz", { "dependencies": { "framer-motion": "^12.40.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA=="], - "motion-dom": ["motion-dom@12.38.0", "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.38.0.tgz", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], + "motion-dom": ["motion-dom@12.40.0", "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.40.0.tgz", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg=="], - "motion-utils": ["motion-utils@12.36.0", "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.36.0.tgz", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], + "motion-utils": ["motion-utils@12.39.0", "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.39.0.tgz", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msw": ["msw@2.14.2", "https://registry.npmmirror.com/msw/-/msw-2.14.2.tgz", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.7", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-D2bTe0tpuf9nw4DA39wFaqUD/hRPKj0DKpo2lAqu+A47Ifg4+h0hbfn6QxVOsiUY2uhgEN6TTpGSHDsc+ysYNg=="], + "msw": ["msw@2.14.6", "https://registry.npmmirror.com/msw/-/msw-2.14.6.tgz", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.11", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg=="], "mute-stream": ["mute-stream@3.0.0", "https://registry.npmmirror.com/mute-stream/-/mute-stream-3.0.0.tgz", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], - "namespace-emitter": ["namespace-emitter@2.0.1", "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz", {}, "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g=="], - "nanoid": ["nanoid@5.1.11", "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.11.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg=="], "natural-compare": ["natural-compare@1.4.0", "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "negotiator": ["negotiator@0.6.3", "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "negotiator": ["negotiator@1.0.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "next-themes": ["next-themes@0.4.6", "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.6.tgz", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -2051,11 +1818,11 @@ "node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "node-releases": ["node-releases@2.0.38", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + "node-releases": ["node-releases@2.0.46", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.46.tgz", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], "npm-run-path": ["npm-run-path@6.0.0", "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-6.0.0.tgz", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - "numeral": ["numeral@2.0.6", "", {}, "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="], + "numeral": ["numeral@2.0.6", "https://registry.npmmirror.com/numeral/-/numeral-2.0.6.tgz", {}, "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="], "object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -2063,7 +1830,7 @@ "object-treeify": ["object-treeify@1.1.33", "https://registry.npmmirror.com/object-treeify/-/object-treeify-1.1.33.tgz", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], - "on-change": ["on-change@4.0.2", "", {}, "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA=="], + "on-change": ["on-change@4.0.2", "https://registry.npmmirror.com/on-change/-/on-change-4.0.2.tgz", {}, "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA=="], "on-finished": ["on-finished@2.4.1", "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -2071,9 +1838,9 @@ "onetime": ["onetime@5.1.2", "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + "oniguruma-parser": ["oniguruma-parser@0.12.2", "https://registry.npmmirror.com/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "https://registry.npmmirror.com/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], "open": ["open@11.0.0", "https://registry.npmmirror.com/open/-/open-11.0.0.tgz", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], @@ -2081,7 +1848,7 @@ "ora": ["ora@8.2.0", "https://registry.npmmirror.com/ora/-/ora-8.2.0.tgz", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], - "orval": ["orval@8.9.0", "https://registry.npmmirror.com/orval/-/orval-8.9.0.tgz", { "dependencies": { "@commander-js/extra-typings": "^14.0.0", "@orval/angular": "8.9.0", "@orval/axios": "8.9.0", "@orval/core": "8.9.0", "@orval/fetch": "8.9.0", "@orval/hono": "8.9.0", "@orval/mcp": "8.9.0", "@orval/mock": "8.9.0", "@orval/query": "8.9.0", "@orval/solid-start": "8.9.0", "@orval/swr": "8.9.0", "@orval/zod": "8.9.0", "@scalar/json-magic": "^0.12.8", "@scalar/openapi-parser": "^0.25.12", "@scalar/openapi-types": "0.8.0", "chokidar": "^5.0.0", "commander": "^14.0.2", "enquirer": "^2.4.1", "execa": "^9.6.1", "find-up": "8.0.0", "fs-extra": "^11.3.2", "jiti": "^2.6.1", "js-yaml": "4.1.1", "remeda": "^2.33.6", "string-argv": "^0.3.2", "tsconfck": "^3.1.6", "typedoc": "^0.28.17", "typedoc-plugin-coverage": "^4.0.2", "typedoc-plugin-markdown": "^4.10.0" }, "peerDependencies": { "prettier": ">=3.0.0" }, "optionalPeers": ["prettier"], "bin": { "orval": "dist/bin/orval.mjs" } }, "sha512-KjXCXWHFbKIRouWeP5ZOy/4YO60KHaElyvCcrLO3+JEYNvcpy79IQbF8qV1akdfxFP0+9RXJg1fDVibo95681A=="], + "orval": ["orval@8.12.3", "https://registry.npmmirror.com/orval/-/orval-8.12.3.tgz", { "dependencies": { "@commander-js/extra-typings": "^14.0.0", "@orval/angular": "8.12.3", "@orval/axios": "8.12.3", "@orval/core": "8.12.3", "@orval/fetch": "8.12.3", "@orval/hono": "8.12.3", "@orval/mcp": "8.12.3", "@orval/mock": "8.12.3", "@orval/query": "8.12.3", "@orval/solid-start": "8.12.3", "@orval/swr": "8.12.3", "@orval/zod": "8.12.3", "@scalar/json-magic": "^0.12.8", "@scalar/openapi-parser": "^0.25.12", "@scalar/openapi-types": "0.8.0", "chokidar": "^5.0.0", "commander": "^14.0.2", "enquirer": "^2.4.1", "execa": "^9.6.1", "find-up": "8.0.0", "fs-extra": "^11.3.2", "get-tsconfig": "^4.14.0", "jiti": "^2.6.1", "js-yaml": "4.1.1", "remeda": "^2.33.6", "string-argv": "^0.3.2", "typedoc": "^0.28.19", "typedoc-plugin-coverage": "^4.0.2", "typedoc-plugin-markdown": "^4.10.0" }, "peerDependencies": { "prettier": ">=3.0.0" }, "optionalPeers": ["prettier"], "bin": { "orval": "dist/bin/orval.mjs" } }, "sha512-j9kGaPcI2ufzE2kflj0oDXKAqnayZU7Dlm2mqDR0pxVLsJE+pCSKgwQ2iiGI/fo2O3IlQpIR77hlr78OmU3PoA=="], "outvariant": ["outvariant@1.4.3", "https://registry.npmmirror.com/outvariant/-/outvariant-1.4.3.tgz", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], @@ -2089,13 +1856,7 @@ "p-locate": ["p-locate@5.0.0", "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-queue": ["p-queue@8.1.1", "https://registry.npmmirror.com/p-queue/-/p-queue-8.1.1.tgz", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], - - "p-retry": ["p-retry@6.2.1", "https://registry.npmmirror.com/p-retry/-/p-retry-6.2.1.tgz", { "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", "retry": "^0.13.1" } }, "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ=="], - - "p-timeout": ["p-timeout@6.1.4", "https://registry.npmmirror.com/p-timeout/-/p-timeout-6.1.4.tgz", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], - - "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.6.0.tgz", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "parent-module": ["parent-module@1.0.1", "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -2105,23 +1866,23 @@ "parse-ms": ["parse-ms@4.0.0", "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parseurl": ["parseurl@1.3.3", "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-browserify": ["path-browserify@1.0.1", "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], - "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "path-data-parser": ["path-data-parser@0.1.0", "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], "path-exists": ["path-exists@4.0.0", "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-parse": ["path-parse@1.0.7", "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "path-to-regexp": ["path-to-regexp@6.3.0", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "path-type": ["path-type@4.0.0", "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -2131,40 +1892,28 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + "points-on-curve": ["points-on-curve@0.2.0", "https://registry.npmmirror.com/points-on-curve/-/points-on-curve-0.2.0.tgz", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], - "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + "points-on-path": ["points-on-path@0.2.1", "https://registry.npmmirror.com/points-on-path/-/points-on-path-0.2.1.tgz", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], - "polished": ["polished@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], + "polished": ["polished@4.3.1", "https://registry.npmmirror.com/polished/-/polished-4.3.1.tgz", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="], - "postcss": ["postcss@8.5.13", "https://registry.npmmirror.com/postcss/-/postcss-8.5.13.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="], + "postcss": ["postcss@8.5.15", "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], "powershell-utils": ["powershell-utils@0.1.0", "https://registry.npmmirror.com/powershell-utils/-/powershell-utils-0.1.0.tgz", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - "preact": ["preact@10.29.1", "https://registry.npmmirror.com/preact/-/preact-10.29.1.tgz", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="], - "prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.8.3", "https://registry.npmmirror.com/prettier/-/prettier-3.8.3.tgz", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], - - "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.4", "https://registry.npmmirror.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.4.tgz", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-UKii4RjY05SNt/WQi6/NcOn/LsT0/ILLXsxygjbRg5/YZelsSu5jTqorYHPDGq4nZy5q5hpCu+XdGZ1xaJEQgw=="], - "pretty-ms": ["pretty-ms@9.3.0", "https://registry.npmmirror.com/pretty-ms/-/pretty-ms-9.3.0.tgz", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], - "promise-queue": ["promise-queue@2.2.5", "https://registry.npmmirror.com/promise-queue/-/promise-queue-2.2.5.tgz", {}, "sha512-p/iXrPSVfnqPft24ZdNNLECw/UrtLTpT3jpAAMzl/o5/rDsGCPo3/CQS2611flL6LkoEJ3oQZw7C8Q80ZISXRQ=="], - "prompts": ["prompts@2.4.2", "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - - "proper-lockfile": ["proper-lockfile@4.1.2", "https://registry.npmmirror.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "prop-types": ["prop-types@15.8.1", "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "property-information": ["property-information@7.1.0", "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "protobufjs": ["protobufjs@7.5.8", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA=="], - "proxy-addr": ["proxy-addr@2.0.7", "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], "proxy-from-env": ["proxy-from-env@2.1.0", "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], @@ -2173,143 +1922,149 @@ "punycode.js": ["punycode.js@2.3.1", "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], - "qs": ["qs@6.15.1", "https://registry.npmmirror.com/qs/-/qs-6.15.1.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], + "qs": ["qs@6.15.2", "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="], - "query-string": ["query-string@9.3.1", "", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw=="], - - "querystringify": ["querystringify@2.2.0", "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], + "query-string": ["query-string@9.4.0", "https://registry.npmmirror.com/query-string/-/query-string-9.4.0.tgz", { "dependencies": { "decode-uri-component": "^0.4.1", "filter-obj": "^5.1.0", "split-on-first": "^3.0.0" } }, "sha512-ivvWyHqU9K1Log4hJFhqVIIMoEi0nzmlRhvk2pPcTuQH/Y0K5iTTMxEx7R0PRHD2Z1hMVbWnjfsEWbIKIK+3IA=="], "queue-microtask": ["queue-microtask@1.2.3", "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "radix-ui": ["radix-ui@1.4.3", "https://registry.npmmirror.com/radix-ui/-/radix-ui-1.4.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], - "range-parser": ["range-parser@1.2.1", "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@3.0.2", "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], + "rc-collapse": ["rc-collapse@4.0.0", "https://registry.npmmirror.com/rc-collapse/-/rc-collapse-4.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="], - "rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="], + "rc-dialog": ["rc-dialog@9.6.0", "https://registry.npmmirror.com/rc-dialog/-/rc-dialog-9.6.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="], - "rc-footer": ["rc-footer@0.6.8", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-JBZ+xcb6kkex8XnBd4VHw1ZxjV6kmcwUumSHaIFdka2qzMCo7Klcy4sI6G0XtUpG/vtpislQCc+S9Bc+NLHYMg=="], + "rc-footer": ["rc-footer@0.6.8", "https://registry.npmmirror.com/rc-footer/-/rc-footer-0.6.8.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-JBZ+xcb6kkex8XnBd4VHw1ZxjV6kmcwUumSHaIFdka2qzMCo7Klcy4sI6G0XtUpG/vtpislQCc+S9Bc+NLHYMg=="], - "rc-image": ["rc-image@7.12.0", "", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q=="], + "rc-image": ["rc-image@7.12.0", "https://registry.npmmirror.com/rc-image/-/rc-image-7.12.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q=="], - "rc-input": ["rc-input@1.8.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.18.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA=="], + "rc-input": ["rc-input@1.8.0", "https://registry.npmmirror.com/rc-input/-/rc-input-1.8.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.18.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA=="], - "rc-input-number": ["rc-input-number@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", "rc-input": "~1.8.0", "rc-util": "^5.40.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag=="], + "rc-input-number": ["rc-input-number@9.5.0", "https://registry.npmmirror.com/rc-input-number/-/rc-input-number-9.5.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", "rc-input": "~1.8.0", "rc-util": "^5.40.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag=="], - "rc-menu": ["rc-menu@9.16.1", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg=="], + "rc-menu": ["rc-menu@9.16.1", "https://registry.npmmirror.com/rc-menu/-/rc-menu-9.16.1.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg=="], - "rc-motion": ["rc-motion@2.9.5", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA=="], + "rc-motion": ["rc-motion@2.9.5", "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA=="], - "rc-overflow": ["rc-overflow@1.5.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.37.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg=="], + "rc-overflow": ["rc-overflow@1.5.0", "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.5.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.37.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg=="], - "rc-resize-observer": ["rc-resize-observer@1.4.3", "", { "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", "rc-util": "^5.44.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ=="], + "rc-resize-observer": ["rc-resize-observer@1.4.3", "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", { "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", "rc-util": "^5.44.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ=="], - "rc-util": ["rc-util@5.44.4", "", { "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w=="], + "rc-util": ["rc-util@5.44.4", "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w=="], - "re-resizable": ["re-resizable@6.11.2", "", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="], + "re-resizable": ["re-resizable@6.11.2", "https://registry.npmmirror.com/re-resizable/-/re-resizable-6.11.2.tgz", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="], - "react": ["react@19.2.5", "https://registry.npmmirror.com/react/-/react-19.2.5.tgz", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], + "react": ["react@19.2.6", "https://registry.npmmirror.com/react/-/react-19.2.6.tgz", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - "react-avatar-editor": ["react-avatar-editor@15.1.0", "", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Zto7u9l6Wd5LPPtjeFJ+7uwoT4bs01OSgkN2kxD18lWl8IiZ0GY3nWCbKPx4qIU7Au1vENsMJm19rfVWHHayaQ=="], + "react-arborist": ["react-arborist@3.7.0", "https://registry.npmmirror.com/react-arborist/-/react-arborist-3.7.0.tgz", { "dependencies": { "react-dnd": "^14.0.3", "react-dnd-html5-backend": "^14.0.3", "react-window": "^1.8.11", "redux": "^5.0.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": ">= 16.14", "react-dom": ">= 16.14" } }, "sha512-gh2SoO0eXQVSP6zxXMGqFeXF+l2uabDGBVn0+RKqy/s7mrG5xGnfM5mhyB67cMVobC3vWYLqe6HGh7ZEZadW/w=="], - "react-colorful": ["react-colorful@5.6.2", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-7Vankf05ygS7v4T1gJPxqNIJZcsZ46K71J3fF995cfYOMFskAkFYUXna+90bwK/dr/1zVqrJQorNuc9OTV/qXA=="], + "react-avatar-editor": ["react-avatar-editor@15.1.0", "https://registry.npmmirror.com/react-avatar-editor/-/react-avatar-editor-15.1.0.tgz", { "peerDependencies": { "react": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Zto7u9l6Wd5LPPtjeFJ+7uwoT4bs01OSgkN2kxD18lWl8IiZ0GY3nWCbKPx4qIU7Au1vENsMJm19rfVWHHayaQ=="], - "react-day-picker": ["react-day-picker@9.14.0", "https://registry.npmmirror.com/react-day-picker/-/react-day-picker-9.14.0.tgz", { "dependencies": { "@date-fns/tz": "^1.4.1", "@tabby_ai/hijri-converter": "1.0.5", "date-fns": "^4.1.0", "date-fns-jalali": "4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA=="], + "react-colorful": ["react-colorful@5.7.0", "https://registry.npmmirror.com/react-colorful/-/react-colorful-5.7.0.tgz", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg=="], - "react-dom": ["react-dom@19.2.5", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.5.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + "react-day-picker": ["react-day-picker@10.0.1", "https://registry.npmmirror.com/react-day-picker/-/react-day-picker-10.0.1.tgz", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w=="], - "react-draggable": ["react-draggable@4.5.0", "", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="], + "react-diff-view": ["react-diff-view@3.3.3", "https://registry.npmmirror.com/react-diff-view/-/react-diff-view-3.3.3.tgz", { "dependencies": { "classnames": "^2.3.2", "diff-match-patch": "^1.0.5", "gitdiff-parser": "^0.3.1", "lodash": "^4.17.21", "shallow-equal": "^3.1.0", "warning": "^4.0.3" }, "peerDependencies": { "react": ">=16.14.0" } }, "sha512-CPveApk6n7ZbkW7T6PoptR7LWAvD9hohTHZ7WnKnu3GZkTfUB5rvg486apPo94iYVi4fZd3Nt+rtBZ5877exoQ=="], - "react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="], + "react-dnd": ["react-dnd@14.0.5", "https://registry.npmmirror.com/react-dnd/-/react-dnd-14.0.5.tgz", { "dependencies": { "@react-dnd/invariant": "^2.0.0", "@react-dnd/shallowequal": "^2.0.0", "dnd-core": "14.0.1", "fast-deep-equal": "^3.1.3", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "@types/hoist-non-react-statics": ">= 3.3.1", "@types/node": ">= 12", "@types/react": ">= 16", "react": ">= 16.14" }, "optionalPeers": ["@types/hoist-non-react-statics", "@types/node", "@types/react"] }, "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A=="], - "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="], + "react-dnd-html5-backend": ["react-dnd-html5-backend@14.1.0", "https://registry.npmmirror.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz", { "dependencies": { "dnd-core": "14.0.1" } }, "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw=="], - "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], + "react-dom": ["react-dom@19.2.6", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.6.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], - "react-hook-form": ["react-hook-form@7.75.0", "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.75.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw=="], + "react-draggable": ["react-draggable@4.5.0", "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.5.0.tgz", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="], - "react-hotkeys-hook": ["react-hotkeys-hook@5.3.2", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-DDDy9xK6mbTQ6aPlQvIl0dA/a90T/AWml4Rm21JXFDLlRHalIg4/Rv3equUQYs5xPTWq+oEl6RD7mi/nBpU3Uw=="], + "react-dropzone": ["react-dropzone@12.1.0", "https://registry.npmmirror.com/react-dropzone/-/react-dropzone-12.1.0.tgz", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="], - "react-is": ["react-is@19.2.5", "https://registry.npmmirror.com/react-is/-/react-is-19.2.5.tgz", {}, "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ=="], + "react-error-boundary": ["react-error-boundary@6.1.2", "https://registry.npmmirror.com/react-error-boundary/-/react-error-boundary-6.1.2.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng=="], + + "react-fast-compare": ["react-fast-compare@3.2.2", "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], + + "react-hotkeys-hook": ["react-hotkeys-hook@5.3.2", "https://registry.npmmirror.com/react-hotkeys-hook/-/react-hotkeys-hook-5.3.2.tgz", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-DDDy9xK6mbTQ6aPlQvIl0dA/a90T/AWml4Rm21JXFDLlRHalIg4/Rv3equUQYs5xPTWq+oEl6RD7mi/nBpU3Uw=="], + + "react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "react-jsx-parser": ["react-jsx-parser@2.4.1", "https://registry.npmmirror.com/react-jsx-parser/-/react-jsx-parser-2.4.1.tgz", { "dependencies": { "acorn": "^8.12.1", "acorn-jsx": "^5.3.2" }, "optionalDependencies": { "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-gE25DiHseuKmSbb5O1K1IkyQ/bwth3zl4z3EdW8XMhwrb2agUpuh8GvqHFvGwh/wdZjDDuadGrSR2lEgjcOvqQ=="], "react-markdown": ["react-markdown@10.1.0", "https://registry.npmmirror.com/react-markdown/-/react-markdown-10.1.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], - "react-merge-refs": ["react-merge-refs@3.0.2", "", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["react"] }, "sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw=="], + "react-merge-refs": ["react-merge-refs@3.0.2", "https://registry.npmmirror.com/react-merge-refs/-/react-merge-refs-3.0.2.tgz", { "peerDependencies": { "react": ">=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["react"] }, "sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw=="], - "react-redux": ["react-redux@9.2.0", "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], - - "react-refresh": ["react-refresh@0.18.0", "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "react-redux": ["react-redux@9.3.0", "https://registry.npmmirror.com/react-redux/-/react-redux-9.3.0.tgz", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="], "react-remove-scroll": ["react-remove-scroll@2.7.2", "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-resizable-panels": ["react-resizable-panels@4.10.0", "https://registry.npmmirror.com/react-resizable-panels/-/react-resizable-panels-4.10.0.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-frjewRQt7TCv/vCH1pJfjZ7RxAhr5pKuqVQtVgzFq/vherxBFOWyC3xMbryx5Ti2wylViGUFc93Etg4rB3E0UA=="], + "react-resizable-panels": ["react-resizable-panels@4.11.2", "https://registry.npmmirror.com/react-resizable-panels/-/react-resizable-panels-4.11.2.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-+kfFbDZ8mygc7g0vxOcDzCVGuwiIUOnILqPoUHo6/uP+Mmyx6HzZU+kj1aOPDlktXuobYbr6BtQekvJwHRX4Eg=="], - "react-rnd": ["react-rnd@10.5.3", "", { "dependencies": { "re-resizable": "^6.11.2", "react-draggable": "^4.5.0", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q=="], + "react-rnd": ["react-rnd@10.5.3", "https://registry.npmmirror.com/react-rnd/-/react-rnd-10.5.3.tgz", { "dependencies": { "re-resizable": "^6.11.2", "react-draggable": "^4.5.0", "tslib": "2.6.2" }, "peerDependencies": { "react": ">=16.3.0", "react-dom": ">=16.3.0" } }, "sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q=="], - "react-router": ["react-router@7.14.2", "https://registry.npmmirror.com/react-router/-/react-router-7.14.2.tgz", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw=="], + "react-router": ["react-router@7.15.1", "https://registry.npmmirror.com/react-router/-/react-router-7.15.1.tgz", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A=="], - "react-router-dom": ["react-router-dom@7.14.2", "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.14.2.tgz", { "dependencies": { "react-router": "7.14.2" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ=="], + "react-router-dom": ["react-router-dom@7.15.1", "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.15.1.tgz", { "dependencies": { "react-router": "7.15.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg=="], "react-style-singleton": ["react-style-singleton@2.2.3", "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "react-virtuoso": ["react-virtuoso@4.18.6", "", { "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", "react-dom": ">=16 || >=17 || >= 18 || >=19" } }, "sha512-CrT3P6HyjJMHZVWSste2bG2q5aWGlHfW2QuySZjiFwB2Qok/xsvgy+k8Z2jeDP8PP5KsBip7zNrl/F0QoxeyKw=="], + "react-virtuoso": ["react-virtuoso@4.18.7", "https://registry.npmmirror.com/react-virtuoso/-/react-virtuoso-4.18.7.tgz", { "peerDependencies": { "react": ">=16 || >=17 || >= 18 || >= 19", "react-dom": ">=16 || >=17 || >= 18 || >=19" } }, "sha512-xNF5zDGEEIMB7cKwcen/pLig0YDf6OnfFrVgKFa7sHPf9fRem0CaLshyObbBcP88jzn0enavL39EgplgdyT21g=="], - "react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="], + "react-window": ["react-window@1.8.11", "https://registry.npmmirror.com/react-window/-/react-window-1.8.11.tgz", { "dependencies": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" }, "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ=="], + + "react-zoom-pan-pinch": ["react-zoom-pan-pinch@3.7.0", "https://registry.npmmirror.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz", { "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA=="], "readdirp": ["readdirp@5.0.0", "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "recast": ["recast@0.23.11", "https://registry.npmmirror.com/recast/-/recast-0.23.11.tgz", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], - "recharts": ["recharts@3.8.1", "https://registry.npmmirror.com/recharts/-/recharts-3.8.1.tgz", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="], + "recharts": ["recharts@3.8.0", "https://registry.npmmirror.com/recharts/-/recharts-3.8.0.tgz", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="], - "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + "recma-build-jsx": ["recma-build-jsx@1.0.0", "https://registry.npmmirror.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], - "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + "recma-jsx": ["recma-jsx@1.0.1", "https://registry.npmmirror.com/recma-jsx/-/recma-jsx-1.0.1.tgz", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], - "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + "recma-parse": ["recma-parse@1.0.0", "https://registry.npmmirror.com/recma-parse/-/recma-parse-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], - "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "recma-stringify": ["recma-stringify@1.0.0", "https://registry.npmmirror.com/recma-stringify/-/recma-stringify-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], "redux": ["redux@5.0.1", "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], "redux-thunk": ["redux-thunk@3.1.0", "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], - "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + "regex": ["regex@6.1.0", "https://registry.npmmirror.com/regex/-/regex-6.1.0.tgz", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], - "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + "regex-recursion": ["regex-recursion@6.0.2", "https://registry.npmmirror.com/regex-recursion/-/regex-recursion-6.0.2.tgz", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], - "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "regex-utilities": ["regex-utilities@2.3.0", "https://registry.npmmirror.com/regex-utilities/-/regex-utilities-2.3.0.tgz", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], - "rehype-github-alerts": ["rehype-github-alerts@4.2.0", "", { "dependencies": { "@primer/octicons": "^19.20.0", "hast-util-from-html": "^2.0.3", "hast-util-is-element": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-6di6kEu9WUHKLKrkKG2xX6AOuaCMGghg0Wq7MEuM/jBYUPVIq6PJpMe00dxMfU+/YSBtDXhffpDimgDi+BObIQ=="], + "rehype-github-alerts": ["rehype-github-alerts@4.2.0", "https://registry.npmmirror.com/rehype-github-alerts/-/rehype-github-alerts-4.2.0.tgz", { "dependencies": { "@primer/octicons": "^19.20.0", "hast-util-from-html": "^2.0.3", "hast-util-is-element": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-6di6kEu9WUHKLKrkKG2xX6AOuaCMGghg0Wq7MEuM/jBYUPVIq6PJpMe00dxMfU+/YSBtDXhffpDimgDi+BObIQ=="], - "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], + "rehype-harden": ["rehype-harden@1.1.8", "https://registry.npmmirror.com/rehype-harden/-/rehype-harden-1.1.8.tgz", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], - "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], + "rehype-highlight": ["rehype-highlight@7.0.2", "https://registry.npmmirror.com/rehype-highlight/-/rehype-highlight-7.0.2.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + "rehype-katex": ["rehype-katex@7.0.1", "https://registry.npmmirror.com/rehype-katex/-/rehype-katex-7.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], - "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + "rehype-raw": ["rehype-raw@7.0.0", "https://registry.npmmirror.com/rehype-raw/-/rehype-raw-7.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], + "rehype-recma": ["rehype-recma@1.0.0", "https://registry.npmmirror.com/rehype-recma/-/rehype-recma-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], - "remark-breaks": ["remark-breaks@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], + "rehype-sanitize": ["rehype-sanitize@6.0.0", "https://registry.npmmirror.com/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], - "remark-cjk-friendly": ["remark-cjk-friendly@2.0.1", "", { "dependencies": { "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA=="], + "remark-breaks": ["remark-breaks@4.0.0", "https://registry.npmmirror.com/remark-breaks/-/remark-breaks-4.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-newline-to-break": "^2.0.0", "unified": "^11.0.0" } }, "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ=="], - "remark-cjk-friendly-gfm-strikethrough": ["remark-cjk-friendly-gfm-strikethrough@2.0.1", "", { "dependencies": { "micromark-extension-cjk-friendly-gfm-strikethrough": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-pWKj25O2eLXIL1aBupayl1fKhco+Brw8qWUWJPVB9EBzbQNd7nGLj0nLmJpggWsGLR5j5y40PIdjxby9IEYTuA=="], + "remark-cjk-friendly": ["remark-cjk-friendly@2.0.1", "https://registry.npmmirror.com/remark-cjk-friendly/-/remark-cjk-friendly-2.0.1.tgz", { "dependencies": { "micromark-extension-cjk-friendly": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA=="], + + "remark-cjk-friendly-gfm-strikethrough": ["remark-cjk-friendly-gfm-strikethrough@2.0.1", "https://registry.npmmirror.com/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.0.1.tgz", { "dependencies": { "micromark-extension-cjk-friendly-gfm-strikethrough": "2.0.1" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-pWKj25O2eLXIL1aBupayl1fKhco+Brw8qWUWJPVB9EBzbQNd7nGLj0nLmJpggWsGLR5j5y40PIdjxby9IEYTuA=="], "remark-gfm": ["remark-gfm@4.0.1", "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - "remark-github": ["remark-github@12.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0", "mdast-util-to-string": "^4.0.0", "to-vfile": "^8.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-ByefQKFN184LeiGRCabfl7zUJsdlMYWEhiLX1gpmQ11yFg6xSuOTW7LVCv0oc1x+YvUMJW23NU36sJX2RWGgvg=="], + "remark-github": ["remark-github@12.0.0", "https://registry.npmmirror.com/remark-github/-/remark-github-12.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-find-and-replace": "^3.0.0", "mdast-util-to-string": "^4.0.0", "to-vfile": "^8.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-ByefQKFN184LeiGRCabfl7zUJsdlMYWEhiLX1gpmQ11yFg6xSuOTW7LVCv0oc1x+YvUMJW23NU36sJX2RWGgvg=="], - "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], + "remark-math": ["remark-math@6.0.0", "https://registry.npmmirror.com/remark-math/-/remark-math-6.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], - "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + "remark-mdx": ["remark-mdx@3.1.1", "https://registry.npmmirror.com/remark-mdx/-/remark-mdx-3.1.1.tgz", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], "remark-parse": ["remark-parse@11.0.0", "https://registry.npmmirror.com/remark-parse/-/remark-parse-11.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], @@ -2317,39 +2072,35 @@ "remark-stringify": ["remark-stringify@11.0.0", "https://registry.npmmirror.com/remark-stringify/-/remark-stringify-11.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - "remeda": ["remeda@2.34.0", "https://registry.npmmirror.com/remeda/-/remeda-2.34.0.tgz", {}, "sha512-zL4cEPkLHxwmlDRPyvJZjojpG5M5HXrDiABNKof+dq7kkuyQttP6NrF2uJB0DKIU09K8cTq+sQDlbo2r7mdR5Q=="], + "remeda": ["remeda@2.34.1", "https://registry.npmmirror.com/remeda/-/remeda-2.34.1.tgz", {}, "sha512-k5iIF3lHm2NQ+2bNGDvZTD5jZl/JZkCS6AQOfGjYBd7V4rbb3K5whHvab0/O7CqPI43vYzbnIVCQXQJqmOyI6w=="], - "remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="], + "remend": ["remend@1.3.0", "https://registry.npmmirror.com/remend/-/remend-1.3.0.tgz", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="], "require-directory": ["require-directory@2.1.1", "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], - - "requires-port": ["requires-port@1.0.0", "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], - "reselect": ["reselect@5.1.1", "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], + "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], - "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + "resolve": ["resolve@1.22.12", "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "restore-cursor": ["restore-cursor@5.1.0", "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "retry": ["retry@0.13.1", "https://registry.npmmirror.com/retry/-/retry-0.13.1.tgz", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], - - "rettime": ["rettime@0.11.8", "https://registry.npmmirror.com/rettime/-/rettime-0.11.8.tgz", {}, "sha512-0fERGXktJTyJ+h8fBEiPxHPEFOu0h15JY7JtwrOVqR5K+vb99ho6IyOo7ekLS3h4sJCzIDy4VWKIbZUfe9njmg=="], + "rettime": ["rettime@0.11.11", "https://registry.npmmirror.com/rettime/-/rettime-0.11.11.tgz", {}, "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ=="], "reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + "robust-predicates": ["robust-predicates@3.0.3", "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.3.tgz", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], - "rollup": ["rollup@4.60.2", "https://registry.npmmirror.com/rollup/-/rollup-4.60.2.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], + "rolldown": ["rolldown@1.0.2", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.2.tgz", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="], - "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "roughjs": ["roughjs@4.6.6", "https://registry.npmmirror.com/roughjs/-/roughjs-4.6.6.tgz", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], "router": ["router@2.2.0", "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -2357,19 +2108,19 @@ "run-parallel": ["run-parallel@1.2.0", "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + "rw": ["rw@1.3.3", "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="], + "screenfull": ["screenfull@5.2.0", "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="], - "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], "semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + "semver-compare": ["semver-compare@1.0.0", "https://registry.npmmirror.com/semver-compare/-/semver-compare-1.0.0.tgz", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], "send": ["send@1.2.1", "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -2377,11 +2128,11 @@ "set-cookie-parser": ["set-cookie-parser@2.7.2", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - "set-value": ["set-value@2.0.1", "", { "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" } }, "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw=="], + "set-value": ["set-value@2.0.1", "https://registry.npmmirror.com/set-value/-/set-value-2.0.1.tgz", { "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", "is-plain-object": "^2.0.3", "split-string": "^3.0.1" } }, "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw=="], "setprototypeof": ["setprototypeof@1.2.0", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shadcn": ["shadcn@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w=="], + "shadcn": ["shadcn@4.8.0", "https://registry.npmmirror.com/shadcn/-/shadcn-4.8.0.tgz", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-LAm3I/1FdoU/zu5GVG8Hbna4X9zlzEG5TeeCPXqsopkjvGk8QUF9OFhqeRN8oM6Oh/ynUI/yQHZxQAO3Ymcqsg=="], "shallow-equal": ["shallow-equal@3.1.0", "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-3.1.0.tgz", {}, "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg=="], @@ -2389,9 +2140,9 @@ "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], + "shiki": ["shiki@4.1.0", "https://registry.npmmirror.com/shiki/-/shiki-4.1.0.tgz", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="], - "shiki-stream": ["shiki-stream@0.1.4", "", { "dependencies": { "@shikijs/core": "^3.0.0" }, "peerDependencies": { "react": "^19.0.0", "solid-js": "^1.9.0", "vue": "^3.2.0" }, "optionalPeers": ["react", "solid-js", "vue"] }, "sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw=="], + "shiki-stream": ["shiki-stream@0.1.4", "https://registry.npmmirror.com/shiki-stream/-/shiki-stream-0.1.4.tgz", { "dependencies": { "@shikijs/core": "^3.0.0" }, "peerDependencies": { "react": "^19.0.0", "solid-js": "^1.9.0", "vue": "^3.2.0" }, "optionalPeers": ["react", "solid-js", "vue"] }, "sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw=="], "side-channel": ["side-channel@1.1.0", "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], @@ -2405,11 +2156,7 @@ "sisteransi": ["sisteransi@1.0.5", "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "slash": ["slash@5.1.0", "https://registry.npmmirror.com/slash/-/slash-5.1.0.tgz", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], - - "socket.io": ["socket.io@4.8.3", "https://registry.npmmirror.com/socket.io/-/socket.io-4.8.3.tgz", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="], - - "socket.io-adapter": ["socket.io-adapter@2.5.6", "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="], + "socket.io-client": ["socket.io-client@4.8.3", "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.3.tgz", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1", "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" } }, "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g=="], "socket.io-parser": ["socket.io-parser@4.2.6", "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.6.tgz", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg=="], @@ -2421,21 +2168,23 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "split-on-first": ["split-on-first@3.0.0", "", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="], + "split-on-first": ["split-on-first@3.0.0", "https://registry.npmmirror.com/split-on-first/-/split-on-first-3.0.0.tgz", {}, "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="], - "split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="], + "split-string": ["split-string@3.1.0", "https://registry.npmmirror.com/split-string/-/split-string-3.1.0.tgz", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="], + + "state-local": ["state-local@1.0.7", "https://registry.npmmirror.com/state-local/-/state-local-1.0.7.tgz", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], "statuses": ["statuses@2.0.2", "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "stdin-discarder": ["stdin-discarder@0.2.2", "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - "streamdown": ["streamdown@2.5.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "mermaid": "^11.12.2", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.3.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA=="], + "streamdown": ["streamdown@2.5.0", "https://registry.npmmirror.com/streamdown/-/streamdown-2.5.0.tgz", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "mermaid": "^11.12.2", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.3.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA=="], "strict-event-emitter": ["strict-event-emitter@0.5.1", "https://registry.npmmirror.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], "string-argv": ["string-argv@0.3.2", "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], - "string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="], + "string-convert": ["string-convert@0.2.1", "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="], "string-width": ["string-width@7.2.0", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -2449,48 +2198,48 @@ "strip-final-newline": ["strip-final-newline@4.0.0", "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "style-to-js": ["style-to-js@1.1.21", "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.21.tgz", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "https://registry.npmmirror.com/style-to-object/-/style-to-object-1.0.14.tgz", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + "stylis": ["stylis@4.4.0", "https://registry.npmmirror.com/stylis/-/stylis-4.4.0.tgz", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], - "supports-color": ["supports-color@7.2.0", "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-color": ["supports-color@10.2.2", "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], + "swr": ["swr@2.4.1", "https://registry.npmmirror.com/swr/-/swr-2.4.1.tgz", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], - "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], + "tabbable": ["tabbable@6.4.0", "https://registry.npmmirror.com/tabbable/-/tabbable-6.4.0.tgz", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], "tagged-tag": ["tagged-tag@1.0.0", "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + "tailwind-merge": ["tailwind-merge@3.6.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.6.0.tgz", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], - "tailwindcss": ["tailwindcss@4.2.4", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.2.4.tgz", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="], + "tailwindcss": ["tailwindcss@4.3.0", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.0.tgz", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], "tapable": ["tapable@2.3.3", "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "throttle-debounce": ["throttle-debounce@5.0.2", "", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="], + "throttle-debounce": ["throttle-debounce@5.0.2", "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="], "tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + "tinyexec": ["tinyexec@1.2.3", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.2.3.tgz", {}, "sha512-g62dB+w1/OEFnPvmX0yd/HnetYITOL+1nJW7kitOycOeAvmbWC/nu0fwmmQ/kupNojqExzyC/T++pST/jRJ2mQ=="], "tinyglobby": ["tinyglobby@0.2.16", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], - "tldts": ["tldts@7.0.30", "https://registry.npmmirror.com/tldts/-/tldts-7.0.30.tgz", { "dependencies": { "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw=="], + "tldts": ["tldts@7.1.1", "https://registry.npmmirror.com/tldts/-/tldts-7.1.1.tgz", { "dependencies": { "tldts-core": "^7.1.1" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-VuvOq9QVVdzQyIwynB0MRZlEup+u5BD62FjgmKvRDFO8u1RgAzpeg7Qd70hUmrxwkkecqoz1N6t1yGMygx7rnA=="], - "tldts-core": ["tldts-core@7.0.30", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.30.tgz", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="], + "tldts-core": ["tldts-core@7.1.1", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.1.1.tgz", {}, "sha512-v9zYcyFEAJBeyG7g4+y/HFL9i2cHqpV+9cHohNZIhA6xjO2MSVgijFgx6quQaRBDzM5FT8fs5NPjsNITOhlCzg=="], "to-regex-range": ["to-regex-range@5.0.1", "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "to-vfile": ["to-vfile@8.0.0", "", { "dependencies": { "vfile": "^6.0.0" } }, "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg=="], + "to-vfile": ["to-vfile@8.0.0", "https://registry.npmmirror.com/to-vfile/-/to-vfile-8.0.0.tgz", { "dependencies": { "vfile": "^6.0.0" } }, "sha512-IcmH1xB5576MJc9qcfEC/m/nQCFt3fzMHz45sSlgJyTWjRbKW1HAkJpuf3DgE57YzIlZcwcBZA5ENQbBo4aLkg=="], "toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tokenlens": ["tokenlens@1.3.1", "https://registry.npmmirror.com/tokenlens/-/tokenlens-1.3.1.tgz", { "dependencies": { "@tokenlens/core": "1.3.0", "@tokenlens/fetch": "1.3.0", "@tokenlens/helpers": "1.3.1", "@tokenlens/models": "1.3.0" } }, "sha512-7oxmsS5PNCX3z+b+z07hL5vCzlgHKkCGrEQjQmWl5l+v5cUrtL7S1cuST4XThaL1XyjbTX8J5hfP0cjDJRkaLA=="], + "tough-cookie": ["tough-cookie@6.0.1", "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], "trim-lines": ["trim-lines@3.0.1", "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -2499,27 +2248,23 @@ "ts-api-utils": ["ts-api-utils@2.5.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], - "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-dedent": ["ts-dedent@2.2.0", "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], - "ts-md5": ["ts-md5@2.0.1", "", {}, "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w=="], + "ts-md5": ["ts-md5@2.0.1", "https://registry.npmmirror.com/ts-md5/-/ts-md5-2.0.1.tgz", {}, "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w=="], "ts-morph": ["ts-morph@26.0.0", "https://registry.npmmirror.com/ts-morph/-/ts-morph-26.0.0.tgz", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], - "tsconfck": ["tsconfck@3.1.6", "https://registry.npmmirror.com/tsconfck/-/tsconfck-3.1.6.tgz", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], - "tsconfig-paths": ["tsconfig-paths@4.2.0", "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tus-js-client": ["tus-js-client@4.3.1", "https://registry.npmmirror.com/tus-js-client/-/tus-js-client-4.3.1.tgz", { "dependencies": { "buffer-from": "^1.1.2", "combine-errors": "^3.0.3", "is-stream": "^2.0.0", "js-base64": "^3.7.2", "lodash.throttle": "^4.1.1", "proper-lockfile": "^4.1.2", "url-parse": "^1.5.7" } }, "sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg=="], - "tw-animate-css": ["tw-animate-css@1.4.0", "https://registry.npmmirror.com/tw-animate-css/-/tw-animate-css-1.4.0.tgz", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-check": ["type-check@0.4.0", "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@5.6.0", "https://registry.npmmirror.com/type-fest/-/type-fest-5.6.0.tgz", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="], - "type-is": ["type-is@2.0.1", "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + "type-is": ["type-is@2.1.0", "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typedoc": ["typedoc@0.28.19", "https://registry.npmmirror.com/typedoc/-/typedoc-0.28.19.tgz", { "dependencies": { "@gerrit0/mini-shiki": "^3.23.0", "lunr": "^2.3.9", "markdown-it": "^14.1.1", "minimatch": "^10.2.5", "yaml": "^2.8.3" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" }, "bin": { "typedoc": "bin/typedoc" } }, "sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw=="], @@ -2527,13 +2272,11 @@ "typedoc-plugin-markdown": ["typedoc-plugin-markdown@4.11.0", "https://registry.npmmirror.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.11.0.tgz", { "peerDependencies": { "typedoc": "0.28.x" } }, "sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw=="], - "typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.59.1", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.1.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.1", "@typescript-eslint/parser": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/utils": "8.59.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ=="], + "typescript-eslint": ["typescript-eslint@8.59.4", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.4.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.4", "@typescript-eslint/parser": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/utils": "8.59.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ=="], - "ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], - - "uc.micro": ["uc.micro@2.1.0", "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "uc.micro": ["uc.micro@1.0.6", "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz", {}, "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="], "undici-types": ["undici-types@7.16.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -2541,15 +2284,15 @@ "unified": ["unified@11.0.5", "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + "unist-util-find-after": ["unist-util-find-after@5.0.0", "https://registry.npmmirror.com/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], "unist-util-is": ["unist-util-is@6.0.1", "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-position": ["unist-util-position@5.0.0", "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], - "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "https://registry.npmmirror.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], - "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "https://registry.npmmirror.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], @@ -2565,17 +2308,13 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "uppy": ["uppy@5.2.4", "https://registry.npmmirror.com/uppy/-/uppy-5.2.4.tgz", { "dependencies": { "@uppy/audio": "3.1.0", "@uppy/aws-s3": "5.1.0", "@uppy/box": "4.1.0", "@uppy/companion-client": "5.1.1", "@uppy/compressor": "3.1.0", "@uppy/core": "5.2.0", "@uppy/dashboard": "5.1.1", "@uppy/drag-drop": "5.1.0", "@uppy/drop-target": "4.1.0", "@uppy/dropbox": "5.1.0", "@uppy/facebook": "5.1.0", "@uppy/form": "5.1.0", "@uppy/golden-retriever": "5.2.1", "@uppy/google-drive": "5.1.0", "@uppy/google-drive-picker": "1.1.1", "@uppy/google-photos-picker": "1.1.0", "@uppy/image-editor": "4.2.0", "@uppy/image-generator": "1.0.0", "@uppy/instagram": "5.1.0", "@uppy/locales": "5.1.1", "@uppy/onedrive": "5.1.0", "@uppy/provider-views": "5.2.2", "@uppy/remote-sources": "3.1.0", "@uppy/screen-capture": "5.1.0", "@uppy/status-bar": "5.1.0", "@uppy/store-default": "5.0.0", "@uppy/thumbnail-generator": "5.1.0", "@uppy/transloadit": "5.5.1", "@uppy/tus": "5.1.1", "@uppy/unsplash": "5.1.0", "@uppy/url": "5.1.0", "@uppy/webcam": "5.1.0", "@uppy/webdav": "1.1.1", "@uppy/xhr-upload": "5.2.0", "@uppy/zoom": "4.1.0" } }, "sha512-1btgAGUb7Tu7GympmA9XkyGcXBSM93d1FA36J6pY4lr6a0jMXfvFUg66Dbnm/Xaq6sFvm8KwSPJF46d8Ng0E8Q=="], - "uri-js": ["uri-js@4.4.1", "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "url-join": ["url-join@5.0.0", "", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], - - "url-parse": ["url-parse@1.5.10", "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + "url-join": ["url-join@5.0.0", "https://registry.npmmirror.com/url-join/-/url-join-5.0.0.tgz", {}, "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA=="], "use-callback-ref": ["use-callback-ref@1.3.3", "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], - "use-merge-value": ["use-merge-value@1.2.0", "", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="], + "use-merge-value": ["use-merge-value@1.2.0", "https://registry.npmmirror.com/use-merge-value/-/use-merge-value-1.2.0.tgz", { "peerDependencies": { "react": ">= 16.x" } }, "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw=="], "use-sidecar": ["use-sidecar@1.1.3", "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.3.tgz", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], @@ -2583,9 +2322,9 @@ "util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@13.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="], + "uuid": ["uuid@13.0.2", "https://registry.npmmirror.com/uuid/-/uuid-13.0.2.tgz", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="], - "v8n": ["v8n@1.5.1", "", {}, "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A=="], + "v8n": ["v8n@1.5.1", "https://registry.npmmirror.com/v8n/-/v8n-1.5.1.tgz", {}, "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A=="], "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "https://registry.npmmirror.com/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], @@ -2595,55 +2334,41 @@ "vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + "vfile-location": ["vfile-location@5.0.3", "https://registry.npmmirror.com/vfile-location/-/vfile-location-5.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], "vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="], + "virtua": ["virtua@0.49.1", "https://registry.npmmirror.com/virtua/-/virtua-0.49.1.tgz", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="], - "vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + "vite": ["vite@8.0.14", "https://registry.npmmirror.com/vite/-/vite-8.0.14.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="], - "vite-bundle-analyzer": ["vite-bundle-analyzer@1.3.8", "", { "bin": { "analyze": "dist/bin.js" } }, "sha512-IIk7WPhoYs7pyo75jwI+dFt7yykgjK7NY+dqnJtiZnyqP2k6NgPb3TY80FLFjtgnfk/o+OjI18+anKyeviCbRA=="], + "warning": ["warning@4.0.3", "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="], - "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], - - "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], - - "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], - - "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], - - "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - - "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], - - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "web-namespaces": ["web-namespaces@2.0.1", "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], - "which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "wildcard": ["wildcard@1.1.2", "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz", {}, "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng=="], - "word-wrap": ["word-wrap@1.2.5", "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wrap-ansi": ["wrap-ansi@7.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.18.3", "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "ws": ["ws@8.20.1", "https://registry.npmmirror.com/ws/-/ws-8.20.1.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], "wsl-utils": ["wsl-utils@0.3.1", "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.3.1.tgz", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + "y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.8.4", "https://registry.npmmirror.com/yaml/-/yaml-2.8.4.tgz", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + "yaml": ["yaml@2.9.0", "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -2651,123 +2376,115 @@ "yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "yocto-spinner": ["yocto-spinner@1.1.0", "https://registry.npmmirror.com/yocto-spinner/-/yocto-spinner-1.1.0.tgz", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA=="], + "yocto-spinner": ["yocto-spinner@1.2.0", "https://registry.npmmirror.com/yocto-spinner/-/yocto-spinner-1.2.0.tgz", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw=="], "yoctocolors": ["yoctocolors@2.1.2", "https://registry.npmmirror.com/yoctocolors/-/yoctocolors-2.1.2.tgz", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "zod": ["zod@4.4.2", "https://registry.npmmirror.com/zod/-/zod-4.4.2.tgz", {}, "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw=="], + "zod": ["zod@4.4.3", "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zod-validation-error": ["zod-validation-error@4.0.2", "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "zustand": ["zustand@5.0.12", "https://registry.npmmirror.com/zustand/-/zustand-5.0.12.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="], + "zustand": ["zustand@5.0.13", "https://registry.npmmirror.com/zustand/-/zustand-5.0.13.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="], "zwitch": ["zwitch@2.0.4", "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@base-ui/utils/reselect": ["reselect@5.2.0", "https://registry.npmmirror.com/reselect/-/reselect-5.2.0.tgz", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/execa": ["execa@5.1.1", "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "@dotenvx/dotenvx/which": ["which@4.0.0", "https://registry.npmmirror.com/which/-/which-4.0.0.tgz", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], - "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + "@emotion/babel-plugin/@emotion/hash": ["@emotion/hash@0.9.2", "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], - "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.9.0.tgz", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], - "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "https://registry.npmmirror.com/source-map/-/source-map-0.5.7.tgz", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], - "@emotion/babel-plugin/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + "@emotion/babel-plugin/stylis": ["stylis@4.2.0", "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], - "@emotion/cache/stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + "@emotion/cache/stylis": ["stylis@4.2.0", "https://registry.npmmirror.com/stylis/-/stylis-4.2.0.tgz", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], - "@emotion/serialize/@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], + "@emotion/serialize/@emotion/hash": ["@emotion/hash@0.9.2", "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], - "@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], + "@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.10.0.tgz", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@eslint/eslintrc/ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "@gerrit0/mini-shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - "@eslint/eslintrc/globals": ["globals@14.0.0", "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@gerrit0/mini-shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], - "@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], + "@gerrit0/mini-shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - "@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], + "@gerrit0/mini-shiki/@shikijs/types": ["@shikijs/types@3.23.0", "https://registry.npmmirror.com/@shikijs/types/-/types-3.23.0.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - "@lobehub/ui/@base-ui/react": ["@base-ui/react@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.3", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.3.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg=="], + "@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.562.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.562.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], - "@lobehub/ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.469.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="], - "@lobehub/ui/immer": ["immer@11.1.4", "https://registry.npmmirror.com/immer/-/immer-11.1.4.tgz", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "@lobehub/ui/immer": ["immer@11.1.8", "https://registry.npmmirror.com/immer/-/immer-11.1.8.tgz", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="], - "@lobehub/ui/shiki": ["shiki@4.0.2", "", { "dependencies": { "@shikijs/core": "4.0.2", "@shikijs/engine-javascript": "4.0.2", "@shikijs/engine-oniguruma": "4.0.2", "@shikijs/langs": "4.0.2", "@shikijs/themes": "4.0.2", "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ=="], + "@mdx-js/mdx/source-map": ["source-map@0.7.6", "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "@mswjs/interceptors/@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "https://registry.npmmirror.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], + "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.23.0", "https://registry.npmmirror.com/@shikijs/transformers/-/transformers-3.23.0.tgz", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw=="], + "@pierre/diffs/diff": ["diff@8.0.3", "https://registry.npmmirror.com/diff/-/diff-8.0.3.tgz", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], - "@opentelemetry/instrumentation-fetch/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], + "@pierre/diffs/shiki": ["shiki@3.23.0", "https://registry.npmmirror.com/shiki/-/shiki-3.23.0.tgz", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], - "@opentelemetry/instrumentation-fetch/@opentelemetry/sdk-trace-web": ["@opentelemetry/sdk-trace-web@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-R4/i0rISvAujG4Zwk3s6ySyrWG+Db3SerZVM4jZ2lEzjrNylF7nRAy1hVvWe8gTbwIxX+6w6ZvZwdtl2C7UQHQ=="], + "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.3.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], - "@opentelemetry/instrumentation-xml-http-request/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], + "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - "@opentelemetry/instrumentation-xml-http-request/@opentelemetry/sdk-trace-web": ["@opentelemetry/sdk-trace-web@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/sdk-trace-base": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-R4/i0rISvAujG4Zwk3s6ySyrWG+Db3SerZVM4jZ2lEzjrNylF7nRAy1hVvWe8gTbwIxX+6w6ZvZwdtl2C7UQHQ=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@opentelemetry/otlp-transformer/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw=="], + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw=="], + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw=="], + "@rc-component/dialog/@rc-component/portal": ["@rc-component/portal@2.2.0", "https://registry.npmmirror.com/@rc-component/portal/-/portal-2.2.0.tgz", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - "@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + "@rc-component/drawer/@rc-component/portal": ["@rc-component/portal@2.2.0", "https://registry.npmmirror.com/@rc-component/portal/-/portal-2.2.0.tgz", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - "@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="], + "@rc-component/image/@rc-component/portal": ["@rc-component/portal@2.2.0", "https://registry.npmmirror.com/@rc-component/portal/-/portal-2.2.0.tgz", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - "@pierre/diffs/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "@rc-component/tour/@rc-component/portal": ["@rc-component/portal@2.2.0", "https://registry.npmmirror.com/@rc-component/portal/-/portal-2.2.0.tgz", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - "@rc-component/dialog/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + "@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "https://registry.npmmirror.com/@rc-component/portal/-/portal-2.2.0.tgz", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - "@rc-component/drawer/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + "@rc-component/util/react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "@rc-component/image/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], + "@reduxjs/toolkit/immer": ["immer@11.1.8", "https://registry.npmmirror.com/immer/-/immer-11.1.8.tgz", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="], - "@rc-component/tour/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - - "@rc-component/trigger/@rc-component/portal": ["@rc-component/portal@2.2.0", "", { "dependencies": { "@rc-component/util": "^1.2.1", "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ=="], - - "@rc-component/util/is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="], - - "@rc-component/util/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "@reduxjs/toolkit/immer": ["immer@11.1.4", "https://registry.npmmirror.com/immer/-/immer-11.1.4.tgz", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "@reduxjs/toolkit/reselect": ["reselect@5.2.0", "https://registry.npmmirror.com/reselect/-/reselect-5.2.0.tgz", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], "@scalar/openapi-parser/@scalar/helpers": ["@scalar/helpers@0.5.2", "https://registry.npmmirror.com/@scalar/helpers/-/helpers-0.5.2.tgz", {}, "sha512-Pi1GAl8jO6ungmGj2sjDfCfqiBNrKW6HXDZmminV94ybGU/KtRLOqHwd0n9FIhY3j0RYGpGC0VCuniCICfQPHg=="], "@scalar/openapi-parser/@scalar/json-magic": ["@scalar/json-magic@0.12.8", "https://registry.npmmirror.com/@scalar/json-magic/-/json-magic-0.12.8.tgz", { "dependencies": { "@scalar/helpers": "0.5.2", "pathe": "^2.0.3", "yaml": "^2.8.0" } }, "sha512-a559iO8tmFeA90JJAAM3U5x1Asf3mr0Z8uDC1PmyLTDjdSOfajP7EY9VzNoXE2cM48ilf9qrjmkbw/d4VCFjQw=="], - "@shikijs/core/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], + "@scalar/openapi-parser/ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "@shikijs/primitive/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], - - "@shikijs/transformers/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], + "@streamdown/code/shiki": ["shiki@3.23.0", "https://registry.npmmirror.com/shiki/-/shiki-3.23.0.tgz", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], @@ -2777,79 +2494,71 @@ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - - "@ts-morph/common/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "accepts/mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "ajv-draft-04/ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + "ajv-formats/ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], "cliui/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "https://registry.npmmirror.com/cose-base/-/cose-base-2.2.0.tgz", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], - "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "d3-dsv/commander": ["commander@7.2.0", "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + "d3-sankey/d3-array": ["d3-array@2.12.1", "https://registry.npmmirror.com/d3-array/-/d3-array-2.12.1.tgz", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], - "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "engine.io/cookie": ["cookie@0.7.2", "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "dnd-core/redux": ["redux@4.2.1", "https://registry.npmmirror.com/redux/-/redux-4.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.9.2" } }, "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w=="], - "eslint/ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], - - "estree-util-to-js/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - - "express/accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "estree-util-to-js/source-map": ["source-map@0.7.6", "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "express/cookie": ["cookie@0.7.2", "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "express/mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + "extend-shallow/is-extendable": ["is-extendable@0.1.1", "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "globby/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "globby/unicorn-magic": ["unicorn-magic@0.4.0", "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.4.0.tgz", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="], - "headers-polyfill/set-cookie-parser": ["set-cookie-parser@3.1.0", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "katex/commander": ["commander@8.3.0", "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - - "leva/zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="], - - "log-symbols/chalk": ["chalk@5.6.2", "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "leva/zustand": ["zustand@3.7.2", "https://registry.npmmirror.com/zustand/-/zustand-3.7.2.tgz", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="], "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "markdown-it/linkify-it": ["linkify-it@5.0.1", "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.1.tgz", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="], + + "markdown-it/uc.micro": ["uc.micro@2.1.0", "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + "mermaid/dompurify": ["dompurify@3.4.7", "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.7.tgz", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA=="], - "mermaid/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="], + "mermaid/marked": ["marked@16.4.2", "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + + "mermaid/uuid": ["uuid@14.0.0", "https://registry.npmmirror.com/uuid/-/uuid-14.0.0.tgz", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], "micromatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "monaco-editor/marked": ["marked@14.0.0", "https://registry.npmmirror.com/marked/-/marked-14.0.0.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], - "ora/chalk": ["chalk@5.6.2", "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "ora/strip-ansi": ["strip-ansi@7.2.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -2857,23 +2566,21 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "postcss/nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], "prompts/kleur": ["kleur@3.0.3", "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "rc-menu/@rc-component/trigger": ["@rc-component/trigger@2.3.1", "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.1.tgz", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A=="], - "proper-lockfile/retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "rc-util/react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "react-jsx-parser/@types/react": ["@types/react@18.3.29", "https://registry.npmmirror.com/@types/react/-/react-18.3.29.tgz", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg=="], - "rc-menu/@rc-component/trigger": ["@rc-component/trigger@2.3.1", "", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A=="], + "react-jsx-parser/@types/react-dom": ["@types/react-dom@18.3.7", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], - "rc-util/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - - "react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], + "react-rnd/tslib": ["tslib@2.6.2", "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], "restore-cursor/onetime": ["onetime@7.0.0", "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], @@ -2881,24 +2588,22 @@ "send/mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + "set-value/is-extendable": ["is-extendable@0.1.1", "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "shadcn/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "shadcn/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + "shiki-stream/@shikijs/core": ["@shikijs/core@3.23.0", "https://registry.npmmirror.com/@shikijs/core/-/core-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - "shiki-stream/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - - "split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], + "split-string/extend-shallow": ["extend-shallow@3.0.2", "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-3.0.2.tgz", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], "string-width/strip-ansi": ["strip-ansi@7.2.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "tus-js-client/is-stream": ["is-stream@2.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "type-is/content-type": ["content-type@2.0.0", "https://registry.npmmirror.com/content-type/-/content-type-2.0.0.tgz", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "type-is/mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "typedoc/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "wrap-ansi/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "yargs/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -2917,39 +2622,53 @@ "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "https://registry.npmmirror.com/isexe/-/isexe-3.1.5.tgz", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "@lobehub/ui/@base-ui/react/@base-ui/utils": ["@base-ui/utils@0.2.3", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "https://registry.npmmirror.com/@shikijs/core/-/core-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - "@lobehub/ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag=="], + "@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.23.0", "https://registry.npmmirror.com/@shikijs/types/-/types-3.23.0.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - "@lobehub/ui/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg=="], + "@pierre/diffs/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "https://registry.npmmirror.com/@shikijs/core/-/core-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], - "@lobehub/ui/shiki/@shikijs/langs": ["@shikijs/langs@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg=="], + "@pierre/diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], - "@lobehub/ui/shiki/@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="], + "@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - "@lobehub/ui/shiki/@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="], + "@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], - "@pierre/diffs/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + "@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "https://registry.npmmirror.com/@shikijs/types/-/types-3.23.0.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@scalar/openapi-parser/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "babel-plugin-macros/cosmiconfig/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], + "@streamdown/code/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "https://registry.npmmirror.com/@shikijs/core/-/core-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], + + "@streamdown/code/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], + + "@streamdown/code/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], + + "@streamdown/code/shiki/@shikijs/langs": ["@shikijs/langs@3.23.0", "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], + + "@streamdown/code/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.23.0.tgz", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + + "@streamdown/code/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "https://registry.npmmirror.com/@shikijs/types/-/types-3.23.0.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + + "accepts/mime-types/mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "ajv-draft-04/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "babel-plugin-macros/cosmiconfig/yaml": ["yaml@1.10.3", "https://registry.npmmirror.com/yaml/-/yaml-1.10.3.tgz", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "https://registry.npmmirror.com/layout-base/-/layout-base-2.0.1.tgz", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], - "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], - "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - - "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "express/accepts/negotiator": ["negotiator@1.0.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], "express/mime-types/mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -2959,24 +2678,20 @@ "send/mime-types/mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "shadcn/https-proxy-agent/agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "shiki-stream/@shikijs/core/@shikijs/types": ["@shikijs/types@3.23.0", "https://registry.npmmirror.com/@shikijs/types/-/types-3.23.0.tgz", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "type-is/mime-types/mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "typedoc/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "orval/find-up/locate-path/p-locate": ["p-locate@6.0.0", "https://registry.npmmirror.com/p-locate/-/p-locate-6.0.0.tgz", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], - "typedoc/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "orval/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "https://registry.npmmirror.com/p-limit/-/p-limit-4.0.0.tgz", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], "orval/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-1.2.2.tgz", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], diff --git a/components.json b/components.json index c27d912..bd14fcc 100644 --- a/components.json +++ b/components.json @@ -1,6 +1,6 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "radix-nova", + "style": "base-nova", "rsc": false, "tsx": true, "tailwind": { @@ -12,6 +12,8 @@ }, "iconLibrary": "lucide", "rtl": false, + "menuColor": "default", + "menuAccent": "subtle", "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -19,9 +21,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "menuColor": "default", - "menuAccent": "subtle", "registries": { - "@ai-elements": "https://ai-sdk.dev/elements/api/registry/{name}.json" + "@manifest": "https://ui.manifest.build/r/{name}.json" } } diff --git a/deploy/.helmignore b/deploy/.helmignore deleted file mode 100644 index 9359339..0000000 --- a/deploy/.helmignore +++ /dev/null @@ -1,25 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ -# Secrets -.server.yaml diff --git a/deploy/Chart.yaml b/deploy/Chart.yaml deleted file mode 100644 index 8a6482d..0000000 --- a/deploy/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -name: deploy -description: Helm chart for the project backend services -type: application -version: 0.1.0 -appVersion: "0.2.9" \ No newline at end of file diff --git a/deploy/README.md b/deploy/README.md deleted file mode 100644 index 8781a63..0000000 --- a/deploy/README.md +++ /dev/null @@ -1,209 +0,0 @@ -# Deploy Helm Chart - -Monolithic Helm chart for all backend services. - -## Services - -| Service | Port(s) | Replicas | HPA | Purpose | -|----------------------|-------------------------|----------|----------|---------------------------------------------| -| `app` | 3000 (HTTP) | 2 | 2–10 | Main API server | -| `gitserver` | 8021 (HTTP), 2222 (SSH) | 1 | 1–5 | Git HTTP + SSH server | -| `email_worker` | 8084 (HTTP) | 1 | disabled | Email queue consumer (single instance only) | -| `git_hook` | 8083 (HTTP) | 1 | 1–5 | Git hook worker pool | -| `metrics_aggregator` | 9090 (HTTP) | 1 | 1–5 | Prometheus scrape + Loki push | -| `static_server` | 8081 (HTTP) | 1 | 1–5 | Static file server (avatars, blobs, media) | - -## Prerequisites - -The following resources must exist in the cluster **before** installing the Helm chart. They are not managed by Helm — -install, upgrade, and uninstall of the chart will not touch them. - -### 1. Namespace - -```bash -kubectl create namespace app -``` - -### 2. PVC (aliyun-nfs-app, 200Ti, ReadWriteMany) - -```bash -kubectl apply -f - <<'EOF' -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: shared-data - namespace: app -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 200Ti - storageClassName: aliyun-nfs-app -EOF -``` - -> The chart references this PVC by hardcoded name `shared-data`. This name is immutable — it cannot be changed via Helm -> values. - -### 3. ConfigMap - -```bash -kubectl apply -f - <<'EOF' -apiVersion: v1 -kind: ConfigMap -metadata: - name: app-env - namespace: app -data: - APP_REPOS_ROOT: "/data/repos" - APP_AVATAR_PATH: "/data/avatars" - STORAGE_PATH: "/data/files" - STATIC_ROOT: "/data" - APP_LOG_LEVEL: "info" - APP_COOKIE_SECURE: "false" - APP_DOMAIN_URL: "https://your-domain.com" - APP_DATABASE_URL: "postgres://user:pass@postgres:5432/app" - APP_REDIS_URL: "redis://redis:6379" - APP_AI_BASIC_URL: "https://api.openai.com/v1" - APP_AI_API_KEY: "sk-..." - APP_SMTP_PASSWORD: "..." - APP_SESSION_SECRET: "min-32-byte-random-string..." - APP_SSH_SERVER_PRIVATE_KEY: "" -EOF -``` - -| Variable | Default / Example | Required | -|------------------------------|-----------------------------|-----------| -| `APP_REPOS_ROOT` | `/data/repos` | Yes | -| `APP_AVATAR_PATH` | `/data/avatars` | Yes | -| `STORAGE_PATH` | `/data/files` | Yes | -| `STATIC_ROOT` | `/data` | Yes | -| `APP_LOG_LEVEL` | `info` | No | -| `APP_COOKIE_SECURE` | `false` | No | -| `APP_DOMAIN_URL` | `https://your-domain.com` | Yes | -| `APP_DATABASE_URL` | `postgres://...` | **Yes** | -| `APP_REDIS_URL` | `redis://...` | **Yes** | -| `APP_AI_BASIC_URL` | `https://api.openai.com/v1` | **Yes** | -| `APP_AI_API_KEY` | `sk-...` | **Yes** | -| `APP_SMTP_PASSWORD` | `...` | **Yes** | -| `APP_SESSION_SECRET` | min 32 bytes | **Yes** | -| `APP_SSH_SERVER_PRIVATE_KEY` | hex-encoded PEM | **Yes** | -| `APP_SSH_PORT` | `2222` | Yes (k8s) | - -> **SSH host key**: `APP_SSH_SERVER_PRIVATE_KEY` must be the hex-encoded Ed25519 private key PEM bytes. -> ```bash -> ssh-keygen -t ed25519 -f /tmp/ssh_host_key -N "" -> hexdump -v -e '/1 "%02x"' < /tmp/ssh_host_key -> ``` -> -> **Session secret**: generate 48 random bytes: -> ```bash -> openssl rand -base64 48 -> ``` -> -> Override the ConfigMap name with `--set configMapName=your-cm-name`. - -### 4. Verify prerequisites - -```bash -kubectl get namespace app -kubectl get pvc -n app shared-data -kubectl get configmap -n app app-env -``` - -## Quick Start - -```bash -helm template deploy ./deploy --namespace app --set imageRegistry=ghcr.io/your-org -helm lint ./deploy - -# Install -helm upgrade --install deploy ./deploy \ - --namespace app \ - --set imageRegistry=ghcr.io/your-org \ - --set imageTag=v0.2.9 -``` - -## Storage - -All services share a single PVC (`shared-data`) via `subPath` mounts: - -| SubPath | Mount | Used By | -|-----------|-----------------|--------------------------| -| `repos` | `/data/repos` | app, gitserver, git-hook | -| `avatars` | `/data/avatars` | app | -| `files` | `/data/files` | app | -| `static` | `/data` | static-server | - -Pods run as UID/GID `1000` and set `fsGroup: 1000` so Git processes can create temporary object -directories under bare repositories. If an existing PVC was previously written by another UID, -fix ownership once from a maintenance pod: - -```bash -chown -R 1000:1000 /data/repos -chmod -R u+rwX,g+rwX /data/repos -``` - -## Autoscaling - -All services except `email_worker` have HPA enabled by default. The email worker is fixed at 1 replica and must not be -scaled. - -To adjust HPA bounds per service: - -```bash ---set services.app.autoscaling.maxReplicas=20 ---set services.app.autoscaling.targetCPUUtilization=70 -``` - -To disable HPA for a service: - -```bash ---set services.git_hook.autoscaling.enabled=false -``` - -## Ingress - -```bash -helm upgrade --install deploy ./deploy \ - --namespace app \ - --set ingress.enabled=true \ - --set ingress.className=nginx \ - --set ingress.hosts[0].host=your-domain.com -``` - -## Dependencies - -All services require these to be reachable from the cluster: - -- PostgreSQL (via `APP_DATABASE_URL`) -- Redis (via `APP_REDIS_URL`) -- Git binary (included in all Docker images) -- OpenAI-compatible API (via `APP_AI_BASIC_URL` + `APP_AI_API_KEY`) -- Qdrant vector DB (via `APP_QDRANT_URL`) -- SMTP server (via `APP_SMTP_*`) -- Embedding model (via `APP_EMBED_MODEL_*`) - -Optional dependencies with graceful degradation: - -| Dependency | Variable | Fallback | -|----------------|-------------------------------|------------------| -| NATS JetStream | `NATS_URL` + `NATS_TOKEN` | Redis queue | -| Loki | `LOKI_URL` | Logs discarded | -| OTEL Collector | `OTEL_EXPORTER_OTLP_ENDPOINT` | Tracing disabled | - -## Production Example - -```bash -helm upgrade --install deploy ./deploy \ - --namespace app \ - --set imageRegistry=ghcr.io/your-org \ - --set imageTag=v0.2.9 \ - --set services.app.replicas=3 \ - --set services.app.autoscaling.maxReplicas=20 \ - --set ingress.enabled=true \ - --set ingress.className=nginx \ - --set ingress.hosts[0].host=your-domain.com \ - --set configMapName=app-env -``` diff --git a/deploy/templates/NOTES.txt b/deploy/templates/NOTES.txt deleted file mode 100644 index e80c939..0000000 --- a/deploy/templates/NOTES.txt +++ /dev/null @@ -1,19 +0,0 @@ -Project backend services deployed to namespace: {{ .Release.Namespace }} - -Services: -{{- range $svcKey, $svcVal := .Values.services }} - {{ $svcKey | replace "_" "-" }}: {{ if $svcVal.ports }}{{ range $portName, $portNum := $svcVal.ports }}{{ $portName }}={{ $portNum }} {{ end }}{{ else }}port={{ $svcVal.port }}{{ end }} {{ if $svcVal.autoscaling.enabled }}(HPA: {{ $svcVal.autoscaling.minReplicas }}-{{ $svcVal.autoscaling.maxReplicas }}){{ else }}(static: {{ $svcVal.replicaCount }}){{ end }} -{{- end }} - -To access the app locally: - kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "deploy.serviceFullname" (dict "root" . "svcKey" "app") }} 3000:3000 - -To check HPA status: -{{- range $svcKey, $svcVal := .Values.services }} -{{- if $svcVal.autoscaling.enabled }} - kubectl get hpa -n {{ $.Release.Namespace }} {{ include "deploy.serviceFullname" (dict "root" $ "svcKey" $svcKey) }} -{{- end }} -{{- end }} - -To check all pods: - kubectl get pods -n {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "deploy.name" . }}" diff --git a/deploy/templates/_helpers.tpl b/deploy/templates/_helpers.tpl deleted file mode 100644 index cd3f7b7..0000000 --- a/deploy/templates/_helpers.tpl +++ /dev/null @@ -1,78 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "deploy.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -*/}} -{{- define "deploy.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Service fullname — includes service key for per-service resources. -Underscores in svcKey are replaced with hyphens for valid Kubernetes names. -*/}} -{{- define "deploy.serviceFullname" -}} -{{- printf "%s-%s" (include "deploy.fullname" .root) (.svcKey | replace "_" "-") | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Chart name and version as used by the chart label. -*/}} -{{- define "deploy.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "deploy.labels" -}} -helm.sh/chart: {{ include "deploy.chart" . }} -{{ include "deploy.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "deploy.selectorLabels" -}} -app.kubernetes.io/name: {{ include "deploy.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Per-service selector labels — used by Service to target the right Deployment. -Underscores in svcKey are replaced with hyphens for valid Kubernetes label values. -*/}} -{{- define "deploy.serviceSelectorLabels" -}} -app.kubernetes.io/name: {{ include "deploy.name" .root }} -app.kubernetes.io/instance: {{ .root.Release.Name }} -app.kubernetes.io/component: {{ .svcKey | replace "_" "-" }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "deploy.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "deploy.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/deploy/templates/app/deployment.yaml b/deploy/templates/app/deployment.yaml deleted file mode 100644 index 66d551d..0000000 --- a/deploy/templates/app/deployment.yaml +++ /dev/null @@ -1,89 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "app") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: app -spec: - replicas: {{ .Values.services.app.replicaCount | default 1 }} - selector: - matchLabels: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "app") | nindent 6 }} - template: - metadata: - labels: - {{- include "deploy.labels" . | nindent 8 }} - app.kubernetes.io/component: app - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "deploy.serviceAccountName" . }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: app - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ .Values.imageRegistry }}/{{ .Values.services.app.repository }}:{{ .Values.imageTag | default .Chart.AppVersion }}" - imagePullPolicy: IfNotPresent - {{- with .Values.services.app.command }} - command: - {{- toYaml . | nindent 12 }} - {{- end }} - ports: - - name: http - containerPort: {{ .Values.services.app.port }} - protocol: TCP - envFrom: - - configMapRef: - name: {{ .Values.configMapName }} - {{- with .Values.services.app.extraEnv }} - env: - {{- range $key, $val := . }} - - name: {{ $key }} - value: {{ $val | quote }} - {{- end }} - {{- end }} - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 15 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with .Values.services.app.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.services.app.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: shared-data - persistentVolumeClaim: - claimName: shared-data - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file diff --git a/deploy/templates/app/service.yaml b/deploy/templates/app/service.yaml deleted file mode 100644 index 63900d6..0000000 --- a/deploy/templates/app/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "app") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: app -spec: - type: ClusterIP - ports: - - port: {{ .Values.services.app.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "app") | nindent 4 }} \ No newline at end of file diff --git a/deploy/templates/email_worker/deployment.yaml b/deploy/templates/email_worker/deployment.yaml deleted file mode 100644 index 90ab18e..0000000 --- a/deploy/templates/email_worker/deployment.yaml +++ /dev/null @@ -1,70 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "email_worker") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: email-worker -spec: - replicas: {{ .Values.services.email_worker.replicaCount | default 1 }} - selector: - matchLabels: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "email_worker") | nindent 6 }} - template: - metadata: - labels: - {{- include "deploy.labels" . | nindent 8 }} - app.kubernetes.io/component: email-worker - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "deploy.serviceAccountName" . }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: email-worker - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ .Values.imageRegistry }}/{{ .Values.services.email_worker.repository }}:{{ .Values.imageTag | default .Chart.AppVersion }}" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: {{ .Values.services.email_worker.port }} - protocol: TCP - envFrom: - - configMapRef: - name: {{ .Values.configMapName }} - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 15 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with .Values.services.email_worker.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file diff --git a/deploy/templates/email_worker/service.yaml b/deploy/templates/email_worker/service.yaml deleted file mode 100644 index 0aa751e..0000000 --- a/deploy/templates/email_worker/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "email_worker") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: email-worker -spec: - type: ClusterIP - ports: - - port: {{ .Values.services.email_worker.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "email_worker") | nindent 4 }} \ No newline at end of file diff --git a/deploy/templates/git_hook/deployment.yaml b/deploy/templates/git_hook/deployment.yaml deleted file mode 100644 index 7cfd1f2..0000000 --- a/deploy/templates/git_hook/deployment.yaml +++ /dev/null @@ -1,78 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "git_hook") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: git-hook -spec: - replicas: {{ .Values.services.git_hook.replicaCount | default 1 }} - selector: - matchLabels: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "git_hook") | nindent 6 }} - template: - metadata: - labels: - {{- include "deploy.labels" . | nindent 8 }} - app.kubernetes.io/component: git-hook - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "deploy.serviceAccountName" . }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: git-hook - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ .Values.imageRegistry }}/{{ .Values.services.git_hook.repository }}:{{ .Values.imageTag | default .Chart.AppVersion }}" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: {{ .Values.services.git_hook.port }} - protocol: TCP - envFrom: - - configMapRef: - name: {{ .Values.configMapName }} - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 15 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with .Values.services.git_hook.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.services.git_hook.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: shared-data - persistentVolumeClaim: - claimName: shared-data - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file diff --git a/deploy/templates/git_hook/service.yaml b/deploy/templates/git_hook/service.yaml deleted file mode 100644 index a8e5cda..0000000 --- a/deploy/templates/git_hook/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "git_hook") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: git-hook -spec: - type: ClusterIP - ports: - - port: {{ .Values.services.git_hook.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "git_hook") | nindent 4 }} \ No newline at end of file diff --git a/deploy/templates/gitserver/deployment.yaml b/deploy/templates/gitserver/deployment.yaml deleted file mode 100644 index 78d98f8..0000000 --- a/deploy/templates/gitserver/deployment.yaml +++ /dev/null @@ -1,88 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "gitserver") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: gitserver -spec: - replicas: {{ .Values.services.gitserver.replicaCount | default 1 }} - selector: - matchLabels: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "gitserver") | nindent 6 }} - template: - metadata: - labels: - {{- include "deploy.labels" . | nindent 8 }} - app.kubernetes.io/component: gitserver - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "deploy.serviceAccountName" . }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: gitserver - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ .Values.imageRegistry }}/{{ .Values.services.gitserver.repository }}:{{ .Values.imageTag | default .Chart.AppVersion }}" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: {{ .Values.services.gitserver.ports.http }} - protocol: TCP - - name: ssh - containerPort: {{ .Values.services.gitserver.ports.ssh }} - protocol: TCP - envFrom: - - configMapRef: - name: {{ .Values.configMapName }} - {{- with .Values.services.gitserver.extraEnv }} - env: - {{- range $key, $val := . }} - - name: {{ $key }} - value: {{ $val | quote }} - {{- end }} - {{- end }} - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 15 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with .Values.services.gitserver.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.services.gitserver.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: shared-data - persistentVolumeClaim: - claimName: shared-data - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file diff --git a/deploy/templates/gitserver/service.yaml b/deploy/templates/gitserver/service.yaml deleted file mode 100644 index c6eaadc..0000000 --- a/deploy/templates/gitserver/service.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "gitserver") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: gitserver -spec: - type: ClusterIP - ports: - - port: {{ .Values.services.gitserver.ports.http }} - targetPort: http - protocol: TCP - name: http - - port: {{ .Values.services.gitserver.ports.ssh }} - targetPort: ssh - protocol: TCP - name: ssh - selector: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "gitserver") | nindent 4 }} \ No newline at end of file diff --git a/deploy/templates/gitserver/ssh-service.yaml b/deploy/templates/gitserver/ssh-service.yaml deleted file mode 100644 index 1fcfaa7..0000000 --- a/deploy/templates/gitserver/ssh-service.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "gitserver") }}-ssh - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: gitserver - annotations: - {{- with .Values.services.gitserver.sshService.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - type: LoadBalancer - externalTrafficPolicy: Local - ports: - - port: 22 - targetPort: ssh - protocol: TCP - name: ssh - selector: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "gitserver") | nindent 4 }} \ No newline at end of file diff --git a/deploy/templates/hpa.yaml b/deploy/templates/hpa.yaml deleted file mode 100644 index 41ed0a0..0000000 --- a/deploy/templates/hpa.yaml +++ /dev/null @@ -1,26 +0,0 @@ -{{- range $svcKey, $svcVal := .Values.services }} -{{- if $svcVal.autoscaling.enabled }} ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" $ "svcKey" $svcKey) }} - labels: - {{- include "deploy.labels" $ | nindent 4 }} - app.kubernetes.io/component: {{ $svcKey | replace "_" "-" }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "deploy.serviceFullname" (dict "root" $ "svcKey" $svcKey) }} - minReplicas: {{ $svcVal.autoscaling.minReplicas }} - maxReplicas: {{ $svcVal.autoscaling.maxReplicas }} - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ $svcVal.autoscaling.targetCPUUtilization }} -{{- end }} -{{- end }} diff --git a/deploy/templates/ingress.yaml b/deploy/templates/ingress.yaml deleted file mode 100644 index 0283ddd..0000000 --- a/deploy/templates/ingress.yaml +++ /dev/null @@ -1,41 +0,0 @@ -{{- if .Values.ingress.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ include "deploy.fullname" . }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- with .Values.ingress.className }} - ingressClassName: {{ . }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - pathType: {{ .pathType }} - backend: - service: - name: {{ include "deploy.serviceFullname" (dict "root" $ "svcKey" .serviceName) }} - port: - number: {{ .servicePort }} - {{- end }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/deploy/templates/metrics_aggregator/deployment.yaml b/deploy/templates/metrics_aggregator/deployment.yaml deleted file mode 100644 index 316ac1d..0000000 --- a/deploy/templates/metrics_aggregator/deployment.yaml +++ /dev/null @@ -1,70 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "metrics_aggregator") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: metrics-aggregator -spec: - replicas: {{ .Values.services.metrics_aggregator.replicaCount | default 1 }} - selector: - matchLabels: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "metrics_aggregator") | nindent 6 }} - template: - metadata: - labels: - {{- include "deploy.labels" . | nindent 8 }} - app.kubernetes.io/component: metrics-aggregator - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "deploy.serviceAccountName" . }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: metrics-aggregator - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ .Values.imageRegistry }}/{{ .Values.services.metrics_aggregator.repository }}:{{ .Values.imageTag | default .Chart.AppVersion }}" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: {{ .Values.services.metrics_aggregator.port }} - protocol: TCP - envFrom: - - configMapRef: - name: {{ .Values.configMapName }} - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 15 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with .Values.services.metrics_aggregator.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file diff --git a/deploy/templates/metrics_aggregator/service.yaml b/deploy/templates/metrics_aggregator/service.yaml deleted file mode 100644 index 30945ae..0000000 --- a/deploy/templates/metrics_aggregator/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "metrics_aggregator") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: metrics-aggregator -spec: - type: ClusterIP - ports: - - port: {{ .Values.services.metrics_aggregator.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "metrics_aggregator") | nindent 4 }} \ No newline at end of file diff --git a/deploy/templates/secret.yaml b/deploy/templates/secret.yaml deleted file mode 100644 index 1adda38..0000000 --- a/deploy/templates/secret.yaml +++ /dev/null @@ -1 +0,0 @@ -{{/* Secret disabled — all config via ConfigMap */}} diff --git a/deploy/templates/serviceaccount.yaml b/deploy/templates/serviceaccount.yaml deleted file mode 100644 index 8167738..0000000 --- a/deploy/templates/serviceaccount.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "deploy.serviceAccountName" . }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automount }} -{{- end }} diff --git a/deploy/templates/static_server/deployment.yaml b/deploy/templates/static_server/deployment.yaml deleted file mode 100644 index bc91ea0..0000000 --- a/deploy/templates/static_server/deployment.yaml +++ /dev/null @@ -1,78 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "static_server") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: static-server -spec: - replicas: {{ .Values.services.static_server.replicaCount | default 1 }} - selector: - matchLabels: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "static_server") | nindent 6 }} - template: - metadata: - labels: - {{- include "deploy.labels" . | nindent 8 }} - app.kubernetes.io/component: static-server - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "deploy.serviceAccountName" . }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: static-server - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ .Values.imageRegistry }}/{{ .Values.services.static_server.repository }}:{{ .Values.imageTag | default .Chart.AppVersion }}" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: {{ .Values.services.static_server.port }} - protocol: TCP - envFrom: - - configMapRef: - name: {{ .Values.configMapName }} - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 10 - periodSeconds: 15 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - {{- with .Values.services.static_server.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.services.static_server.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - volumes: - - name: shared-data - persistentVolumeClaim: - claimName: shared-data - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file diff --git a/deploy/templates/static_server/service.yaml b/deploy/templates/static_server/service.yaml deleted file mode 100644 index fa555fc..0000000 --- a/deploy/templates/static_server/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "deploy.serviceFullname" (dict "root" . "svcKey" "static_server") }} - labels: - {{- include "deploy.labels" . | nindent 4 }} - app.kubernetes.io/component: static-server -spec: - type: ClusterIP - ports: - - port: {{ .Values.services.static_server.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "deploy.serviceSelectorLabels" (dict "root" . "svcKey" "static_server") | nindent 4 }} \ No newline at end of file diff --git a/deploy/values.yaml b/deploy/values.yaml deleted file mode 100644 index 3c34f8e..0000000 --- a/deploy/values.yaml +++ /dev/null @@ -1,212 +0,0 @@ -# Global image registry and tag -imageRegistry: "" -imageTag: "" - -# External ConfigMap (managed outside Helm) -configMapName: "app-env" - -# Service definitions -services: - app: - repository: app - port: 3000 - replicaCount: 2 - autoscaling: - enabled: true - minReplicas: 2 - maxReplicas: 10 - targetCPUUtilization: 80 - command: - - "app" - - "--bind" - - "0.0.0.0:3000" - resources: - requests: - cpu: 200m - memory: 256Mi - limits: - cpu: "1" - memory: 512Mi - volumeMounts: - - name: shared-data - mountPath: /data/repos - subPath: repos - - name: shared-data - mountPath: /data/avatars - subPath: avatars - - name: shared-data - mountPath: /data/files - subPath: files - - email_worker: - repository: email-worker - port: 8084 - replicaCount: 1 - autoscaling: - enabled: false # email must stay at 1 replica - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 256Mi - - git_hook: - repository: git-hook - port: 8083 - replicaCount: 1 - autoscaling: - enabled: true - minReplicas: 1 - maxReplicas: 5 - targetCPUUtilization: 80 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 256Mi - volumeMounts: - - name: shared-data - mountPath: /data/repos - subPath: repos - - gitserver: - repository: gitserver - ports: - http: 8021 - ssh: 2222 - replicaCount: 1 - autoscaling: - enabled: true - minReplicas: 1 - maxReplicas: 5 - targetCPUUtilization: 80 - # SSH port must match the containerPort - extraEnv: - APP_SSH_PORT: "2222" - # SSH service config (MetalLB + Cilium) - # Shared IP: nginx ingress (80/443) + SSH (22) on same VIP - # Requires ingress-nginx svc also annotated with allow-shared-ip: "gitdata-shared" - sshService: - annotations: {} - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 256Mi - volumeMounts: - - name: shared-data - mountPath: /data/repos - subPath: repos - - metrics_aggregator: - repository: metrics-aggregator - port: 9090 - replicaCount: 1 - autoscaling: - enabled: true - minReplicas: 1 - maxReplicas: 5 - targetCPUUtilization: 80 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 256Mi - - static_server: - repository: static-server - port: 8081 - replicaCount: 1 - autoscaling: - enabled: true - minReplicas: 1 - maxReplicas: 5 - targetCPUUtilization: 80 - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - cpu: 200m - memory: 128Mi - volumeMounts: - - name: shared-data - mountPath: /data - subPath: static - -# Ingress -ingress: - enabled: true - className: "nginx" - annotations: - cert-manager.io/cluster-issuer: "cloudflare-acme-cluster-issuer" - nginx.ingress.kubernetes.io/proxy-body-size: "0" - nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" - nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" - nginx.ingress.kubernetes.io/affinity: "cookie" - nginx.ingress.kubernetes.io/session-cookie-name: "INGRESSROUTE" - nginx.ingress.kubernetes.io/session-cookie-path: "/" - nginx.ingress.kubernetes.io/session-cookie-max-age: "86400" - nginx.ingress.kubernetes.io/enable-real-ip: "true" - nginx.ingress.kubernetes.io/real-ip-header: "X-Forwarded-For" - nginx.ingress.kubernetes.io/use-forwarded-headers: "true" - hosts: - - host: gitdata.ai - paths: - - path: / - pathType: Prefix - serviceName: app - servicePort: 3000 - - host: static.gitdata.ai - paths: - - path: / - pathType: Prefix - serviceName: static_server - servicePort: 8081 - - host: git.gitdata.ai - paths: - - path: / - pathType: Prefix - serviceName: gitserver - servicePort: 8021 - tls: - - secretName: gitdata-ai-tls - hosts: - - gitdata.ai - - static.gitdata.ai - - git.gitdata.ai - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - create: true - automount: true - annotations: {} - name: "" - -podSecurityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - fsGroupChangePolicy: OnRootMismatch - -securityContext: - capabilities: - drop: - - ALL - readOnlyRootFilesystem: false - -nodeSelector: {} -tolerations: [] -affinity: {} diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile deleted file mode 100644 index 5d995d2..0000000 --- a/docker/app.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates libssl3 openssh-client procps git \ - && rm -rf /var/lib/apt/lists/* -RUN git config --system --add safe.directory '*' -WORKDIR /app -COPY ./target/release/app /bin -EXPOSE 3000 -CMD ["app"] \ No newline at end of file diff --git a/docker/email.Dockerfile b/docker/email.Dockerfile index 54588a7..207dde6 100644 --- a/docker/email.Dockerfile +++ b/docker/email.Dockerfile @@ -1,8 +1,61 @@ -FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates libssl3 \ +# GitDataAI Backend - Email Service +# Multi-stage build for Rust application + +# Stage 1: Build the application +FROM rust:1.96-bookworm AS builder + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libssl-dev \ + libpq-dev \ + cmake \ && rm -rf /var/lib/apt/lists/* + +# Create app directory WORKDIR /app -COPY ./target/release/email-worker /bin -EXPOSE 8084 -CMD ["email-worker"] \ No newline at end of file + +# Copy workspace files +COPY Cargo.toml Cargo.lock ./ +COPY app/ app/ +COPY lib/ lib/ + +# Build the application in release mode +RUN cargo build --release --bin email-service + +# Stage 2: Create runtime image +FROM debian:bookworm-slim + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + libssl3 \ + libpq5 \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN useradd -r -s /bin/false appuser + +# Create directories +RUN mkdir -p /app/logs \ + && chown -R appuser:appuser /app + +# Copy binary from builder +COPY --from=builder /app/target/release/email-service /app/email-service + +# Set ownership +RUN chown -R appuser:appuser /app + +# Switch to non-root user +USER appuser + +# Set working directory +WORKDIR /app + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD pgrep email-service || exit 1 + +# Run the application +CMD ["./email-service"] \ No newline at end of file diff --git a/docker/githook.Dockerfile b/docker/githook.Dockerfile deleted file mode 100644 index baa85ee..0000000 --- a/docker/githook.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates libssl3 git \ - && rm -rf /var/lib/apt/lists/* -RUN git config --system --add safe.directory '*' -WORKDIR /app -COPY ./target/release/git-hook /bin -EXPOSE 8083 -CMD ["git-hook"] \ No newline at end of file diff --git a/docker/gitserver.Dockerfile b/docker/gitserver.Dockerfile deleted file mode 100644 index f109f02..0000000 --- a/docker/gitserver.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates libssl3 git openssh-client \ - && rm -rf /var/lib/apt/lists/* -RUN git config --system --add safe.directory '*' -WORKDIR /app -COPY ./target/release/gitserver /bin -EXPOSE 8021 2222 -CMD ["gitserver"] \ No newline at end of file diff --git a/docker/metrics.Dockerfile b/docker/metrics.Dockerfile deleted file mode 100644 index 6fa1266..0000000 --- a/docker/metrics.Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates libssl3 \ - && rm -rf /var/lib/apt/lists/* -WORKDIR /app -COPY ./target/release/metrics-aggregator /bin -EXPOSE 9090 -CMD ["metrics-aggregator"] \ No newline at end of file diff --git a/docker/static.Dockerfile b/docker/static.Dockerfile deleted file mode 100644 index 6a9113a..0000000 --- a/docker/static.Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates libssl3 \ - && rm -rf /var/lib/apt/lists/* -WORKDIR /app -COPY ./target/release/static-server /bin -EXPOSE 8081 -CMD ["static-server"] \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 16ecca4..ef614d2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -16,15 +16,7 @@ export default defineConfig([ reactRefresh.configs.vite, ], languageOptions: { - ecmaVersion: 2020, globals: globals.browser, }, - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowExportNames: ['useThemeCustomization', 'useThemePreset', 'useRoom', 'useOptionalRoom', 'resetAllThemeVars', 'loadThemeVars', 'applyThemePreset'] }, - ], - 'react-hooks/exhaustive-deps': 'warn', - }, }, ]) diff --git a/gene.md b/gene.md deleted file mode 100644 index 5dcea19..0000000 --- a/gene.md +++ /dev/null @@ -1,901 +0,0 @@ -# Gene 方案 - -## 这份文档针对什么 - -这个项目里,`Skill` 已经不是一个抽象概念,而是完整的业务实体: - -* 后端有 `project_skill` 持久化模型 -* Git 同步会扫描仓库里的 `SKILL.md` -* 聊天构建会把启用的技能注入上下文 -* 前端有技能列表、详情、编辑、删除、扫描 -* 内建技能模板也已经存在 - -所以这里不应该“用 Gene 替换 Skill”,而应该是: - -* `Skill` 继续负责执行和交付能力 -* `Gene` 作为新增元层,负责让技能可演化、可比较、可追踪、可继承 - ---- - -## 设计结论 - -`Gene` 的正确位置不是新的执行系统,也不是 `Skill` 的替代品,而是 `Skill` 的生命周期治理层。 - -```text -Skill = 可执行内容 + 上下文注入 + 业务交付 -Gene = 演化族 + 版本谱系 + 评估记录 + 选择记录 -``` - -更准确地说: - -```text -Skill 是可执行的技能内容,负责被扫描、编辑、启用、注入和交付能力。 - -Gene 是 Skill 的演化族,负责组织一个 Skill 在项目内的版本、来源、谱系、评估和选择记录。 - -GeneRevision 是 Gene 下的不可变版本节点,绑定到某个 Skill 内容快照。 - -GeneEvaluation 是对某个 GeneRevision 的评估结果。 - -GeneSelection 是对当前有效 GeneRevision 的显式选择记录。 -``` - ---- - -## 项目现状 - -### Skill 已经落在这些地方 - -* `libs/models/projects/project_skill.rs` - - * 定义了 `project_skill` 实体 - * 已包含 `source` - * 已包含 `repo_id` - * 已包含 `commit_sha` - * 已包含 `blob_hash` - * 已包含 `content` - * 已包含 `metadata` - * 已包含 `enabled` - -* `libs/git/hook/sync/mod.rs` - - * 扫描仓库中的 `SKILL.md` - * 从 frontmatter 解析 `name`、`description`、`license`、`compatibility` - * 用 `commit_sha` 和 `blob_hash` 做增量同步 - -* `libs/agent/skills/templates.rs` - - * 内建技能模板已经编译进程序 - * 这些模板本质上是“系统内置 Skill” - -* `libs/agent/chat/message_builder.rs` - - * 读取项目中启用的技能 - * 把技能注入对话上下文 - * 和 embedding / perception 一起影响模型行为 - -* `src/app/project/skills/*` - - * 已有技能管理 UI - * 能查看、编辑、删除、扫描技能 - ---- - -## 现有 Skill 的问题 - -当前 `Skill` 已经能用,但还不够“可进化”: - -* 只有内容,没有明确的演化关系 -* 只有当前状态,没有谱系 -* 只有启用/禁用,没有版本选择 -* 只有来源信息,没有变体比较 -* 只有同步时间和 blob hash,没有“为什么变成这样”的记录 -* 有评估空间,但没有评估结果和版本绑定 -* 有扫描和编辑能力,但没有可审计的回滚与选择记录 - -换句话说,现在的 `Skill` 更像“静态资产”,还不是“进化单元”。 - ---- - -## 设计原则 - -### 1. Skill 仍然是唯一执行实体 - -`Skill` 继续负责: - -* 被扫描 -* 被编辑 -* 被启用或禁用 -* 被注入聊天上下文 -* 交付实际能力 - -`Gene` 不直接执行任务,不直接调用工具,不直接参与上下文拼装。 - ---- - -### 2. Gene 只管理 Skill 的演化元数据 - -`Gene` 管理的是: - -* 版本 -* 来源 -* 父子关系 -* 变体 -* 评估 -* 选择 -* 淘汰 -* 回滚 - -它不应该成为第二套 `Skill` 系统。 - ---- - -### 3. GeneRevision 必须不可变 - -`Skill` 可以是当前可编辑对象。 - -`GeneRevision` 是不可变演化节点。一旦创建,不应再修改: - -* 内容快照 -* 父代关系 -* 来源信息 -* `commit_sha` -* `blob_hash` -* `content_hash` -* `mutation_reason` -* `mutation_diff` - -后续任何内容变化、prompt 变化、工具权限变化、上下文注入规则变化,都应该产生新的 `GeneRevision`。 - ---- - -### 4. Evaluation 必须绑定到具体 Revision - -评估不是评价一个抽象的 `Gene`,而是评价某个具体版本。 - -因此: - -```text -GeneEvaluation 必须绑定 revision_id。 -``` - -否则同一个 `Gene` 下存在多个版本时,评估结果无法精确归因。 - ---- - -### 5. Selection 必须显式记录 - -如果系统选择了某个版本作为当前有效版本,必须记录: - -* 选中了哪个 revision -* 为什么选它 -* 谁选的 -* 依据什么策略选的 -* 什么时候选的 -* 是否仍然 active - -这能支持审计、回滚和后续自动选择。 - ---- - -### 6. SKILL.md 仍然是仓库内 Skill 的事实来源 - -仓库里的 `SKILL.md` 仍然是 Git 同步的事实来源。 - -`Gene` 只记录它如何演化,不改变仓库同步的基本语义。 - ---- - -## 核心概念 - -### Skill - -`Skill` 是现有业务实体。 - -它负责: - -```text -可执行内容 -上下文注入 -用户可见编辑 -Git 扫描 -启用 / 禁用 -业务交付 -``` - -### Gene - -`Gene` 是某个 Skill 的演化族。 - -它负责组织这个 Skill 的生命周期: - -```text -这个能力从哪里来 -经历过哪些版本 -有哪些变体 -评估结果如何 -当前选择哪个版本 -哪些版本被淘汰 -``` - -### GeneRevision - -`GeneRevision` 是 `Gene` 下的一个不可变版本节点。 - -它绑定某个 `Skill` 的内容状态,例如: - -```text -skill_id -skill_slug -commit_sha -blob_hash -content_hash -content_snapshot_ref -``` - -### GeneEvaluation - -`GeneEvaluation` 是对某个 `GeneRevision` 的评估结果。 - -它回答: - -```text -这个版本好不好? -在哪个数据集上测的? -指标是什么? -是否通过? -成本和延迟如何? -失败样本是什么? -``` - -### GeneSelection - -`GeneSelection` 是对当前有效版本的显式选择记录。 - -它回答: - -```text -当前选中哪个 revision? -为什么选它? -谁选的? -依据什么策略? -是否还在生效? -``` - ---- - -## Gene 和 Skill 的关系 - -可以把关系理解为: - -```text -Gene 1 ── has many ── GeneRevision -GeneRevision ── references ── Skill snapshot -GeneRevision ── has many ── GeneEvaluation -Gene ── has one active ── GeneSelection -GeneSelection ── selects ── GeneRevision -Skill ── executes ── actual capability -``` - -也就是说: - -* `Skill` 解决“能不能做” -* `Gene` 解决“该保留哪个版本、为什么保留、怎么变体、怎么传播” -* `GeneRevision` 解决“这个能力在某一刻具体长什么样” -* `GeneEvaluation` 解决“这个版本是否足够好” -* `GeneSelection` 解决“当前应该用哪个版本” - ---- - -## 数据模型 - -### ProjectGene - -```text -ProjectGene - - gene_id - - project_uuid - - skill_id - - skill_slug - - name - - description - - owner - - status - - created_at - - updated_at -``` - -说明: - -* `gene_id` 是 Gene 的唯一标识 -* `project_uuid` 绑定项目 -* `skill_id` / `skill_slug` 绑定现有 Skill -* `status` 可为 `active`、`archived`、`deprecated` -* `owner` 用于责任归属 - ---- - -### ProjectGeneRevision - -```text -ProjectGeneRevision - - revision_id - - gene_id - - version - - parent_revision_id - - origin - - source - - skill_id - - skill_slug - - commit_sha - - blob_hash - - content_hash - - content_snapshot_ref - - mutation_reason - - mutation_diff - - created_by - - created_at -``` - -说明: - -* `revision_id` 是内部唯一版本节点 -* `version` 是用户可见版本号,不承担唯一性职责 -* `parent_revision_id` 表示单父版本关系 -* `origin` 表示来源,例如 `git_sync`、`manual_edit`、`builtin_template`、`migration` -* `source` 复用现有 `Skill.source` -* `commit_sha` / `blob_hash` 记录 Git 来源 -* `content_hash` 记录内容稳定标识 -* `content_snapshot_ref` 用于指向当时的内容快照 -* `mutation_reason` 记录为什么产生这个版本 -* `mutation_diff` 记录相对父版本的变化 - ---- - -### ProjectGeneEvaluation - -```text -ProjectGeneEvaluation - - evaluation_id - - gene_id - - revision_id - - eval_name - - evaluator - - dataset_ref - - dataset_version - - metric_name - - score - - threshold - - passed - - sample_count - - failure_count - - latency_ms_avg - - cost - - result_summary - - result_json - - evaluated_at -``` - -说明: - -* `revision_id` 必填 -* `score` 不应脱离 `metric_name` 单独解释 -* `threshold` 用于判断是否通过 -* `sample_count` 和 `failure_count` 用于判断评估可信度 -* `result_json` 存放详细结果、失败样本、分项指标等 - ---- - -### ProjectGeneSelection - -```text -ProjectGeneSelection - - selection_id - - gene_id - - selected_revision_id - - project_uuid - - policy - - reason - - selected_by - - selected_at - - active -``` - -说明: - -* 同一个 `gene_id` 同一时间只能有一个 active selection -* `policy` 可以是 `manual`、`latest_passed`、`best_score`、`stable_low_cost` -* `reason` 用于审计和回滚 -* `selected_by` 可以是用户、系统或自动策略 - ---- - -## 关于 GeneLineage - -MVP 阶段不单独引入 `GeneLineage` 表。 - -原因是:如果每个版本只有一个父版本,`ProjectGeneRevision.parent_revision_id` 已经足够表达谱系。 - -```text -MVP:单父版本树 -Future:多父 DAG / merge / cross-skill inheritance -``` - -未来如果需要支持合并、交叉继承或复杂谱系,再引入: - -```text -ProjectGeneEdge - - parent_revision_id - - child_revision_id - - edge_type - - mutation_reason - - mutation_diff -``` - ---- - -## 关于 Variant - -`Variant` 是 Gene 的重要能力,但不一定是 MVP 的独立表。 - -这些变化都可以先作为新的 `GeneRevision` 表达: - -* 更严格的 prompt -* 更短的 prompt -* 不同工具权限 -* 不同上下文注入规则 -* 不同评估阈值 - -MVP 中: - -```text -不单独引入 ProjectGeneVariant。 -所有变体先作为 GeneRevision 表达。 -``` - -当系统需要以下能力时,再引入 `GeneExperiment` / `GeneVariant`: - -* A/B 实验 -* 并行流量分配 -* 实验分组 -* 统计显著性 -* 多候选版本同时比较 - ---- - -## Git 同步流程 - -当前 Git 同步已经会扫描仓库中的 `SKILL.md`,并使用 `commit_sha` 和 `blob_hash` 做增量同步。 - -引入 Gene 后,Git 同步流程建议变成: - -```text -当 Git 同步发现 SKILL.md 的 blob_hash 变化时: - -1. 正常创建或更新 project_skill。 -2. 查找该 skill 对应的 project_gene。 -3. 如果不存在 project_gene,则创建一个。 -4. 查找该 gene 下最新的 project_gene_revision。 -5. 如果新的 blob_hash / content_hash 不同,则创建新的 GeneRevision。 -6. 将上一 revision 设为 parent_revision_id。 -7. mutation_reason 默认记录为 git_sync。 -8. mutation_diff 记录上一个 SKILL.md 与当前 SKILL.md 的 diff。 -9. 不自动改变 selected_revision,除非选择策略明确允许。 -``` - -关键约束: - -```text -Git sync 可以产生新的 GeneRevision。 -Git sync 不应默认切换当前线上选中版本。 -``` - -这样可以避免仓库变更自动导致线上行为漂移。 - ---- - -## 手动编辑流程 - -当用户在 UI 中编辑 `Skill` 内容时: - -```text -1. 更新 project_skill。 -2. 计算新的 content_hash。 -3. 查找对应 project_gene。 -4. 创建新的 project_gene_revision。 -5. parent_revision_id 指向编辑前的 revision。 -6. mutation_reason 记录为 manual_edit。 -7. mutation_diff 记录编辑前后的差异。 -8. 可选择是否自动把新 revision 标记为 selected。 -``` - -建议 MVP 默认: - -```text -手动编辑后创建新 revision,但不自动覆盖 active selection。 -由用户显式选择是否启用该 revision。 -``` - -如果产品希望“编辑即生效”,也可以让 selection 同步更新,但必须记录: - -```text -policy = manual_edit_auto_select -reason = "User edited skill content" -``` - ---- - -## 聊天上下文注入流程 - -现有流程中,`libs/agent/chat/message_builder.rs` 读取项目中启用的 `Skill`,并把技能注入对话上下文。 - -引入 Gene 后,这个原则不变: - -```text -message_builder 不直接读取 Gene 内容。 -message_builder 仍然读取启用的 Skill。 -Gene 只影响哪个 Skill revision 被视为推荐版本或当前选中版本。 -``` - -如果未来要让 `GeneSelection` 影响上下文注入,推荐路径是: - -```text -GeneSelection - -> resolve selected GeneRevision - -> materialize / update project_skill - -> message_builder reads project_skill -``` - -不建议: - -```text -message_builder - -> read Gene - -> assemble prompt -``` - -因为这会让 `Gene` 偷偷变成新的执行层。 - ---- - -## API 设计 - -MVP API 可以包括: - -```text -GET /projects/:project_uuid/skills/:skill_id/gene -POST /projects/:project_uuid/skills/:skill_id/gene - -GET /projects/:project_uuid/genes/:gene_id/revisions -POST /projects/:project_uuid/genes/:gene_id/revisions - -GET /projects/:project_uuid/genes/:gene_id/evaluations -POST /projects/:project_uuid/genes/:gene_id/evaluations - -GET /projects/:project_uuid/genes/:gene_id/selection -POST /projects/:project_uuid/genes/:gene_id/select -``` - -选择接口示例: - -```json -{ - "selected_revision_id": "rev_123", - "policy": "manual", - "reason": "Higher pass rate on regression eval" -} -``` - -评估写入接口示例: - -```json -{ - "revision_id": "rev_123", - "eval_name": "skill_regression_eval", - "dataset_ref": "datasets/skill-regression-v1", - "dataset_version": "2026-01-01", - "metric_name": "task_success_rate", - "score": 0.92, - "threshold": 0.85, - "passed": true, - "sample_count": 100, - "failure_count": 8, - "latency_ms_avg": 1200, - "cost": 0.34 -} -``` - ---- - -## UI 设计 - -`Gene` 不替代现有技能管理 UI。 - -推荐把它放在 `Skill Detail` 页面中的一个“演化”标签页。 - -```text -Skill Detail - - 基本信息 - - 内容编辑 - - 启用状态 - - Evolution / Gene - - 当前选中 revision - - 版本列表 - - 父子关系 - - 每个版本的 diff - - 每个版本的评估结果 - - 选择按钮 - - 回滚按钮 -``` - -MVP UI 可以先做只读: - -```text -1. 显示 Gene 信息 -2. 显示 Revision 列表 -3. 显示每个 Revision 的来源、时间、commit_sha、blob_hash -4. 显示相邻 Revision 的 diff -``` - -第二阶段再加入: - -```text -1. 手动选择 revision -2. 回滚 revision -3. 展示评估结果 -4. 根据评估结果推荐版本 -``` - ---- - -## 评估指标 - -Gene 的核心不是“更像生物学”,而是“更像可验证的演化对象”。 - -每个 `GeneRevision` 都应该支持评估,例如: - -* 任务成功率 -* 失败率 -* 平均耗时 -* 误触发率 -* 人工接受率 -* 回滚率 -* 成本 -* 稳定性 -* 安全失败数 -* 用户满意度 - -没有评估的 Gene,只是重命名后的 Skill 管理。 - ---- - -## 选择策略 - -如果存在多个 GeneRevision,系统应该能选择更优版本。 - -MVP 阶段不做自动选择,只做人工选择和可审计回滚。 - -后续选择规则可以逐步加入: - -```text -1. 先看是否通过基础测试 -2. 再看近期成功率 -3. 再看失败率 -4. 再看成本 -5. 再看延迟 -6. 再看稳定性 -7. 再看人工接受率 -``` - -可选策略: - -```text -manual -latest_passed -best_score -stable_low_cost -lowest_latency -highest_acceptance_rate -``` - -自动选择必须满足: - -```text -有评估数据 -有样本量 -有失败分类 -有回滚机制 -有选择审计记录 -``` - ---- - -## Migration 策略 - -对于已有的 `project_skill`,可以执行一次初始化迁移: - -```text -对每个 project_skill: - -1. 创建 project_gene。 -2. 创建初始 project_gene_revision。 -3. revision.origin = migration。 -4. revision.skill_id = project_skill.id。 -5. revision.skill_slug = project_skill.slug。 -6. revision.commit_sha = project_skill.commit_sha。 -7. revision.blob_hash = project_skill.blob_hash。 -8. revision.content_hash = hash(project_skill.content)。 -9. revision.content_snapshot_ref = 当前 Skill 内容快照。 -10. 创建 active project_gene_selection。 -11. selected_revision_id = 初始 revision。 -12. policy = migration。 -13. reason = "Initial gene selection from existing project_skill"。 -``` - -这样现有 Skill 都可以无损进入 Gene 生命周期模型。 - ---- - -## 推荐实现顺序 - -```text -1. 保持 Skill 执行、扫描、编辑、注入流程不变。 -2. 增加 project_gene 和 project_gene_revision 表。 -3. 为现有 project_skill 执行一次 migration:每个 Skill 创建一个 Gene 和初始 Revision。 -4. 在 Git sync 发现 blob_hash 变化时,自动创建新的 GeneRevision。 -5. 在 Skill 详情页增加只读版本历史和 diff 展示。 -6. 增加 ProjectGeneEvaluation 表和手动写入 API。 -7. 增加 ProjectGeneSelection 表和人工选择 / 回滚能力。 -8. 当评估数据稳定后,再做自动选择策略。 -9. 最后再考虑 Variant / Experiment / A-B 测试。 -``` - ---- - -## 落地判断标准 - -一个能力如果只是: - -* 能被扫描 -* 能被编辑 -* 能被启用或禁用 -* 能被注入上下文 -* 能交付业务能力 - -它还是 `Skill`。 - -一个能力如果还能: - -* 被追踪来源 -* 被比较版本 -* 被记录变体 -* 被评估好坏 -* 被继承和淘汰 -* 被审计选择 -* 被安全回滚 - -它才进入 `Gene` 管理范畴。 - ---- - -## 非目标 - -Gene MVP 不做以下事情: - -```text -1. 不替换 project_skill。 -2. 不改变 SKILL.md 作为仓库技能事实来源的地位。 -3. 不直接参与聊天上下文构建。 -4. 不直接执行工具调用。 -5. 不自动改写 Skill 内容。 -6. 不在没有评估和回滚机制的情况下自动切换线上版本。 -7. 不一开始支持复杂遗传算法、交叉、随机变异或自动进化。 -8. 不新增一套和 Skill 并列的执行 UI。 -``` - ---- - -## 风险与约束 - -### 1. Gene 变成另一个 Skill - -风险: - -```text -Gene 中也开始存 prompt、工具权限、上下文注入规则,并且聊天时直接读取 Gene。 -``` - -规避: - -```text -Gene 不存放执行主内容。 -执行内容仍然归 Skill 所有。 -GeneRevision 只引用或快照 Skill 的某个状态。 -``` - ---- - -### 2. 评估绑定到可变 Skill - -风险: - -```text -评估结果只挂在 skill_id 上,Skill 内容变化后,评估记录失真。 -``` - -规避: - -```text -评估必须绑定 revision_id + content_hash / blob_hash。 -``` - ---- - -### 3. 自动选择过早上线 - -风险: - -```text -没有足够评估数据时自动切换版本,导致线上行为漂移。 -``` - -规避: - -```text -MVP 只做人工选择和可审计回滚。 -自动选择必须依赖稳定评估和回滚机制。 -``` - ---- - -### 4. mutation_diff 膨胀 - -风险: - -```text -每个版本都保存完整 diff,长期可能膨胀。 -``` - -规避: - -```text -MVP 可直接存 mutation_diff。 -后续引入 mutation_diff_ref 或对象存储引用。 -``` - ---- - -### 5. version 语义不清 - -风险: - -```text -version 同时承担用户展示、唯一标识、Git 版本等多种含义。 -``` - -规避: - -```text -revision_id = 系统内部唯一版本节点 -version = 用户可见版本号 -commit_sha / blob_hash = Git 来源版本标识 -content_hash = 内容稳定标识 -``` - ---- - -## 最终结论 - -这个项目已经具备 `Skill` 的完整工程闭环。 - -`Gene` 的正确位置不是替换它,而是补上它缺失的演化层: - -```text -Skill 负责落地执行 -Gene 负责生命周期治理 -GeneRevision 负责不可变版本节点 -GeneEvaluation 负责版本质量判断 -GeneSelection 负责显式选择和回滚 -``` - -两者结合后,技能系统才从“可配置”变成“可进化”。 diff --git a/index.html b/index.html index 77fb581..8346b2f 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,13 @@ - - - - - GitData.AI - - - -
- - + + + + + GitDataAI + + +
+ + diff --git a/lib.rs b/lib.rs deleted file mode 100644 index 1c53c6a..0000000 --- a/lib.rs +++ /dev/null @@ -1 +0,0 @@ -// Frontend embedding is handled by libs/frontend crate. ci diff --git a/libs/agent/Cargo.toml b/libs/agent/Cargo.toml deleted file mode 100644 index 5541839..0000000 --- a/libs/agent/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "agent" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true -[lib] -path = "lib.rs" -name = "agent" -[features] -default = ["rig"] -rig = [] -[dependencies] -rig-core = { workspace = true, features = ["derive"] } -tokio = { workspace = true } -async-trait = { workspace = true } -qdrant-client = { workspace = true } -sea-orm = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -db = { workspace = true } -config = { path = "../config" } -models = { workspace = true } -chrono = { workspace = true } -uuid = { workspace = true, features = ["v7"] } -futures = { workspace = true } -tiktoken-rs = { workspace = true } -once_cell = { workspace = true } -regex = { workspace = true } -tracing = { workspace = true } -metrics = { workspace = true } -rust_decimal = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -utoipa = { workspace = true } -tokio-stream = { workspace = true } -redis = { workspace = true, features = ["tokio-comp"] } -queue = { workspace = true } -[lints] -workspace = true diff --git a/libs/agent/agent/mod.rs b/libs/agent/agent/mod.rs deleted file mode 100644 index 7d966af..0000000 --- a/libs/agent/agent/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Rig-based agent using rig's built-in Agent with full feature support. - -pub mod rig_tool; -pub use rig_tool::{AgentResponse, RigAgentService, StreamChunk}; diff --git a/libs/agent/agent/rig_tool.rs b/libs/agent/agent/rig_tool.rs deleted file mode 100644 index 192bf68..0000000 --- a/libs/agent/agent/rig_tool.rs +++ /dev/null @@ -1,234 +0,0 @@ -use futures::Stream; -use futures::StreamExt; -use rig::{ - agent::{AgentBuilder, MultiTurnStreamItem}, - client::CompletionClient, - completion::Prompt, - streaming::{StreamedAssistantContent, StreamingPrompt}, -}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::ReceiverStream; - -use crate::client::AiClientConfig; -use crate::error::AgentError; - -#[derive(Debug)] -pub struct AgentResponse { - pub content: String, - pub input_tokens: u64, - pub output_tokens: u64, -} - -#[derive(Debug)] -pub enum StreamChunk { - Text(String), - Final { - content: String, - input_tokens: u64, - output_tokens: u64, - }, -} - -pub struct RigAgentService { - config: AiClientConfig, - model_name: String, -} - -impl RigAgentService { - pub fn new(config: AiClientConfig, model_name: impl Into) -> Self { - Self { - config, - model_name: model_name.into(), - } - } - - pub async fn prompt( - &self, - system_prompt: &str, - user_input: &str, - ) -> std::result::Result { - let client = self.config.build_rig_client(); - let model = client.completion_model(&self.model_name); - - let agent = AgentBuilder::new(model).preamble(system_prompt).build(); - - let response = agent - .prompt(user_input) - .extended_details() - .await - .map_err(|e: rig::completion::PromptError| AgentError::OpenAi(e.to_string()))?; - - Ok(AgentResponse { - content: response.output, - input_tokens: response.usage.input_tokens, - output_tokens: response.usage.output_tokens, - }) - } - - pub async fn prompt_with_tools( - &self, - system_prompt: &str, - user_input: &str, - tools: Vec>, - max_turns: usize, - ) -> std::result::Result { - let client = self.config.build_rig_client(); - let model = client.completion_model(&self.model_name); - - let agent = AgentBuilder::new(model) - .preamble(system_prompt) - .tools(tools) - .default_max_turns(max_turns) - .build(); - - let response = agent - .prompt(user_input) - .max_turns(max_turns) - .extended_details() - .await - .map_err(|e: rig::completion::PromptError| AgentError::OpenAi(e.to_string()))?; - - Ok(AgentResponse { - content: response.output, - input_tokens: response.usage.input_tokens, - output_tokens: response.usage.output_tokens, - }) - } - - pub async fn stream_prompt( - &self, - system_prompt: &str, - user_input: &str, - ) -> std::result::Result< - impl Stream>, - AgentError, - > { - let client = self.config.build_rig_client(); - let model = client.completion_model(&self.model_name); - - let agent = AgentBuilder::new(model).preamble(system_prompt).build(); - - let stream: rig::agent::StreamingResult<_> = agent.stream_prompt(user_input).await; - - let (tx, rx) = mpsc::channel::>(100); - - tokio::spawn(async move { - let mut final_content = String::new(); - - tokio::pin!(stream); - - while let Some(item) = stream.next().await { - match item { - Ok(MultiTurnStreamItem::StreamAssistantItem( - StreamedAssistantContent::Text(text), - )) => { - let _ = tx.send(Ok(StreamChunk::Text(text.text.clone()))).await; - final_content.push_str(&text.text); - } - Ok(MultiTurnStreamItem::StreamAssistantItem( - StreamedAssistantContent::ToolCall { - tool_call, - internal_call_id: _, - }, - )) => { - let args_str = match &tool_call.function.arguments { - serde_json::Value::String(s) => s.clone(), - v => serde_json::to_string(v).unwrap_or_default(), - }; - tracing::info!( - tool = %tool_call.function.name, - args = %args_str, - "rig_agent_streaming_tool_call" - ); - } - Ok(MultiTurnStreamItem::StreamUserItem( - rig::streaming::StreamedUserContent::ToolResult { tool_result, .. }, - )) => { - tracing::info!( - tool_result_id = %tool_result.id, - "rig_agent_streaming_tool_result" - ); - } - Ok(MultiTurnStreamItem::FinalResponse(resp)) => { - let usage = resp.usage(); - let _ = tx - .send(Ok(StreamChunk::Final { - content: final_content.clone(), - input_tokens: usage.input_tokens, - output_tokens: usage.output_tokens, - })) - .await; - } - Err(e) => { - let _ = tx.send(Err(AgentError::OpenAi(e.to_string()))).await; - } - _ => {} - } - } - }); - - Ok(ReceiverStream::new(rx)) - } - - pub async fn stream_prompt_with_tools( - &self, - system_prompt: &str, - user_input: &str, - tools: Vec>, - max_turns: usize, - ) -> std::result::Result< - impl Stream>, - AgentError, - > { - let client = self.config.build_rig_client(); - let model = client.completion_model(&self.model_name); - - let agent = AgentBuilder::new(model) - .preamble(system_prompt) - .tools(tools) - .default_max_turns(max_turns) - .build(); - - let stream = agent - .stream_prompt(user_input) - .with_history(Vec::::new()) - .multi_turn(max_turns) - .await; - let (tx, rx) = mpsc::channel::>(100); - tokio::spawn(async move { - let mut final_content = String::new(); - tokio::pin!(stream); - while let Some(item) = stream.next().await { - match item { - Ok(MultiTurnStreamItem::StreamAssistantItem( - StreamedAssistantContent::Text(text), - )) => { - let _ = tx.send(Ok(StreamChunk::Text(text.text.clone()))).await; - final_content.push_str(&text.text); - } - Ok(MultiTurnStreamItem::FinalResponse(resp)) => { - let usage = resp.usage(); - let _ = tx - .send(Ok(StreamChunk::Final { - content: final_content.clone(), - input_tokens: usage.input_tokens, - output_tokens: usage.output_tokens, - })) - .await; - } - Err(e) => { - let _ = tx.send(Err(AgentError::OpenAi(e.to_string()))).await; - } - _ => {} - } - } - }); - - Ok(ReceiverStream::new(rx)) - } - - pub fn count_tokens(&self, text: &str) -> Result { - crate::tokent::count_text(text, &self.model_name) - .map_err(|e| AgentError::Internal(e.to_string())) - } -} diff --git a/libs/agent/billing.rs b/libs/agent/billing.rs deleted file mode 100644 index 9271e2c..0000000 --- a/libs/agent/billing.rs +++ /dev/null @@ -1,668 +0,0 @@ -//! Billing service — handles user-level and project-level billing, deduction, -//! credit initialization, and error persistence. -//! -//! Architecture: -//! - Each user gets $10 personal balance on signup. -//! - Each project gets $20 balance only if it's the creator's first project, -//! $0 otherwise. -//! - AI usage is deducted from the project balance first; if insufficient, -//! falls through to the user's personal balance. -//! - Monthly quota only applies to pro users (is_pro = true). -//! - If both project and user balance are insufficient, a billing_error -//! record is persisted and an error is returned to the caller. - -use db::database::AppDatabase; -use models::agents::model_pricing; -use models::ai::billing_error; -use models::projects::{project, project_billing, project_billing_history}; -use models::users::{user_billing, user_billing_history}; -use rust_decimal::Decimal; -use sea_orm::*; -use uuid::Uuid; - -use crate::error::AgentError; - -fn default_user_balance() -> Decimal { - Decimal::new(100_000, 4) -} // $10.0000 -fn first_project_credit() -> Decimal { - Decimal::new(200_000, 4) -} // $20.0000 -const SUBSEQUENT_PROJECT_BALANCE: Decimal = Decimal::ZERO; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] -pub struct BillingRecord { - pub cost: f64, - pub currency: String, - pub input_tokens: i64, - pub output_tokens: i64, - pub deducted_from: String, // "project" or "user" -} - -#[derive(Debug)] -pub enum BillingResult { - Success(BillingRecord), - InsufficientBalance { message: String }, -} - -/// Record AI usage: deduct from project balance first, fall through to user balance. -/// -/// Returns `InsufficientBalance` if neither account can cover the cost. -/// On insufficient balance, a `billing_error` record is persisted for frontend display. -pub async fn record_ai_usage( - db: &AppDatabase, - project_uid: Uuid, - user_uid: Uuid, - model_id: Uuid, - input_tokens: i64, - output_tokens: i64, -) -> Result { - let total_cost = compute_cost(db, model_id, input_tokens, output_tokens).await?; - let currency = get_currency(db, model_id).await?; - - // Verify project exists - let _ = project::Entity::find_by_id(project_uid) - .one(db) - .await? - .ok_or_else(|| AgentError::Internal("Project not found".into()))?; - - // Attempt project-level deduction first - let project_result = deduct_from_project( - db, - project_uid, - total_cost, - ¤cy, - model_id, - input_tokens, - output_tokens, - ) - .await; - - match project_result { - Ok(()) => { - let cost_f64 = decimal_to_f64(total_cost); - tracing::info!( - project_id = %project_uid, - model_id = %model_id, - input_tokens, output_tokens, - cost = %cost_f64, - currency = %currency, - deducted_from = "project", - "ai_usage_recorded" - ); - Ok(BillingResult::Success(BillingRecord { - cost: cost_f64, - currency, - input_tokens, - output_tokens, - deducted_from: "project".to_string(), - })) - } - Err(_) => { - // Project balance insufficient — try user personal balance - let user_result = deduct_from_user( - db, - user_uid, - total_cost, - ¤cy, - project_uid, - model_id, - input_tokens, - output_tokens, - ) - .await; - - match user_result { - Ok(()) => { - let cost_f64 = decimal_to_f64(total_cost); - tracing::info!( - user_id = %user_uid, - project_id = %project_uid, - model_id = %model_id, - input_tokens, output_tokens, - cost = %cost_f64, - currency = %currency, - deducted_from = "user", - "ai_usage_recorded" - ); - Ok(BillingResult::Success(BillingRecord { - cost: cost_f64, - currency, - input_tokens, - output_tokens, - deducted_from: "user".to_string(), - })) - } - Err(insufficient_msg) => { - // Both project and user balance insufficient — persist error - persist_billing_error( - db, - "project", - project_uid, - "insufficient_balance", - &insufficient_msg, - Some(serde_json::json!({ - "user_id": user_uid.to_string(), - "model_id": model_id.to_string(), - "input_tokens": input_tokens, - "output_tokens": output_tokens, - "cost": decimal_to_f64(total_cost), - "currency": currency, - })), - ) - .await?; - - Ok(BillingResult::InsufficientBalance { - message: insufficient_msg, - }) - } - } - } - } -} - -/// Record personal AI usage against the user's own billing balance. -pub async fn record_user_ai_usage( - db: &AppDatabase, - user_uid: Uuid, - model_id: Uuid, - input_tokens: i64, - output_tokens: i64, -) -> Result { - let total_cost = compute_cost(db, model_id, input_tokens, output_tokens).await?; - let currency = get_currency(db, model_id).await?; - - match deduct_from_user_personal( - db, - user_uid, - total_cost, - ¤cy, - model_id, - input_tokens, - output_tokens, - ) - .await - { - Ok(()) => { - let cost_f64 = decimal_to_f64(total_cost); - tracing::info!( - user_id = %user_uid, - model_id = %model_id, - input_tokens, output_tokens, - cost = %cost_f64, - currency = %currency, - deducted_from = "user", - scope = "personal", - "ai_usage_recorded" - ); - Ok(BillingResult::Success(BillingRecord { - cost: cost_f64, - currency, - input_tokens, - output_tokens, - deducted_from: "user".to_string(), - })) - } - Err(insufficient_msg) => { - persist_billing_error( - db, - "user", - user_uid, - "insufficient_balance", - &insufficient_msg, - Some(serde_json::json!({ - "user_id": user_uid.to_string(), - "model_id": model_id.to_string(), - "input_tokens": input_tokens, - "output_tokens": output_tokens, - "cost": decimal_to_f64(total_cost), - "currency": currency, - "scope": "personal", - })), - ) - .await?; - - Ok(BillingResult::InsufficientBalance { - message: insufficient_msg, - }) - } - } -} - -/// Check whether a project + user has sufficient combined balance for a potential AI call. -/// Called before starting AI processing to avoid wasted compute. -pub async fn check_balance( - db: &AppDatabase, - project_uid: Uuid, - user_uid: Uuid, - model_id: Uuid, - estimated_input_tokens: i64, - estimated_output_tokens: i64, -) -> Result { - let estimated_cost = compute_cost( - db, - model_id, - estimated_input_tokens, - estimated_output_tokens, - ) - .await?; - let project_balance = get_project_balance(db, project_uid).await; - let user_balance = get_user_balance(db, user_uid).await; - - Ok(project_balance + user_balance >= estimated_cost) -} - -/// Check whether a user's personal balance can cover a potential AI call. -pub async fn check_user_balance( - db: &AppDatabase, - user_uid: Uuid, - model_id: Uuid, - estimated_input_tokens: i64, - estimated_output_tokens: i64, -) -> Result { - let estimated_cost = compute_cost( - db, - model_id, - estimated_input_tokens, - estimated_output_tokens, - ) - .await?; - let user_balance = get_user_balance(db, user_uid).await; - - Ok(user_balance >= estimated_cost) -} - -// ── Initialization ── - -/// Initialize a user billing account with the default $10 balance. -/// Called on user signup / first login. -pub async fn initialize_user_billing(db: &AppDatabase, user_uid: Uuid) -> Result<(), AgentError> { - let now = chrono::Utc::now(); - user_billing::ActiveModel { - user: Set(user_uid), - balance: Set(default_user_balance()), - currency: Set("USD".to_string()), - is_pro: Set(false), - monthly_quota: Set(Decimal::ZERO), - month_used: Set(Decimal::ZERO), - cycle_start: Set(None), - cycle_end: Set(None), - updated_at: Set(now), - created_at: Set(now), - } - .insert(db) - .await - .map_err(|e| AgentError::Internal(format!("failed to create user billing: {}", e)))?; - - tracing::info!(user_id = %user_uid, balance = "$10", "user_billing_initialized"); - Ok(()) -} - -/// Initialize a project billing account. -/// Grants $20 only if this is the creator's first project; $0 otherwise. -pub async fn initialize_project_billing( - db: &AppDatabase, - project_uid: Uuid, - creator_uid: Uuid, -) -> Result<(), AgentError> { - // Check how many projects this user has already created - let existing_count = project::Entity::find() - .filter(project::Column::CreatedBy.eq(creator_uid)) - .filter(project::Column::Id.ne(project_uid)) - .count(db) - .await - .map_err(|e| AgentError::Internal(format!("failed to count user projects: {}", e)))?; - - let is_first = existing_count == 0; - let initial_balance = if is_first { - first_project_credit() - } else { - SUBSEQUENT_PROJECT_BALANCE - }; - let now = chrono::Utc::now(); - - project_billing::ActiveModel { - project: Set(project_uid), - balance: Set(initial_balance), - currency: Set("USD".to_string()), - user: Set(Some(creator_uid)), - initial_credit_granted: Set(is_first), - is_pro: Set(false), - monthly_quota: Set(Decimal::ZERO), - month_used: Set(Decimal::ZERO), - cycle_start: Set(None), - cycle_end: Set(None), - updated_at: Set(now), - created_at: Set(now), - } - .insert(db) - .await - .map_err(|e| AgentError::Internal(format!("failed to create project billing: {}", e)))?; - - if is_first { - // Record the credit in billing history - project_billing_history::ActiveModel { - uid: Set(Uuid::new_v4()), - project: Set(project_uid), - user: Set(Some(creator_uid)), - amount: Set(first_project_credit()), - currency: Set("USD".to_string()), - reason: Set("first_project_credit".to_string()), - extra: Set(Some(serde_json::json!({ - "is_first_project": true, - }))), - created_at: Set(now), - ..Default::default() - } - .insert(db) - .await - .map_err(|e| AgentError::Internal(format!("failed to record credit history: {}", e)))?; - } - - tracing::info!( - project_id = %project_uid, - creator_id = %creator_uid, - is_first_project = is_first, - balance = if is_first { "$20" } else { "$0" }, - "project_billing_initialized" - ); - Ok(()) -} - -// ── Internal helpers ── - -async fn compute_cost( - db: &AppDatabase, - model_id: Uuid, - input_tokens: i64, - output_tokens: i64, -) -> Result { - let pricing = model_pricing::Entity::find() - .filter(model_pricing::Column::ModelVersionId.eq(model_id)) - .order_by_desc(model_pricing::Column::EffectiveFrom) - .one(db) - .await? - .ok_or_else(|| { - AgentError::Internal( - "No pricing record found for this model. Please configure AI model pricing first." - .into(), - ) - })?; - - let input_price: Decimal = pricing - .input_price_per_1k_tokens - .parse() - .map_err(|e| AgentError::Internal(format!("Invalid input price: {}", e)))?; - let output_price: Decimal = pricing - .output_price_per_1k_tokens - .parse() - .map_err(|e| AgentError::Internal(format!("Invalid output price: {}", e)))?; - - if input_price <= Decimal::ZERO && output_price <= Decimal::ZERO { - return Err(AgentError::Internal( - "Model pricing is not configured or is zero. Please configure non-zero AI model pricing first." - .into(), - )); - } - - // DB stores per-1M-token prices; divide tokens by 1M to compute cost. - let million = Decimal::from(1_000_000); - Ok((Decimal::from(input_tokens) / million) * input_price - + (Decimal::from(output_tokens) / million) * output_price) -} - -async fn get_currency(db: &AppDatabase, model_id: Uuid) -> Result { - let pricing = model_pricing::Entity::find() - .filter(model_pricing::Column::ModelVersionId.eq(model_id)) - .one(db) - .await? - .ok_or_else(|| AgentError::Internal("No pricing found".into()))?; - Ok(pricing.currency.clone()) -} - -async fn get_project_balance(db: &AppDatabase, project_uid: Uuid) -> Decimal { - project_billing::Entity::find_by_id(project_uid) - .one(db) - .await - .ok() - .flatten() - .map(|b| b.balance) - .unwrap_or(Decimal::ZERO) -} - -async fn get_user_balance(db: &AppDatabase, user_uid: Uuid) -> Decimal { - user_billing::Entity::find_by_id(user_uid) - .one(db) - .await - .ok() - .flatten() - .map(|b| b.balance) - .unwrap_or(Decimal::ZERO) -} - -async fn deduct_from_project( - db: &AppDatabase, - project_uid: Uuid, - cost: Decimal, - currency: &str, - model_id: Uuid, - input_tokens: i64, - output_tokens: i64, -) -> Result<(), String> { - let txn = db - .begin() - .await - .map_err(|e| format!("db txn error: {}", e))?; - - let billing = project_billing::Entity::find_by_id(project_uid) - .lock_exclusive() - .one(&txn) - .await - .map_err(|e| format!("db error: {}", e))? - .ok_or_else(|| "Project billing account not found".to_string())?; - - if billing.balance < cost { - txn.rollback().await.ok(); - return Err(format!( - "Project balance insufficient. Required: {:.4} {}, Available: {:.4} {}", - cost, currency, billing.balance, currency - )); - } - - let now = chrono::Utc::now(); - - project_billing_history::ActiveModel { - uid: Set(Uuid::new_v4()), - project: Set(project_uid), - user: Set(None), - amount: Set(-cost), - currency: Set(currency.to_string()), - reason: Set("ai_usage".to_string()), - extra: Set(Some(serde_json::json!({ - "model_id": model_id.to_string(), - "input_tokens": input_tokens, - "output_tokens": output_tokens, - "deducted_from": "project", - }))), - created_at: Set(now), - ..Default::default() - } - .insert(&txn) - .await - .map_err(|e| format!("failed to insert history: {}", e))?; - - let mut updated: project_billing::ActiveModel = billing.into(); - updated.balance = Set(updated.balance.unwrap() - cost); - updated.updated_at = Set(now); - updated - .update(&txn) - .await - .map_err(|e| format!("failed to update balance: {}", e))?; - - txn.commit() - .await - .map_err(|e| format!("commit error: {}", e))?; - Ok(()) -} - -async fn deduct_from_user( - db: &AppDatabase, - user_uid: Uuid, - cost: Decimal, - currency: &str, - project_uid: Uuid, - model_id: Uuid, - input_tokens: i64, - output_tokens: i64, -) -> Result<(), String> { - let txn = db - .begin() - .await - .map_err(|e| format!("db txn error: {}", e))?; - - let billing = user_billing::Entity::find_by_id(user_uid) - .lock_exclusive() - .one(&txn) - .await - .map_err(|e| format!("db error: {}", e))? - .ok_or_else(|| "User billing account not found".to_string())?; - - if billing.balance < cost { - txn.rollback().await.ok(); - return Err(format!( - "Insufficient balance (project + user). Project: unavailable, User: {:.4} {}. Required: {:.4} {}", - billing.balance, currency, cost, currency - )); - } - - let now = chrono::Utc::now(); - - // Record in project billing history (but deducted from user) - project_billing_history::ActiveModel { - uid: Set(Uuid::new_v4()), - project: Set(project_uid), - user: Set(Some(user_uid)), - amount: Set(-cost), - currency: Set(currency.to_string()), - reason: Set("ai_usage_user_fallback".to_string()), - extra: Set(Some(serde_json::json!({ - "model_id": model_id.to_string(), - "input_tokens": input_tokens, - "output_tokens": output_tokens, - "deducted_from": "user", - }))), - created_at: Set(now), - ..Default::default() - } - .insert(&txn) - .await - .map_err(|e| format!("failed to insert history: {}", e))?; - - let mut updated: user_billing::ActiveModel = billing.into(); - updated.balance = Set(updated.balance.unwrap() - cost); - updated.updated_at = Set(now); - updated - .update(&txn) - .await - .map_err(|e| format!("failed to update user balance: {}", e))?; - - txn.commit() - .await - .map_err(|e| format!("commit error: {}", e))?; - Ok(()) -} - -async fn deduct_from_user_personal( - db: &AppDatabase, - user_uid: Uuid, - cost: Decimal, - currency: &str, - model_id: Uuid, - input_tokens: i64, - output_tokens: i64, -) -> Result<(), String> { - let txn = db - .begin() - .await - .map_err(|e| format!("db txn error: {}", e))?; - - let billing = user_billing::Entity::find_by_id(user_uid) - .lock_exclusive() - .one(&txn) - .await - .map_err(|e| format!("db error: {}", e))? - .ok_or_else(|| "User billing account not found".to_string())?; - - if billing.balance < cost { - txn.rollback().await.ok(); - return Err(format!( - "Insufficient balance. User: {:.4} {}. Required: {:.4} {}", - billing.balance, billing.currency, cost, billing.currency - )); - } - - let now = chrono::Utc::now(); - - user_billing_history::ActiveModel { - uid: Set(Uuid::new_v4()), - user: Set(user_uid), - amount: Set(-cost), - currency: Set(currency.to_string()), - reason: Set("ai_usage_personal".to_string()), - extra: Set(Some(serde_json::json!({ - "model_id": model_id.to_string(), - "input_tokens": input_tokens, - "output_tokens": output_tokens, - "deducted_from": "user", - "scope": "personal", - }))), - created_at: Set(now), - ..Default::default() - } - .insert(&txn) - .await - .map_err(|e| format!("failed to insert user history: {}", e))?; - - let mut updated: user_billing::ActiveModel = billing.into(); - updated.balance = Set(updated.balance.unwrap() - cost); - updated.updated_at = Set(now); - updated - .update(&txn) - .await - .map_err(|e| format!("failed to update user balance: {}", e))?; - - txn.commit() - .await - .map_err(|e| format!("commit error: {}", e))?; - Ok(()) -} - -pub async fn persist_billing_error( - db: &AppDatabase, - scope: &str, - scope_id: Uuid, - error_type: &str, - message: &str, - details: Option, -) -> Result<(), AgentError> { - billing_error::ActiveModel { - id: Set(Uuid::new_v4()), - scope: Set(scope.to_string()), - scope_id: Set(scope_id), - error_type: Set(error_type.to_string()), - message: Set(message.to_string()), - details: Set(details), - resolved: Set(false), - created_at: Set(chrono::Utc::now()), - } - .insert(db) - .await - .map_err(|e| AgentError::Internal(format!("failed to persist billing error: {}", e)))?; - - tracing::warn!(scope, %scope_id, error_type, "billing_error_persisted"); - Ok(()) -} - -fn decimal_to_f64(d: Decimal) -> f64 { - d.round_dp(10).to_string().parse().unwrap_or(0.0) -} diff --git a/libs/agent/chat/chat_execution.rs b/libs/agent/chat/chat_execution.rs deleted file mode 100644 index 3cf6d61..0000000 --- a/libs/agent/chat/chat_execution.rs +++ /dev/null @@ -1,1256 +0,0 @@ -use std::pin::Pin; -use std::sync::Arc; -use uuid::Uuid; - -use super::agent_profile::profile_for_role_name; -use crate::client::AiClientConfig; -use crate::client::types::{ChatRequestMessage, ToolCall}; -use crate::client::{StreamChunk, StreamChunkType, StreamedToolCall, call_stream}; -use crate::embed::EmbedService; -use crate::error::Result; -use crate::tool::registry::ToolRegistry; -use crate::tool::{ - ToolCall as AgentToolCall, ToolContext, ToolDefinition, ToolExecutor, ToolHandler, ToolParam, -}; -use sea_orm::{ActiveModelTrait, EntityTrait, Set}; - -use super::service::StreamResult; -use super::{AiChunkType, AiStreamChunk, StreamCallback}; - -struct SubAgentRunResult { - output: String, - input_tokens: i64, - output_tokens: i64, - cancelled: bool, - error: Option, -} - -/// Persist a sub-agent session record to the database. -async fn persist_sub_agent_session( - db: &db::database::AppDatabase, - conversation_id: Uuid, - children_id: &str, - role: &str, - task: &str, - output: &str, - input_tokens: i64, - output_tokens: i64, - model_name: &str, - status: &str, - error_message: Option, -) { - use models::ai::ai_subagent_session; - use sea_orm::{ActiveModelTrait, Set}; - - let record = ai_subagent_session::ActiveModel { - id: Set(Uuid::now_v7()), - conversation_id: Set(conversation_id), - message_id: Set(Uuid::nil()), - children_id: Set(children_id.to_string()), - role: Set(role.to_string()), - task: Set(task.to_string()), - output: Set(output.to_string()), - input_tokens: Set(input_tokens), - output_tokens: Set(output_tokens), - model_name: Set(Some(model_name.to_string())), - status: Set(status.to_string()), - error_message: Set(error_message), - created_at: Set(chrono::Utc::now()), - }; - - if let Err(e) = record.insert(db.writer()).await { - tracing::warn!(error = %e, children_id = %children_id, "failed to persist sub-agent session"); - } -} - -/// Execute a sub-agent call with streaming output via NATS. -/// -/// The sub-agent output is streamed to NATS JetStream subject -/// `chat.subagent.chunk.{conversation_id}.{children_id}` so the frontend -/// can subscribe via the `/api/ai/subagent/{conversation_id}/{children_id}/stream` endpoint. -/// -/// Returns the full or partial output after the sub-agent completes or is cancelled. -async fn call_sub_agent_stream( - messages: &[ChatRequestMessage], - model_name: &str, - config: &AiClientConfig, - temperature: f32, - max_tokens: u32, - max_tool_depth: usize, - tools: Option<&[serde_json::Value]>, - tool_registry: Option, - db: db::database::AppDatabase, - app_config: config::AppConfig, - project_id: Uuid, - sender_uid: Uuid, - embed_service: Option, - children_id: &str, - conversation_id: Option, - cache: db::cache::AppCache, - queue_producer: Option<&queue::MessageProducer>, -) -> Result { - use std::sync::atomic::{AtomicU64, Ordering}; - use tokio::sync::mpsc; - - let conversation_id = conversation_id.unwrap_or_default(); - let seq = Arc::new(AtomicU64::new(0)); - let children_id_owned = children_id.to_string(); - let queue_ref = queue_producer.cloned(); - let partial_output = Arc::new(tokio::sync::Mutex::new(String::new())); - let (delta_tx, mut delta_rx) = mpsc::unbounded_channel::<(&'static str, String)>(); - - cache - .clear_sub_agent_cancelled(conversation_id, &children_id_owned) - .await; - - let stream_fut = async { - let mut messages = messages.to_vec(); - let mut total_input_tokens = 0i64; - let mut total_output_tokens = 0i64; - let mut last_content = String::new(); - let mut tool_depth = 0usize; - - loop { - let response = call_stream( - &messages, - model_name, - config, - temperature, - max_tokens, - tools, - None, - Arc::new({ - let partial_output = partial_output.clone(); - let delta_tx = delta_tx.clone(); - move |delta| { - let content = delta.to_string(); - let partial_output = partial_output.clone(); - let delta_tx = delta_tx.clone(); - Box::pin(async move { - partial_output.lock().await.push_str(&content); - let _ = delta_tx.send(("token", content)); - }) - as Pin + Send>> - } - }), - Arc::new({ - let delta_tx = delta_tx.clone(); - move |delta| { - let content = delta.to_string(); - let delta_tx = delta_tx.clone(); - Box::pin(async move { - let _ = delta_tx.send(("thinking", content)); - }) - as Pin + Send>> - } - }), - Arc::new(move |_tc: &StreamedToolCall| { - Box::pin(async move {}) as Pin + Send>> - }), - ) - .await?; - - total_input_tokens += response.input_tokens; - total_output_tokens += response.output_tokens; - if !response.content.is_empty() { - last_content = response.content.clone(); - } - - if response.tool_calls.is_empty() { - return Ok::(SubAgentRunResult { - output: response.content, - input_tokens: total_input_tokens, - output_tokens: total_output_tokens, - cancelled: false, - error: None, - }); - } - - if tool_depth >= max_tool_depth { - let fallback_output = if last_content.is_empty() { - "Sub-agent reached maximum tool depth before producing a final summary." - .to_string() - } else { - last_content - }; - return Ok::(SubAgentRunResult { - output: fallback_output, - input_tokens: total_input_tokens, - output_tokens: total_output_tokens, - cancelled: false, - error: Some(format!( - "sub-agent reached maximum tool depth ({max_tool_depth}) before final summary" - )), - }); - } - - let assistant_tool_calls: Vec = response - .tool_calls - .iter() - .map(|tc| ToolCall { - id: tc.id.clone(), - type_: "function".into(), - function: crate::client::types::ToolCallFunction { - name: tc.name.clone(), - arguments: tc.arguments.clone(), - }, - }) - .collect(); - messages.push(ChatRequestMessage::assistant( - Some(response.content.clone()), - Some(assistant_tool_calls), - )); - - let agent_tool_calls: Vec = response - .tool_calls - .iter() - .map(|tc| AgentToolCall { - id: tc.id.clone(), - name: tc.name.clone(), - arguments: tc.arguments.clone(), - }) - .collect(); - - let tool_messages = execute_sub_agent_tools( - &agent_tool_calls, - &db, - &cache, - &app_config, - project_id, - sender_uid, - tool_registry.as_ref(), - embed_service.as_ref(), - ) - .await; - messages.extend(tool_messages); - messages.push(ChatRequestMessage::user( - "Use the tool results above to produce your final concise findings. Do not call another tool unless it is strictly necessary.", - )); - tool_depth += 1; - } - }; - - let children_id_for_cancel = children_id_owned.clone(); - let cancel_fut = async { - let mut interval = tokio::time::interval(std::time::Duration::from_millis(100)); - loop { - interval.tick().await; - if cache - .is_sub_agent_cancelled(conversation_id, &children_id_for_cancel) - .await - { - break; - } - } - }; - let timeout_fut = tokio::time::sleep(std::time::Duration::from_secs(60)); - - let flush_queue = queue_ref.clone(); - let flush_children_id = children_id_owned.clone(); - let flush_seq = seq.clone(); - let mut flush_handle = tokio::spawn(async move { - let Some(queue) = flush_queue else { - while delta_rx.recv().await.is_some() {} - return; - }; - let mut token_buf = String::new(); - let mut thinking_buf = String::new(); - let mut interval = tokio::time::interval(std::time::Duration::from_millis(50)); - - async fn flush( - queue: &queue::MessageProducer, - conversation_id: Uuid, - children_id: &str, - seq: &Arc, - chunk_type: &str, - buffer: &mut String, - ) { - if buffer.is_empty() { - return; - } - let event = queue::types::SubAgentStreamChunkEvent { - conversation_id, - children_id: children_id.to_string(), - seq: seq.fetch_add(1, Ordering::Relaxed), - content: std::mem::take(buffer), - done: false, - error: None, - chunk_type: Some(chunk_type.to_string()), - role: String::new(), - task: String::new(), - }; - queue.publish_sub_agent_chunk_realtime(&event).await; - } - - loop { - tokio::select! { - Some((kind, content)) = delta_rx.recv() => { - let target = if kind == "thinking" { &mut thinking_buf } else { &mut token_buf }; - target.push_str(&content); - if target.len() >= 240 { - flush(&queue, conversation_id, &flush_children_id, &flush_seq, kind, target).await; - } - } - _ = interval.tick() => { - flush(&queue, conversation_id, &flush_children_id, &flush_seq, "thinking", &mut thinking_buf).await; - flush(&queue, conversation_id, &flush_children_id, &flush_seq, "token", &mut token_buf).await; - } - else => break, - } - } - flush( - &queue, - conversation_id, - &flush_children_id, - &flush_seq, - "thinking", - &mut thinking_buf, - ) - .await; - flush( - &queue, - conversation_id, - &flush_children_id, - &flush_seq, - "token", - &mut token_buf, - ) - .await; - }); - - let response = tokio::select! { - result = stream_fut => { - match result { - Ok(response) => Some(response), - Err(e) => Some(SubAgentRunResult { - output: partial_output.lock().await.clone(), - input_tokens: 0, - output_tokens: 0, - cancelled: false, - error: Some(e.to_string()), - }), - } - } - _ = cancel_fut => None, - _ = timeout_fut => Some(SubAgentRunResult { - output: partial_output.lock().await.clone(), - input_tokens: 0, - output_tokens: 0, - cancelled: false, - error: Some("sub-agent timed out after 60 seconds".to_string()), - }), - }; - drop(delta_tx); - if tokio::time::timeout(std::time::Duration::from_secs(2), &mut flush_handle) - .await - .is_err() - { - flush_handle.abort(); - tracing::warn!( - children_id = %children_id, - "sub-agent stream flush timed out; continuing with terminal event" - ); - } - - let cancelled = response.is_none(); - let (total_content, total_input_tokens, total_output_tokens, terminal_error) = match response { - Some(response) => ( - response.output.clone(), - response.input_tokens, - response.output_tokens, - response.error.clone(), - ), - None => (partial_output.lock().await.clone(), 0, 0, None), - }; - - // Send final done/stopped chunk. - let final_seq = seq.load(Ordering::Relaxed); - let event = queue::types::SubAgentStreamChunkEvent { - conversation_id, - children_id: children_id_owned, - seq: final_seq, - content: String::new(), - done: true, - error: terminal_error.clone(), - chunk_type: Some( - if terminal_error.is_some() { - "error" - } else if cancelled { - "stopped" - } else { - "done" - } - .to_string(), - ), - role: String::new(), - task: String::new(), - }; - if let Some(q) = queue_ref { - if tokio::time::timeout( - std::time::Duration::from_secs(1), - q.publish_sub_agent_chunk(&event), - ) - .await - .is_err() - { - tracing::warn!( - children_id = %event.children_id, - "sub-agent terminal event publish timed out" - ); - } - } - - Ok(SubAgentRunResult { - output: total_content, - input_tokens: total_input_tokens, - output_tokens: total_output_tokens, - cancelled, - error: terminal_error, - }) -} - -// Keyword-extraction-based title generator: reads conversation messages, extracts -// meaningful words, and updates the conversation record with a short title. -async fn generate_title_for_conversation( - ctx: &ToolContext, - conversation_id: Uuid, -) -> Result { - use models::ai::{AiMessage, ai_conversation, ai_message}; - use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; - - let db_reader = ctx.db().reader(); - let db_writer = ctx.db().writer(); - let conv = ai_conversation::Entity::find_by_id(conversation_id) - .one(db_reader) - .await - .map_err(|e| crate::error::AgentError::ToolExecutionFailed { - tool: "generate_title".into(), - cause: format!("db error: {}", e), - })? - .ok_or_else(|| crate::error::AgentError::NotFound("Conversation not found".into()))?; - - let recent_messages = AiMessage::find() - .filter(ai_message::Column::ConversationId.eq(conversation_id)) - .filter(ai_message::Column::Role.eq("user")) - .order_by_desc(ai_message::Column::CreatedAt) - .limit(3) - .all(db_reader) - .await - .map_err(|e| crate::error::AgentError::ToolExecutionFailed { - tool: "generate_title".into(), - cause: format!("db error: {}", e), - })?; - - if recent_messages.is_empty() { - return Err(crate::error::AgentError::ToolExecutionFailed { - tool: "generate_title".into(), - cause: "No user messages found".into(), - }); - } - - let content = recent_messages - .first() - .and_then(|m| m.content.as_array()) - .and_then(|arr| arr.first()) - .and_then(|v| v.get("content")) - .and_then(|c| c.as_str()) - .unwrap_or(""); - - let words: Vec<&str> = content - .split_whitespace() - .filter(|w| w.len() > 2 && !is_stop_word(w)) - .take(5) - .collect(); - - let title = if words.is_empty() { - "New Chat".to_string() - } else { - words.join(" ") - }; - - let mut active: ai_conversation::ActiveModel = conv.into(); - active.title = Set(Some(title.clone())); - active.updated_at = Set(chrono::Utc::now()); - active - .update(db_writer) - .await - .map_err(|e| crate::error::AgentError::ToolExecutionFailed { - tool: "generate_title".into(), - cause: format!("failed to update title: {}", e), - })?; - - Ok(serde_json::json!({ "conversation_id": conversation_id.to_string(), "title": title })) -} - -fn is_stop_word(w: &str) -> bool { - matches!( - w.to_lowercase().as_str(), - "the" - | "this" - | "that" - | "what" - | "which" - | "when" - | "where" - | "why" - | "how" - | "can" - | "could" - | "would" - | "should" - | "please" - | "help" - | "thanks" - | "thank" - | "you" - | "your" - | "have" - | "has" - | "had" - | "with" - | "for" - | "from" - | "into" - | "about" - | "also" - | "just" - | "now" - | "very" - | "really" - ) -} - -type SharedCallback = Arc< - dyn Fn(AiStreamChunk) -> Pin + Send>> + Send + Sync, ->; - -/// Simplified ReAct execution for Chat API. -/// -/// Unlike `execute_process_stream` (which requires `AiRequest` with room-specific data), -/// this function takes messages and tools directly. It does NOT record AI sessions to -/// the `ai_session` table -the caller is responsible for persisting results. -pub async fn execute_chat_stream( - messages: Vec, - tools: Vec, - model_name: &str, - config: &AiClientConfig, - temperature: f32, - max_tokens: u32, - max_tool_depth: usize, - tool_registry: Option<&ToolRegistry>, - db: db::database::AppDatabase, - cache: db::cache::AppCache, - app_config: config::AppConfig, - project_id: Uuid, - sender_uid: Uuid, - embed_service: Option, - on_chunk: StreamCallback, - conversation_id: Option, - queue_producer: Option, -) -> Result { - let on_chunk: SharedCallback = Arc::from(on_chunk); - let tools_enabled = !tools.is_empty(); - let mut messages = messages; - let mut tool_depth = 0; - let mut total_input_tokens = 0i64; - let mut total_output_tokens = 0i64; - let mut full_content = String::new(); - let mut all_chunks: Vec = Vec::new(); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); - - // Conditionally inject chat_generate_title tool if conversation has no title - let (tools, _tools_injected) = if let Some(conv_id) = conversation_id { - if let Some(registry) = tool_registry { - let db_reader = db.reader(); - let has_title = models::ai::ai_conversation::Entity::find_by_id(conv_id) - .one(db_reader) - .await - .map(|c| c.map(|m| m.title.is_some()).unwrap_or(false)) - .unwrap_or(false); - if !has_title { - let mut reg = registry.clone(); - reg.register( - ToolDefinition::new("chat_generate_title") - .description( - "Generate a concise title (5 words or fewer) for the current conversation \ - based on its message history, and save it to the conversation record. \ - Call this tool at the start of a new conversation if it has no title.", - ) - .parameters(crate::tool::ToolSchema { - schema_type: "object".into(), - properties: Some({ - let mut p = std::collections::HashMap::new(); - p.insert("conversation_id".into(), ToolParam { - name: "conversation_id".into(), - param_type: "string".into(), - description: Some("The UUID of the conversation (required).".into()), - required: true, - properties: None, - items: None, - }); - p - }), - required: Some(vec!["conversation_id".into()]), - }), - ToolHandler::new(|ctx, args| { - let conv_id = args.get("conversation_id") - .and_then(|v| v.as_str()) - .and_then(|s| Uuid::parse_str(s).ok()); - Box::pin(async move { - match conv_id { - Some(id) => generate_title_for_conversation(&ctx, id).await - .map_err(|e| crate::tool::ToolError::ExecutionError(e.to_string())), - None => Err(crate::tool::ToolError::ExecutionError("conversation_id missing".into())), - } - }) - }), - ); - // Prepend system message instructing the model to generate title first - messages.insert(0, ChatRequestMessage::system( - "IMPORTANT: If the conversation has no title, you MUST call chat_generate_title \ - with the conversation_id immediately before answering any user question. \ - The title must be 5 words or fewer and should summarize the user's intent.".to_string(), - )); - (reg.to_openai_tools(), true) - } else { - (tools.clone(), false) - } - } else { - (tools.clone(), false) - } - } else { - (tools.clone(), false) - }; - - // Add call_sub_agent tool for chat orchestration when tools are available - let tools = if tools_enabled { - let mut t = tools; - t.push(serde_json::json!({ - "type": "function", - "function": { - "name": "call_sub_agent", - "description": "Delegate a task to a specialist sub-agent and receive its output.\nAvailable roles:\n- researcher: Gathers facts, evidence, and data. Best for finding information and searching code.\n- analyst: Builds explanations, highlights causal links and tradeoffs. Best for reasoning about implications.\n- reviewer: Stress-tests proposals, identifies risks and contradictions. Best for quality checks.\n- architect: Maps systems, dependencies, boundaries, and design tradeoffs. Best for architecture decisions.\n- debugger: Finds root causes, suspect changes, and validation paths. Best for bugs and regressions.\n- implementer: Converts requirements into concrete implementation steps. Best for execution planning.\n- tester: Designs validation and regression coverage. Best for test strategy.\n- security: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for sensitive changes.\nProvide a clear, focused task description so the sub-agent knows exactly what to investigate.", - "parameters": { - "type": "object", - "properties": { - "role": { - "type": "string", - "description": "The sub-agent role to delegate to: researcher, analyst, reviewer, architect, debugger, implementer, tester, or security." - }, - "task": { - "type": "string", - "description": "The specific task or question for the sub-agent. Be precise and focused." - } - }, - "required": ["role", "task"] - } - } - })); - t - } else { - tools - }; - - loop { - let on_chunk_cb = on_chunk.clone(); - let on_chunk_cb2 = on_chunk.clone(); - let tx_arc = Arc::new(tx.clone()); - let tx_arc2 = tx_arc.clone(); - - let response = call_stream( - &messages, - model_name, - config, - temperature, - max_tokens, - if tools_enabled { Some(&tools) } else { None }, - None, - Arc::new(move |delta| { - let content = delta.to_string(); - let fut = on_chunk_cb(AiStreamChunk { - content, - done: false, - chunk_type: AiChunkType::Answer, - metadata: None, - children_id: None, - }); - fut - }), - Arc::new(move |delta| { - let fut = on_chunk_cb2(AiStreamChunk { - content: delta.to_string(), - done: false, - chunk_type: AiChunkType::Thinking, - metadata: None, - children_id: None, - }); - fut - }), - Arc::new(move |tc: &StreamedToolCall| { - let tx = tx_arc2.clone(); - let tc_owned = tc.clone(); - Box::pin(async move { - let _ = tx.send(tc_owned); - }) as Pin + Send>> - }), - ) - .await?; - - total_input_tokens += response.input_tokens; - total_output_tokens += response.output_tokens; - all_chunks.extend(response.chunks.clone()); - - let has_tool_calls = tools_enabled && !response.tool_calls.is_empty(); - if !has_tool_calls { - let final_content = response.content.clone(); - // Don't push full content as a chunk -incremental deltas in - // response.chunks (already added above) sum to the same text. - // merge_consecutive_blocks would concatenate delta_sum + full = - // 2x full, causing duplicate content in DB persistence. - return Ok(StreamResult { - content: final_content, - reasoning_content: response.reasoning_content, - input_tokens: total_input_tokens, - output_tokens: total_output_tokens, - chunks: all_chunks, - }); - } - - full_content.push_str(&response.content); - - let tool_calls: Vec = response - .tool_calls - .iter() - .map(|tc| ToolCall { - id: tc.id.clone(), - type_: "function".into(), - function: crate::client::types::ToolCallFunction { - name: tc.name.clone(), - arguments: tc.arguments.clone(), - }, - }) - .collect(); - - messages.push(ChatRequestMessage::assistant( - Some(response.content.clone()), - Some(tool_calls.clone()), - )); - - // Drain tool call notifications - loop { - match rx.try_recv() { - Ok(tc) => { - let args_display = if tc.arguments.len() > 100 { - let end = tc - .arguments - .char_indices() - .map(|(i, _)| i) - .take_while(|&i| i <= 100) - .last() - .unwrap_or(100); - format!("{}...", &tc.arguments[..end]) - } else { - tc.arguments.clone() - }; - let tool_display = format!("[tool] {}({})", tc.name, args_display); - on_chunk(AiStreamChunk { - content: tool_display.clone(), - done: false, - chunk_type: AiChunkType::ToolCall, - metadata: None, - children_id: None, - }) - .await; - all_chunks.push(StreamChunk { - chunk_type: StreamChunkType::ToolCall, - content: tool_display, - }); - } - Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break, - Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break, - } - } - - let (sub_agent_calls, regular_calls): (Vec, Vec) = response - .tool_calls - .iter() - .map(|tc| AgentToolCall { - id: tc.id.clone(), - name: tc.name.clone(), - arguments: tc.arguments.clone(), - }) - .collect::>() - .into_iter() - .partition(|c| c.name == "call_sub_agent"); - - let mut tool_messages = Vec::new(); - let mut sub_agent_tasks = tokio::task::JoinSet::new(); - let mut sub_agent_ids: Vec = Vec::new(); - - // Handle call_sub_agent calls inline -stream sub-agent output via NATS - for sub_call in sub_agent_calls { - let args: serde_json::Value = match serde_json::from_str(&sub_call.arguments) { - Ok(v) => v, - Err(_) => { - tool_messages.push(ChatRequestMessage::tool( - &sub_call.id, - "Failed to parse call_sub_agent arguments", - )); - continue; - } - }; - let role = args - .get("role") - .and_then(|v| v.as_str()) - .unwrap_or("researcher"); - let task = args.get("task").and_then(|v| v.as_str()).unwrap_or(""); - - let profile = profile_for_role_name(role); - - // Generate children_id BEFORE starting sub-agent execution - let sub_agent_id = format!("sub-agent-{}", Uuid::now_v7()); - sub_agent_ids.push(sub_agent_id.clone()); - - // Emit tool_call chunk immediately with children_id so frontend can start watching - let call_display = - format!("[tool] call_sub_agent({role}) - delegating to {role} agent..."); - on_chunk(AiStreamChunk { - content: call_display.clone(), - done: false, - chunk_type: AiChunkType::ToolCall, - metadata: Some(serde_json::json!({ - "tool": "call_sub_agent", - "args": { "role": role.to_string(), "task": task.to_string() }, - "display": call_display, - })), - children_id: Some(sub_agent_id.clone()), - }) - .await; - all_chunks.push(StreamChunk { - chunk_type: StreamChunkType::ToolCall, - content: call_display, - }); - - let sub_system = profile.system_prompt.clone().unwrap_or_default(); - let sub_messages = vec![ - ChatRequestMessage::system(sub_system), - ChatRequestMessage::user(format!( - "Sub-agent role: {role}\n\nTask:\n{task}\n\nFocus only on your assigned task. Return concise, evidence-backed findings." - )), - ]; - - // Filter tools for the sub-agent: only include tools in the profile's allowed list, - // always excluding call_sub_agent and chat_generate_title - let sub_tools: Vec = if let Some(ref allowed) = profile.allowed_tools - { - tools - .iter() - .filter(|t| { - let name = t - .get("function") - .and_then(|f| f.get("name")) - .and_then(|n| n.as_str()) - .unwrap_or(""); - allowed.contains(&name.to_string()) - && name != "call_sub_agent" - && name != "chat_generate_title" - }) - .cloned() - .collect() - } else { - tools - .iter() - .filter(|t| { - let name = t - .get("function") - .and_then(|f| f.get("name")) - .and_then(|n| n.as_str()) - .unwrap_or(""); - name != "call_sub_agent" && name != "chat_generate_title" - }) - .cloned() - .collect() - }; - - let call_id = sub_call.id.clone(); - let role_owned = role.to_string(); - let task_owned = task.to_string(); - let sub_agent_id_owned = sub_agent_id.clone(); - let model_name_owned = model_name.to_string(); - let config_owned = config.clone(); - let cache_owned = cache.clone(); - let db_owned = db.clone(); - let app_config_owned = app_config.clone(); - let embed_service_owned = embed_service.clone(); - let tool_registry_owned = tool_registry.cloned(); - let queue_owned = queue_producer.clone(); - let conversation_id_owned = conversation_id; - let temperature = profile.temperature.unwrap_or(0.7) as f32; - let max_tokens = profile.max_tokens.unwrap_or(4096) as u32; - let sub_max_tool_depth = profile.max_tool_depth.unwrap_or(4) as usize; - - sub_agent_tasks.spawn(async move { - let result = call_sub_agent_stream( - &sub_messages, - &model_name_owned, - &config_owned, - temperature, - max_tokens, - sub_max_tool_depth, - Some(&sub_tools), - tool_registry_owned, - db_owned, - app_config_owned, - project_id, - sender_uid, - embed_service_owned, - &sub_agent_id_owned, - conversation_id_owned, - cache_owned, - queue_owned.as_ref(), - ) - .await; - ( - call_id, - sub_agent_id_owned, - role_owned, - task_owned, - model_name_owned, - result, - ) - }); - } - - let mut cancelled_batch = false; - while let Some(joined) = sub_agent_tasks.join_next().await { - let Ok((call_id, sub_agent_id, role, task, sub_model_name, result)) = joined else { - continue; - }; - - match result { - Ok(result) => { - if result.cancelled && !cancelled_batch { - cancelled_batch = true; - if let Some(conv_id) = conversation_id { - for id in &sub_agent_ids { - if id != &sub_agent_id { - cache.set_sub_agent_cancelled(conv_id, id).await; - } - } - } - } - let status = if result.error.is_some() { - "error" - } else if result.cancelled { - "stopped" - } else { - "ok" - }; - let output = result.output.clone(); - persist_sub_agent_session( - &db, - conversation_id.unwrap_or_default(), - &sub_agent_id, - &role, - &task, - &output, - result.input_tokens, - result.output_tokens, - &sub_model_name, - status, - result.error.clone(), - ) - .await; - - let display = if result.error.is_some() { - format!("Sub-agent failed ({role})") - } else if result.cancelled { - format!("Sub-agent stopped ({role})") - } else { - format!("Sub-agent completed ({role})") - }; - on_chunk(AiStreamChunk { - content: display.clone(), - done: false, - chunk_type: AiChunkType::ToolResult, - metadata: Some(serde_json::json!({ - "tool": "call_sub_agent", - "role": role.clone(), - "task": task.clone(), - "output": output.clone(), - "input_tokens": result.input_tokens, - "output_tokens": result.output_tokens, - "error": result.error.clone(), - "status": status, - "display": display.clone(), - })), - children_id: Some(sub_agent_id.clone()), - }) - .await; - all_chunks.push(StreamChunk { - chunk_type: StreamChunkType::ToolResult, - content: serde_json::json!({ - "tool": "call_sub_agent", - "role": role.clone(), - "task": task.clone(), - "output": output.clone(), - "input_tokens": result.input_tokens, - "output_tokens": result.output_tokens, - "error": result.error.clone(), - "status": status, - "display": display.clone(), - "children_id": sub_agent_id.clone(), - }) - .to_string(), - }); - let tool_content = if let Some(err) = &result.error { - format!( - "{}\n\n[sub_agent_status={} input_tokens={} output_tokens={} error={}]", - output, status, result.input_tokens, result.output_tokens, err - ) - } else { - format!( - "{}\n\n[sub_agent_status={} input_tokens={} output_tokens={}]", - output, status, result.input_tokens, result.output_tokens - ) - }; - tool_messages.push(ChatRequestMessage::tool(&call_id, tool_content)); - } - Err(e) => { - let err_msg = format!("Sub-agent ({role}) failed: {}", e); - let display = format!("Sub-agent failed ({role})"); - let result_json = serde_json::json!({ - "tool": "call_sub_agent", - "role": role, - "status": "error", - "error": err_msg, - "display": display, - }) - .to_string(); - - on_chunk(AiStreamChunk { - content: display.clone(), - done: false, - chunk_type: AiChunkType::ToolResult, - metadata: None, - children_id: Some(sub_agent_id), - }) - .await; - all_chunks.push(StreamChunk { - chunk_type: StreamChunkType::ToolResult, - content: result_json, - }); - tool_messages.push(ChatRequestMessage::tool(&call_id, &err_msg)); - } - } - } - - // Handle regular tool calls via ToolExecutor - if !regular_calls.is_empty() { - let regular_tool_messages = execute_tools( - ®ular_calls, - &db, - &cache, - &app_config, - project_id, - sender_uid, - tool_registry, - embed_service.as_ref(), - &on_chunk, - &mut all_chunks, - ) - .await; - tool_messages.extend(regular_tool_messages); - } - - messages.extend(tool_messages); - - tool_depth += 1; - if tool_depth >= max_tool_depth { - let max_depth_text = format!( - "[AI reached maximum tool depth ({}) - no final answer produced]", - max_tool_depth - ); - on_chunk(AiStreamChunk { - content: max_depth_text.clone(), - done: true, - chunk_type: AiChunkType::Answer, - metadata: None, - children_id: None, - }) - .await; - all_chunks.push(StreamChunk { - chunk_type: StreamChunkType::Answer, - content: max_depth_text, - }); - return Ok(StreamResult { - content: full_content, - reasoning_content: String::new(), - input_tokens: 0, - output_tokens: 0, - chunks: all_chunks, - }); - } - } -} - -async fn execute_sub_agent_tools( - calls: &[AgentToolCall], - db: &db::database::AppDatabase, - cache: &db::cache::AppCache, - app_config: &config::AppConfig, - project_id: Uuid, - sender_uid: Uuid, - tool_registry: Option<&ToolRegistry>, - embed_service: Option<&EmbedService>, -) -> Vec { - let mut tool_messages = Vec::new(); - let mut ctx = ToolContext::new( - db.clone(), - cache.clone(), - app_config.clone(), - Uuid::nil(), - Some(sender_uid), - ) - .with_project(project_id); - if let Some(es) = embed_service { - ctx = ctx.with_embed_service(es.clone()); - } - if let Some(registry) = tool_registry { - ctx.registry_mut().merge(registry.clone()); - } - - let mut join_set = tokio::task::JoinSet::new(); - for call in calls { - let call_clone = call.clone(); - let mut ctx_clone = ctx.clone(); - join_set.spawn(async move { - let executor = ToolExecutor::new(); - let tool_name = call_clone.name.clone(); - let res = match tokio::time::timeout( - std::time::Duration::from_secs(45), - executor.execute_batch(vec![call_clone.clone()], &mut ctx_clone), - ) - .await - { - Ok(res) => res, - Err(_) => Err(crate::tool::ToolError::ExecutionError(format!( - "tool '{}' timed out after 45 seconds", - tool_name - ))), - }; - (call_clone, res) - }); - } - - while let Some(res) = join_set.join_next().await { - let Ok((call, results)) = res else { - continue; - }; - match results { - Ok(results) => tool_messages.extend(ToolExecutor::to_tool_messages(&results)), - Err(e) => { - let err_text = format!("[Sub-agent tool call failed: {}]", e); - tool_messages.push(ChatRequestMessage::tool(&call.id, &err_text)); - } - } - } - - tool_messages -} - -async fn execute_tools( - calls: &[AgentToolCall], - db: &db::database::AppDatabase, - cache: &db::cache::AppCache, - app_config: &config::AppConfig, - project_id: Uuid, - sender_uid: Uuid, - tool_registry: Option<&ToolRegistry>, - embed_service: Option<&EmbedService>, - on_chunk: &SharedCallback, - all_chunks: &mut Vec, -) -> Vec { - let mut tool_messages = Vec::new(); - let mut ctx = ToolContext::new( - db.clone(), - cache.clone(), - app_config.clone(), - Uuid::nil(), - Some(sender_uid), - ) - .with_project(project_id); - if let Some(es) = embed_service { - ctx = ctx.with_embed_service(es.clone()); - } - if let Some(registry) = tool_registry { - ctx.registry_mut().merge(registry.clone()); - } - - let mut join_set = tokio::task::JoinSet::new(); - for call in calls { - let call_clone = call.clone(); - let mut ctx_clone = ctx.clone(); - join_set.spawn(async move { - let executor = ToolExecutor::new(); - let tool_name = call_clone.name.clone(); - let res = match tokio::time::timeout( - std::time::Duration::from_secs(45), - executor.execute_batch(vec![call_clone.clone()], &mut ctx_clone), - ) - .await - { - Ok(res) => res, - Err(_) => Err(crate::tool::ToolError::ExecutionError(format!( - "tool '{}' timed out after 45 seconds", - tool_name - ))), - }; - (call_clone, res) - }); - } - - let heartbeat_dur = std::time::Duration::from_secs(10); - while !join_set.is_empty() { - tokio::select! { - Some(res) = join_set.join_next() => { - if let Ok((call, results)) = res { - match results { - Ok(results) => { - for result in &results { - let preview = match &result.result { - crate::tool::ToolResult::Ok(v) => { - let t = v.to_string(); - if t.len() > 300 { - let end = t.char_indices().map(|(i, _)| i).take_while(|&i| i <= 300).last().unwrap_or(300); - format!("{}...", &t[..end]) - } else { t.clone() } - } - crate::tool::ToolResult::Error(msg) => msg.clone(), - }; - tracing::debug!("tool_result: {} -{}", call.name, preview); - } - let success_display = format!("OK {}", call.name); - on_chunk(AiStreamChunk { content: success_display.clone(), done: false, chunk_type: AiChunkType::ToolResult, metadata: None, children_id: None }).await; - all_chunks.push(StreamChunk { chunk_type: StreamChunkType::ToolResult, content: success_display }); - let msgs = ToolExecutor::to_tool_messages(&results); - tool_messages.extend(msgs); - } - Err(e) => { - tracing::warn!(tool = %call.name, args = %call.arguments, error = %e, "tool_call_failed"); - let err_text = format!("[Tool call failed: {}]", e); - let err_display = format!("ERR {} (failed)", call.name); - on_chunk(AiStreamChunk { content: err_display.clone(), done: false, chunk_type: AiChunkType::ToolResult, metadata: None, children_id: None }).await; - all_chunks.push(StreamChunk { chunk_type: StreamChunkType::ToolResult, content: err_display }); - tool_messages.push(ChatRequestMessage::tool(&call.id, &err_text)); - } - } - } - }, - _ = tokio::time::sleep(heartbeat_dur) => { - on_chunk(AiStreamChunk { content: String::new(), done: false, chunk_type: AiChunkType::ToolCall, metadata: None, children_id: None }).await; - } - } - } - tool_messages -} diff --git a/libs/agent/chat/mod.rs b/libs/agent/chat/mod.rs deleted file mode 100644 index ae08abd..0000000 --- a/libs/agent/chat/mod.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::pin::Pin; - -use config::AppConfig; -use db::cache::AppCache; -use db::database::AppDatabase; -use models::agents::model; -use models::projects::{project, project_context_setting}; -use models::repos::repo; -use models::rooms::{room, room_message}; -use models::users::user; -use std::collections::HashMap; -use uuid::Uuid; - -/// Maximum recursion rounds for tool-call loops (AI → tool → result → AI). -/// Previous default of 3 caused frequent silent termination on realistic multi-step queries. -pub const DEFAULT_MAX_TOOL_DEPTH: usize = 99; - -/// A single chunk from an AI streaming response. -#[derive(Debug, Clone)] -pub struct AiStreamChunk { - pub content: String, - pub done: bool, - /// What kind of content this chunk contains — helps the frontend render - /// thinking, tool calls, and results with different styles. - pub chunk_type: AiChunkType, - /// Structured metadata for tool_call / tool_result events. - /// tool_call: {"tool": "...", "args": {...}} - /// tool_result: {"tool": "...", "status": "ok|error", "result": "..."} - pub metadata: Option, - /// Optional ID of a child process/agent, sent to frontend via SSE. - pub children_id: Option, -} - -/// Type of streaming chunk, used by the frontend for rendering. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AiChunkType { - /// AI reasoning/thinking text before a tool call or answer. - Thinking, - /// Final answer text from the AI. - Answer, - /// A tool call is being executed (content = tool name + args summary). - ToolCall, - /// Tool execution result (content = result or error). - ToolResult, -} - -impl Default for AiChunkType { - fn default() -> Self { - Self::Answer - } -} - -const THINK_OPEN: &str = "\x3cthinking\x3e"; -const THINK_CLOSE: &str = "\x3c/response\x3e"; - -/// Strip XML-format thinking tags that some models (e.g. DeepSeek-R1) embed -/// in reasoning output. Also normalizes excessive consecutive newlines (3+ → 2). -pub fn normalize_thinking_content(content: &str) -> String { - let content = content - .replace(THINK_CLOSE, "") - .replace(THINK_OPEN, "") - .replace("\x3cthinking", "") - .replace("/response\x3e", ""); - let mut result = String::with_capacity(content.len()); - let mut newline_count = 0usize; - for ch in content.chars() { - if ch == '\n' { - newline_count += 1; - if newline_count <= 2 { - result.push(ch); - } - } else { - newline_count = 0; - result.push(ch); - } - } - result.trim().to_string() -} -pub type StreamCallback = Box< - dyn Fn(AiStreamChunk) -> Pin + Send>> + Send + Sync, ->; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AgentRole { - Default, - Supervisor, - Researcher, - Analyst, - Reviewer, - Architect, - Debugger, - Implementer, - Tester, - Security, -} - -#[derive(Debug, Clone, Default)] -pub struct AgentExecutionProfile { - pub role: AgentRole, - pub system_prompt: Option, - pub temperature: Option, - pub max_tokens: Option, - pub top_p: Option, - pub frequency_penalty: Option, - pub presence_penalty: Option, - pub max_tool_depth: Option, - pub allowed_tools: Option>, - pub disable_orchestration: bool, -} - -impl Default for AgentRole { - fn default() -> Self { - Self::Default - } -} - -#[derive(Clone)] -pub struct AiRequest { - pub db: AppDatabase, - pub cache: AppCache, - pub config: AppConfig, - pub model: model::Model, - pub project: project::Model, - pub context_setting: Option, - pub sender: user::Model, - pub room: room::Model, - pub input: String, - pub mention: Vec, - pub history: Vec, - pub history_cutoff_seq: Option, - pub user_names: HashMap, - pub temperature: f64, - pub max_tokens: i32, - pub top_p: f64, - pub frequency_penalty: f64, - pub presence_penalty: f64, - pub think: bool, - pub tools: Option>, - pub max_tool_depth: usize, - pub execution_profile: Option, - pub room_preamble: Option, -} - -#[derive(Clone)] -pub enum Mention { - User(user::Model), - Repo(repo::Model), -} - -pub mod agent_profile; -pub mod chat_execution; -pub mod context; -pub mod message_builder; -pub mod nonstreaming_execution; -pub mod orchestrator; -pub mod react_execution; -pub mod service; -pub mod session_recording; -pub mod state; -pub mod streaming_execution; -pub use context::{AiContextSenderType, RoomMessageContext}; -pub use service::ChatService; -pub use state::{AgentRuntime, AgentState}; diff --git a/libs/agent/chat/orchestrator.rs b/libs/agent/chat/orchestrator.rs deleted file mode 100644 index 8cf243f..0000000 --- a/libs/agent/chat/orchestrator.rs +++ /dev/null @@ -1,317 +0,0 @@ -use std::collections::HashMap; - -use super::agent_profile::{profile_for_role_name, should_enable_delegation, supervisor_profile}; -use super::message_builder::MessageBuilder; -use super::nonstreaming_execution::execute_process; -use super::service::{ProcessResult, StreamResult}; -use super::{AiRequest, StreamCallback}; -use crate::error::Result; -use crate::tool::call::ToolError; -use crate::tool::registry::ToolRegistry; -use crate::tool::{ToolDefinition, ToolHandler, ToolParam, ToolSchema}; - -pub async fn execute_orchestrated_process( - request: AiRequest, - message_builder: &MessageBuilder, - tool_registry: &Option, - ai_base_url: Option, - ai_api_key: Option, -) -> Result { - if request - .execution_profile - .as_ref() - .is_some_and(|p| p.disable_orchestration) - { - return execute_process( - request, - message_builder, - tool_registry, - ai_base_url, - ai_api_key, - ) - .await; - } - - let tools = request.tools.clone().unwrap_or_default(); - if !should_enable_delegation(&request.input, !tools.is_empty()) { - return execute_process( - request, - message_builder, - tool_registry, - ai_base_url, - ai_api_key, - ) - .await; - } - - let mut enhanced_registry = tool_registry.clone().unwrap_or_default(); - register_call_sub_agent_tool( - &mut enhanced_registry, - &request, - message_builder, - tool_registry, - ai_base_url.clone(), - ai_api_key.clone(), - ); - - let mut supervisor_request = request.clone(); - let profile = supervisor_profile(); - supervisor_request.execution_profile = Some(profile.clone()); - supervisor_request.tools = Some(enhanced_registry.to_openai_tools()); - supervisor_request.temperature = profile.temperature.unwrap_or(request.temperature); - supervisor_request.max_tokens = profile.max_tokens.unwrap_or(request.max_tokens); - supervisor_request.top_p = profile.top_p.unwrap_or(request.top_p); - supervisor_request.frequency_penalty = profile - .frequency_penalty - .unwrap_or(request.frequency_penalty); - supervisor_request.presence_penalty = - profile.presence_penalty.unwrap_or(request.presence_penalty); - - execute_process( - supervisor_request, - message_builder, - &Some(enhanced_registry), - ai_base_url, - ai_api_key, - ) - .await -} - -pub async fn execute_orchestrated_stream( - request: AiRequest, - on_chunk: StreamCallback, - message_builder: &MessageBuilder, - tool_registry: &Option, - ai_base_url: Option, - ai_api_key: Option, -) -> Result { - if request - .execution_profile - .as_ref() - .is_some_and(|p| p.disable_orchestration) - { - return super::streaming_execution::execute_process_stream( - request, - on_chunk, - message_builder, - tool_registry, - ai_base_url, - ai_api_key, - ) - .await; - } - - let tools = request.tools.clone().unwrap_or_default(); - if !should_enable_delegation(&request.input, !tools.is_empty()) { - return super::streaming_execution::execute_process_stream( - request, - on_chunk, - message_builder, - tool_registry, - ai_base_url, - ai_api_key, - ) - .await; - } - - let mut enhanced_registry = tool_registry.clone().unwrap_or_default(); - register_call_sub_agent_tool( - &mut enhanced_registry, - &request, - message_builder, - tool_registry, - ai_base_url.clone(), - ai_api_key.clone(), - ); - - let mut supervisor_request = request.clone(); - let profile = supervisor_profile(); - supervisor_request.execution_profile = Some(profile.clone()); - supervisor_request.tools = Some(enhanced_registry.to_openai_tools()); - supervisor_request.temperature = profile.temperature.unwrap_or(request.temperature); - supervisor_request.max_tokens = profile.max_tokens.unwrap_or(request.max_tokens); - supervisor_request.top_p = profile.top_p.unwrap_or(request.top_p); - supervisor_request.frequency_penalty = profile - .frequency_penalty - .unwrap_or(request.frequency_penalty); - supervisor_request.presence_penalty = - profile.presence_penalty.unwrap_or(request.presence_penalty); - - super::streaming_execution::execute_process_stream( - supervisor_request, - on_chunk, - message_builder, - &Some(enhanced_registry), - ai_base_url, - ai_api_key, - ) - .await -} - -fn register_call_sub_agent_tool( - registry: &mut ToolRegistry, - request: &AiRequest, - message_builder: &MessageBuilder, - original_registry: &Option, - ai_base_url: Option, - ai_api_key: Option, -) { - let captured_request = request.clone(); - let captured_message_builder = message_builder.clone(); - let captured_original_registry = original_registry.clone(); - let captured_base_url = ai_base_url; - let captured_api_key = ai_api_key; - - registry.register( - ToolDefinition::new("call_sub_agent") - .description( - "Delegate a task to a specialist sub-agent and receive its output.\n\ - Available roles:\n\ - - researcher: Gathers facts, evidence, and data. Best for finding information and searching code.\n\ - - analyst: Builds explanations, highlights causal links and tradeoffs. Best for reasoning about implications.\n\ - - reviewer: Stress-tests proposals, identifies risks and contradictions. Best for quality checks.\n\ - - architect: Maps systems, dependencies, boundaries, and design tradeoffs. Best for architecture decisions.\n\ - - debugger: Finds root causes, suspect changes, and validation paths. Best for bugs and regressions.\n\ - - implementer: Converts requirements into concrete implementation steps. Best for execution planning.\n\ - - tester: Designs validation and regression coverage. Best for test strategy.\n\ - - security: Reviews auth, data exposure, injection, dependency, and abuse risks. Best for sensitive changes.\n\ - Provide a clear, focused task description so the sub-agent knows exactly what to investigate.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some({ - let mut p = HashMap::new(); - p.insert( - "role".into(), - ToolParam { - name: "role".into(), - param_type: "string".into(), - description: Some( - "The sub-agent role to delegate to: researcher, analyst, reviewer, architect, debugger, implementer, tester, or security.".into(), - ), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "task".into(), - ToolParam { - name: "task".into(), - param_type: "string".into(), - description: Some( - "The specific task or question for the sub-agent. Be precise and focused.".into(), - ), - required: true, - properties: None, - items: None, - }, - ); - p - }), - required: Some(vec!["role".into(), "task".into()]), - }), - ToolHandler::new(move |_ctx, args| { - // Extract owned values from args before async move (avoid borrowing across boundary) - let role = args - .get("role") - .and_then(|v| v.as_str()) - .unwrap_or("researcher") - .to_owned(); - let task = args - .get("task") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_owned(); - - let profile = profile_for_role_name(role.as_str()); - - let mut sub_request = captured_request.clone(); - sub_request.input = format!( - "Sub-agent role: {role}\n\nTask:\n{task}\n\nOriginal user request:\n{}\n\nInstructions:\nFocus only on your assigned task. Return concise, evidence-backed findings.", - captured_request.input - ); - sub_request.execution_profile = Some(profile.clone()); - sub_request.tools = Some(filter_tools_for_sub_agent( - &captured_request.tools, - &profile.allowed_tools, - )); - sub_request.max_tool_depth = profile - .max_tool_depth - .unwrap_or(captured_request.max_tool_depth); - sub_request.temperature = profile.temperature.unwrap_or(captured_request.temperature); - sub_request.max_tokens = profile.max_tokens.unwrap_or(captured_request.max_tokens); - sub_request.top_p = profile.top_p.unwrap_or(captured_request.top_p); - sub_request.frequency_penalty = profile - .frequency_penalty - .unwrap_or(captured_request.frequency_penalty); - sub_request.presence_penalty = profile - .presence_penalty - .unwrap_or(captured_request.presence_penalty); - - // Clone captured values for this invocation so the Fn closure retains them - let mb = captured_message_builder.clone(); - let sub_registry = captured_original_registry.clone(); - let base = captured_base_url.clone(); - let key = captured_api_key.clone(); - - Box::pin(async move { - let result = execute_process(sub_request, &mb, &sub_registry, base, key).await; - match result { - Ok(r) => Ok(serde_json::json!({ - "role": role, - "output": r.content, - "input_tokens": r.input_tokens, - "output_tokens": r.output_tokens, - })), - Err(e) => Err(ToolError::ExecutionError(format!( - "Sub-agent '{}' execution failed: {}", - role, e - ))), - } - }) - }), - ); -} - -/// Filter the original tool definitions by the sub-agent's allowed list, -/// always excluding `call_sub_agent` to prevent recursive delegation. -fn filter_tools_for_sub_agent( - original_tools: &Option>, - allowed_tools: &Option>, -) -> Vec { - let Some(tools) = original_tools else { - return Vec::new(); - }; - let allowed = allowed_tools.as_ref().map(|list| { - list.iter() - .filter(|n| *n != "call_sub_agent") - .cloned() - .collect::>() - }); - - match allowed { - Some(allowed_list) if !allowed_list.is_empty() => tools - .iter() - .filter(|tool| { - let name = tool - .get("function") - .and_then(|f| f.get("name")) - .and_then(|v| v.as_str()) - .unwrap_or(""); - allowed_list.iter().any(|allowed| allowed == name) - }) - .cloned() - .collect(), - _ => tools - .iter() - .filter(|tool| { - tool.get("function") - .and_then(|f| f.get("name")) - .and_then(|v| v.as_str()) - .is_some_and(|name| name != "call_sub_agent") - }) - .cloned() - .collect(), - } -} diff --git a/libs/agent/chat/react_execution.rs b/libs/agent/chat/react_execution.rs deleted file mode 100644 index 9f51c0b..0000000 --- a/libs/agent/chat/react_execution.rs +++ /dev/null @@ -1,233 +0,0 @@ -use futures::StreamExt; -use models::rooms::room_ai; -use rig::agent::{AgentBuilder, MultiTurnStreamItem}; -use rig::client::CompletionClient; -use rig::streaming::{StreamedAssistantContent, StreamingPrompt}; -use sea_orm::*; -use uuid::Uuid; - -use super::AiRequest; -use super::session_recording::record_ai_session; -use crate::client::AiClientConfig; -use crate::error::{AgentError, Result}; -use crate::react::types::Action as ReactAction; -use crate::react::{DEFAULT_SYSTEM_PROMPT, ReactStep}; -use crate::tool::{RecordingTool, registry::ToolRegistry}; - -pub async fn execute_process_react( - request: &AiRequest, - mut on_chunk: C, - tool_registry: &ToolRegistry, - ai_base_url: Option, - ai_api_key: Option, - room_preamble: Option<&str>, - message_producer: Option, -) -> Result<(String, i64, i64)> -where - C: FnMut(ReactStep) -> Fut + Send, - Fut: std::future::Future + Send, -{ - let base_url = ai_base_url.unwrap_or_else(|| "https://api.openai.com".into()); - let api_key = ai_api_key.unwrap_or_default(); - let client_config = AiClientConfig::new(api_key).with_base_url(base_url); - - let db = request.db.clone(); - let cache = request.cache.clone(); - let cfg = request.config.clone(); - let room_id = request.room.id; - let sender_uid = request.sender.uid; - let project_id = request.project.id; - let ai_model_id = request.model.id; - let ai_model_name = request.model.name.clone(); - let sent_in_turn = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - let session_id = Uuid::now_v7(); - let session_start = std::time::Instant::now(); - let version_id = room_ai::Entity::find() - .filter(room_ai::Column::Room.eq(request.room.id)) - .filter(room_ai::Column::Model.eq(request.model.id)) - .one(&request.db) - .await - .ok() - .flatten() - .and_then(|r| r.version); - - let mut tools: Vec> = Vec::new(); - for def in tool_registry.definitions() { - let name = def.name.clone(); - if let Some(handler) = tool_registry.get(&name) { - let adapter = crate::tool::RigToolAdapter::new( - handler.clone(), - def.clone(), - db.clone(), - cache.clone(), - cfg.clone(), - room_id, - Some(sender_uid), - project_id, - message_producer.clone(), - Some(ai_model_id), - Some(ai_model_name.clone()), - sent_in_turn.clone(), - ); - tools.push(Box::new(RecordingTool::new( - Box::new(adapter), - db.clone(), - session_id, - sender_uid, - ))); - } - } - - let rig_client = client_config.build_rig_client(); - let model = rig_client.completion_model(&request.model.name); - - // General rules first (strong LLM attention), room context appended after - // so that output-format rules aren't buried behind long room preamble. - let preamble = match room_preamble { - Some(rp) => format!("{}\n{}", DEFAULT_SYSTEM_PROMPT, rp), - None => DEFAULT_SYSTEM_PROMPT.to_string(), - }; - - let agent = AgentBuilder::new(model) - .preamble(&preamble) - .tools(tools) - .default_max_turns(request.max_tool_depth) - .build(); - - let stream = agent - .stream_prompt(&request.input) - .with_history(Vec::::new()) - .multi_turn(request.max_tool_depth) - .await; - - tokio::pin!(stream); - - let mut step_count = 0usize; - let mut final_content = String::new(); - let mut total_input_tokens: i64 = 0; - let mut total_output_tokens: i64 = 0; - - while let Some(item) = stream.next().await { - match item { - Ok(MultiTurnStreamItem::StreamAssistantItem(StreamedAssistantContent::Text(text))) => { - step_count += 1; - let t = text.text; - on_chunk(ReactStep::Answer { - step: step_count, - answer: t.clone(), - }) - .await; - final_content.push_str(&t); - } - Ok(MultiTurnStreamItem::StreamAssistantItem(StreamedAssistantContent::Reasoning( - reasoning, - ))) => { - let reasoning_text: String = reasoning - .content - .iter() - .filter_map(|c| match c { - rig::completion::message::ReasoningContent::Text { text, .. } => { - Some(text.as_str()) - } - _ => None, - }) - .collect::>() - .join(""); - if !reasoning_text.is_empty() { - step_count += 1; - on_chunk(ReactStep::Thought { - step: step_count, - thought: reasoning_text, - }) - .await; - } - } - Ok(MultiTurnStreamItem::StreamAssistantItem( - StreamedAssistantContent::ReasoningDelta { reasoning, .. }, - )) => { - if !reasoning.is_empty() { - step_count += 1; - on_chunk(ReactStep::Thought { - step: step_count, - thought: reasoning, - }) - .await; - } - } - Ok(MultiTurnStreamItem::StreamAssistantItem(StreamedAssistantContent::ToolCall { - tool_call, - .. - })) => { - step_count += 1; - let args: serde_json::Value = match &tool_call.function.arguments { - serde_json::Value::String(s) => { - serde_json::from_str(s).unwrap_or(serde_json::Value::Null) - } - v => v.clone(), - }; - on_chunk(ReactStep::Action { - step: step_count, - action: ReactAction::new(&tool_call.function.name, args), - }) - .await; - } - Ok(MultiTurnStreamItem::StreamUserItem( - rig::streaming::StreamedUserContent::ToolResult { tool_result, .. }, - )) => { - step_count += 1; - let obs = tool_result_content_to_string(&tool_result.content); - on_chunk(ReactStep::Observation { - step: step_count, - observation: obs, - }) - .await; - } - Ok(MultiTurnStreamItem::FinalResponse(resp)) => { - let usage = resp.usage(); - total_input_tokens = usage.input_tokens as i64; - total_output_tokens = usage.output_tokens as i64; - } - Err(e) => { - let err_msg = format!("rig agent stream error: {}", e); - return Err(AgentError::OpenAi(err_msg)); - } - _ => {} - } - } - - let elapsed_ms = session_start.elapsed().as_millis() as i64; - record_ai_session( - &request.cache, - &request.db, - request.project.id, - request.sender.uid, - session_id, - request.room.id, - request.model.id, - version_id.unwrap_or_default(), - total_input_tokens, - total_output_tokens, - elapsed_ms, - ) - .await; - - Ok((final_content, total_input_tokens, total_output_tokens)) -} - -/// Extract text from rig's ToolResultContent, ignoring images. -fn tool_result_content_to_string( - content: &rig::one_or_many::OneOrMany, -) -> String { - use rig::completion::message::ToolResultContent; - content - .iter() - .filter_map(|item| { - if let ToolResultContent::Text(t) = item { - Some(t.text.clone()) - } else { - None - } - }) - .collect::>() - .join("\n") -} diff --git a/libs/agent/chat/service.rs b/libs/agent/chat/service.rs deleted file mode 100644 index 99b9bf7..0000000 --- a/libs/agent/chat/service.rs +++ /dev/null @@ -1,268 +0,0 @@ -use super::message_builder::MessageBuilder; -use super::{AiRequest, StreamCallback}; -use crate::client::AiClientConfig; -use crate::client::StreamChunk; -use crate::compact::CompactService; -use crate::embed::EmbedService; -use crate::error::Result; -use crate::perception::PerceptionService; -use crate::tool::registry::ToolRegistry; -use queue::MessageProducer; - -/// Result from streaming AI response. -pub struct StreamResult { - pub content: String, - pub reasoning_content: String, - pub input_tokens: i64, - pub output_tokens: i64, - /// All chunks in arrival order — preserves ReAct multi-cycle ordering. - pub chunks: Vec, -} - -/// Result from non-streaming AI response. -pub struct ProcessResult { - pub content: String, - pub input_tokens: i64, - pub output_tokens: i64, -} - -/// Service for handling AI chat requests in rooms. -pub struct ChatService { - ai_base_url: Option, - ai_api_key: Option, - message_builder: MessageBuilder, - tool_registry: Option, -} - -impl ChatService { - pub fn new() -> Self { - Self { - ai_base_url: None, - ai_api_key: None, - message_builder: MessageBuilder::new(), - tool_registry: None, - } - } - - pub fn with_ai_client_config(mut self, config: AiClientConfig) -> Self { - self.ai_base_url = config.base_url.clone(); - self.ai_api_key = Some(config.api_key.clone()); - self - } - - pub fn with_compact_service(mut self, compact_service: CompactService) -> Self { - self.message_builder = self.message_builder.with_compact_service(compact_service); - self - } - - pub fn with_embed_service(mut self, embed_service: EmbedService) -> Self { - self.message_builder = self.message_builder.with_embed_service(embed_service); - self - } - - pub fn with_perception_service(mut self, perception_service: PerceptionService) -> Self { - self.message_builder = self - .message_builder - .with_perception_service(perception_service); - self - } - - pub fn with_tool_registry(mut self, registry: ToolRegistry) -> Self { - self.tool_registry = Some(registry); - self - } - - /// Returns all registered tools as JSON tool definitions. - pub fn tools(&self) -> Vec { - self.tool_registry - .as_ref() - .map(|r| r.to_openai_tools()) - .unwrap_or_default() - } - - /// Build a RigToolSet from the registered tool registry. - /// - /// This enables using the same tools with `RigAgentService` via rig's native Agent. - /// The context (db, cache, config, room_id, sender_id) is passed through to each - /// tool handler at creation time. - #[cfg(feature = "rig")] - pub fn rig_toolset( - &self, - db: db::database::AppDatabase, - cache: db::cache::AppCache, - config: config::AppConfig, - room_id: uuid::Uuid, - sender_id: Option, - project_id: uuid::Uuid, - ) -> Option { - self.tool_registry.as_ref().map(|registry| { - crate::RigToolSet::from_registry( - registry, - db, - cache, - config, - room_id, - sender_id, - project_id, - None, - None, - None, - std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), - ) - }) - } - - /// Get a reference to the underlying ToolRegistry. - pub fn tool_registry(&self) -> Option<&ToolRegistry> { - self.tool_registry.as_ref() - } - - pub async fn build_room_optimized_context_text( - &self, - request: &AiRequest, - ) -> Result<(String, Option)> { - self.message_builder - .build_room_optimized_context_text(request) - .await - } - - /// Process AI request without streaming (tool-call loop with non-streaming API). - pub async fn process(&self, request: AiRequest) -> Result { - super::orchestrator::execute_orchestrated_process( - request, - &self.message_builder, - &self.tool_registry, - self.ai_base_url.clone(), - self.ai_api_key.clone(), - ) - .await - } - - /// Process AI request with streaming (tool-call loop with streaming API, incremental chunks). - pub async fn process_stream( - &self, - request: AiRequest, - on_chunk: StreamCallback, - ) -> Result { - super::orchestrator::execute_orchestrated_stream( - request, - on_chunk, - &self.message_builder, - &self.tool_registry, - self.ai_base_url.clone(), - self.ai_api_key.clone(), - ) - .await - } - - /// Process AI request for room context — direct execution path (bypasses orchestrator). - /// - /// Room AI uses a fast single-agent loop: all tools available, no multi-agent delegation. - /// Merges `room_tools` (send_message, retract_message) into the base registry, - /// then runs `execute_process` / `execute_process_stream` directly. - pub async fn process_room( - &self, - request: AiRequest, - room_tools: ToolRegistry, - ) -> Result { - let mut merged = self.tool_registry.clone().unwrap_or_default(); - merged.merge(room_tools); - - super::nonstreaming_execution::execute_process( - request, - &self.message_builder, - &Some(merged), - self.ai_base_url.clone(), - self.ai_api_key.clone(), - ) - .await - } - - /// Process AI request for room context with streaming — direct execution path. - /// - /// Same as `process_room` but with streaming response. Bypasses orchestrator, - /// gives the room AI all tools (base + room) for fast single-agent execution. - pub async fn process_room_stream( - &self, - request: AiRequest, - on_chunk: StreamCallback, - room_tools: ToolRegistry, - ) -> Result { - let mut merged = self.tool_registry.clone().unwrap_or_default(); - merged.merge(room_tools); - - super::streaming_execution::execute_process_stream( - request, - on_chunk, - &self.message_builder, - &Some(merged), - self.ai_base_url.clone(), - self.ai_api_key.clone(), - ) - .await - } - - /// Process AI request via rig-based ReAct streaming loop. - pub async fn process_react( - &self, - request: &AiRequest, - on_chunk: C, - ) -> Result<(String, i64, i64)> - where - C: FnMut(crate::react::ReactStep) -> Fut + Send, - Fut: std::future::Future + Send, - { - let Some(registry) = &self.tool_registry else { - return Err(crate::error::AgentError::Internal( - "no tool registry registered".into(), - )); - }; - super::react_execution::execute_process_react( - request, - on_chunk, - registry, - self.ai_base_url.clone(), - self.ai_api_key.clone(), - None, - None, - ) - .await - } - - /// Process AI request via rig-based ReAct streaming loop with room-specific tools. - /// - /// Merges `room_tools` (e.g. `send_message`, `retract_message`) into the base - /// tool registry on-the-fly. The `room_preamble` is prepended to the default - /// system prompt to instruct the AI about room communication rules. - /// `message_producer` enables tools to publish events via the message queue. - pub async fn process_react_room( - &self, - request: &AiRequest, - on_chunk: C, - room_tools: ToolRegistry, - room_preamble: Option<&str>, - message_producer: Option, - ) -> Result<(String, i64, i64)> - where - C: FnMut(crate::react::ReactStep) -> Fut + Send, - Fut: std::future::Future + Send, - { - let Some(registry) = &self.tool_registry else { - return Err(crate::error::AgentError::Internal( - "no tool registry registered".into(), - )); - }; - let mut merged = registry.clone(); - merged.merge(room_tools); - super::react_execution::execute_process_react( - request, - on_chunk, - &merged, - self.ai_base_url.clone(), - self.ai_api_key.clone(), - room_preamble, - message_producer, - ) - .await - } -} diff --git a/libs/agent/chat/state.rs b/libs/agent/chat/state.rs deleted file mode 100644 index 655106c..0000000 --- a/libs/agent/chat/state.rs +++ /dev/null @@ -1,217 +0,0 @@ -//! Agent state machine — tracks lifecycle of a single AI agent invocation. -//! -//! States: Idle → Thinking → ToolCall → Thinking → ... → Answering | Error -//! The Thinking ↔ ToolCall cycle repeats until max tool depth or final answer. - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -/// Current phase of an agent's execution lifecycle. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum AgentState { - /// Agent is idle, waiting for input - Idle, - /// Agent is reasoning/thinking (may produce thinking chunks) - Thinking { - started_at: DateTime, - tool_depth: u32, - }, - /// Agent is executing a tool call - ToolCall { - tool_name: String, - started_at: DateTime, - }, - /// Agent is returning the final answer - Answering { - /// Accumulated answer content so far - content_chars: u64, - started_at: DateTime, - }, - /// Agent encountered a non-recoverable error - Error { message: String, tool_depth: u32 }, -} - -impl AgentState { - pub fn is_terminal(&self) -> bool { - matches!( - self, - AgentState::Answering { .. } | AgentState::Error { .. } - ) - } - - pub fn is_idle(&self) -> bool { - matches!(self, AgentState::Idle) - } - - pub fn current_phase(&self) -> &'static str { - match self { - AgentState::Idle => "idle", - AgentState::Thinking { .. } => "thinking", - AgentState::ToolCall { .. } => "tool_call", - AgentState::Answering { .. } => "answering", - AgentState::Error { .. } => "error", - } - } -} - -/// State machine for agent lifecycle transitions. -pub struct AgentRuntime { - state: AgentState, - max_tool_depth: u32, - current_depth: u32, -} - -impl AgentRuntime { - pub fn new(max_tool_depth: u32) -> Self { - Self { - state: AgentState::Idle, - max_tool_depth, - current_depth: 0, - } - } - - pub fn state(&self) -> &AgentState { - &self.state - } - - /// Transition from Idle → Thinking - pub fn start_thinking(&mut self) { - debug_assert!(self.state.is_idle(), "must be Idle to start thinking"); - self.current_depth = 0; - self.state = AgentState::Thinking { - started_at: Utc::now(), - tool_depth: 0, - }; - } - - /// Transition from Thinking → ToolCall (increments tool depth) - pub fn start_tool_call(&mut self, tool_name: String) -> Result<(), &'static str> { - if !matches!(self.state, AgentState::Thinking { .. }) { - return Err("must be Thinking to start tool call"); - } - if self.current_depth >= self.max_tool_depth { - return Err("max tool depth reached"); - } - self.state = AgentState::ToolCall { - tool_name, - started_at: Utc::now(), - }; - Ok(()) - } - - /// Transition from ToolCall → Thinking (back after tool result) - pub fn complete_tool_call(&mut self) -> Result<(), &'static str> { - if !matches!(self.state, AgentState::ToolCall { .. }) { - return Err("must be ToolCall to complete"); - } - self.current_depth += 1; - self.state = AgentState::Thinking { - started_at: Utc::now(), - tool_depth: self.current_depth, - }; - Ok(()) - } - - /// Transition to Answering (terminal) - pub fn start_answer(&mut self) { - self.state = AgentState::Answering { - content_chars: 0, - started_at: Utc::now(), - }; - } - - pub fn append_answer(&mut self, content: &str) { - if let AgentState::Answering { content_chars, .. } = &mut self.state { - *content_chars += content.len() as u64; - } - } - - /// Transition to Error (terminal) - pub fn fail(&mut self, message: String) { - self.state = AgentState::Error { - message, - tool_depth: self.current_depth, - }; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_starts_idle() { - let rt = AgentRuntime::new(10); - assert!(rt.state().is_idle()); - assert_eq!(rt.state().current_phase(), "idle"); - } - - #[test] - fn test_idle_to_thinking() { - let mut rt = AgentRuntime::new(10); - rt.start_thinking(); - assert_eq!(rt.state().current_phase(), "thinking"); - assert!(!rt.state().is_terminal()); - } - - #[test] - fn test_thinking_to_tool_call_and_back() { - let mut rt = AgentRuntime::new(10); - rt.start_thinking(); - rt.start_tool_call("search".into()).unwrap(); - assert_eq!(rt.state().current_phase(), "tool_call"); - rt.complete_tool_call().unwrap(); - assert_eq!(rt.state().current_phase(), "thinking"); - } - - #[test] - fn test_thinking_to_answer() { - let mut rt = AgentRuntime::new(10); - rt.start_thinking(); - rt.start_answer(); - assert_eq!(rt.state().current_phase(), "answering"); - assert!(rt.state().is_terminal()); - } - - #[test] - fn test_append_answer_tracks_chars() { - let mut rt = AgentRuntime::new(10); - rt.start_thinking(); - rt.start_answer(); - rt.append_answer("hello"); - if let AgentState::Answering { content_chars, .. } = rt.state() { - assert_eq!(*content_chars, 5); - } else { - panic!("expected Answering state"); - } - } - - #[test] - fn test_error_is_terminal() { - let mut rt = AgentRuntime::new(10); - rt.start_thinking(); - rt.fail("something broke".into()); - assert_eq!(rt.state().current_phase(), "error"); - assert!(rt.state().is_terminal()); - } - - #[test] - fn test_transition_from_wrong_state() { - let mut rt = AgentRuntime::new(10); - // Can't start tool call from Idle - assert!(rt.start_tool_call("tool".into()).is_err()); - // Can't complete tool call from Idle - assert!(rt.complete_tool_call().is_err()); - } - - #[test] - fn test_max_depth_rejected() { - let mut rt = AgentRuntime::new(2); - rt.start_thinking(); - rt.start_tool_call("tool1".into()).unwrap(); - rt.complete_tool_call().unwrap(); - rt.start_tool_call("tool2".into()).unwrap(); - rt.complete_tool_call().unwrap(); - assert!(rt.start_tool_call("tool3".into()).is_err()); - } -} diff --git a/libs/agent/chat/streaming_execution.rs b/libs/agent/chat/streaming_execution.rs deleted file mode 100644 index b051697..0000000 --- a/libs/agent/chat/streaming_execution.rs +++ /dev/null @@ -1,511 +0,0 @@ -use models::projects::project_skill; -use models::rooms::room_ai; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; -use std::pin::Pin; -use std::sync::Arc; -use uuid::Uuid; - -use super::message_builder::MessageBuilder; -use super::service::StreamResult; -use super::session_recording::record_ai_session; -use super::{AiChunkType, AiRequest, AiStreamChunk, StreamCallback}; -use crate::client::AiClientConfig; -use crate::client::types::{ChatRequestMessage, ToolCall}; -use crate::client::{StreamChunk, StreamChunkType, StreamedToolCall, call_stream}; -use crate::error::Result; -use crate::perception::{SkillEntry, ToolCallEvent}; -use crate::tool::{ToolCall as AgentToolCall, ToolContext, ToolExecutor}; - -type SharedCallback = Arc< - dyn Fn(AiStreamChunk) -> Pin + Send>> + Send + Sync, ->; - -pub async fn execute_process_stream( - request: AiRequest, - on_chunk: StreamCallback, - message_builder: &MessageBuilder, - tool_registry: &Option, - ai_base_url: Option, - ai_api_key: Option, -) -> Result { - let on_chunk: SharedCallback = Arc::from(on_chunk); - let tools: Vec = request.tools.clone().unwrap_or_default(); - let tools_enabled = !tools.is_empty(); - let max_tool_depth = request.max_tool_depth; - - let mut messages = message_builder.build_messages(&request).await?; - - let room_ai_config = room_ai::Entity::find() - .filter(room_ai::Column::Room.eq(request.room.id)) - .filter(room_ai::Column::Model.eq(request.model.id)) - .one(&request.db) - .await?; - - let model_name = request.model.name.clone(); - let profile = request.execution_profile.as_ref(); - let temperature = profile - .and_then(|p| p.temperature.map(|v| v as f32)) - .or_else(|| { - room_ai_config - .as_ref() - .and_then(|r| r.temperature.map(|v| v as f32)) - }) - .unwrap_or(request.temperature as f32); - let max_tokens = profile - .and_then(|p| p.max_tokens.map(|v| v as u32)) - .or_else(|| { - room_ai_config - .as_ref() - .and_then(|r| r.max_tokens.map(|v| v as u32)) - }) - .unwrap_or(request.max_tokens as u32); - let mut tool_depth = 0; - let mut total_input_tokens = 0i64; - let mut total_output_tokens = 0i64; - let session_id = Uuid::now_v7(); - let session_start = std::time::Instant::now(); - let version_id = room_ai_config.as_ref().and_then(|r| r.version); - - let config = AiClientConfig::new(ai_api_key.unwrap_or_default()) - .with_base_url(ai_base_url.unwrap_or_else(|| "https://api.openai.com".into())); - - let mut full_content = String::new(); - let mut all_chunks: Vec = Vec::new(); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); - - loop { - let on_chunk_cb = on_chunk.clone(); - let on_chunk_cb2 = on_chunk.clone(); - let tx_arc = Arc::new(tx.clone()); - let tx_arc2 = tx_arc.clone(); - let response = call_stream( - &messages, - &model_name, - &config, - temperature, - max_tokens, - if tools_enabled { Some(&tools) } else { None }, - None, - Arc::new(move |delta| { - let content = delta.to_string(); - let fut = on_chunk_cb(AiStreamChunk { - content, - done: false, - chunk_type: AiChunkType::Answer, - metadata: None, - children_id: None, - }); - fut - }), - Arc::new(move |delta| { - let fut = on_chunk_cb2(AiStreamChunk { - content: delta.to_string(), - done: false, - chunk_type: AiChunkType::Thinking, - metadata: None, - children_id: None, - }); - fut - }), - Arc::new(move |tc: &StreamedToolCall| { - let tx = tx_arc2.clone(); - let tc_owned = tc.clone(); - Box::pin(async move { - let _ = tx.send(tc_owned); - }) as Pin + Send>> - }), - ) - .await?; - - total_input_tokens += response.input_tokens; - total_output_tokens += response.output_tokens; - all_chunks.extend(response.chunks.clone()); - - let has_tool_calls = tools_enabled && !response.tool_calls.is_empty(); - if !has_tool_calls { - return handle_final_answer( - response, - all_chunks, - &request, - session_id, - version_id, - total_input_tokens, - total_output_tokens, - session_start, - ) - .await; - } - - full_content.push_str(&response.content); - - let tool_calls: Vec = response - .tool_calls - .iter() - .map(|tc| ToolCall { - id: tc.id.clone(), - type_: "function".into(), - function: crate::client::types::ToolCallFunction { - name: tc.name.clone(), - arguments: tc.arguments.clone(), - }, - }) - .collect(); - - messages.push(ChatRequestMessage::assistant( - Some(response.content.clone()), - Some(tool_calls.clone()), - )); - - drain_tool_call_notifications(&mut rx, &on_chunk, &mut all_chunks).await; - - let calls: Vec = response - .tool_calls - .iter() - .map(|tc| AgentToolCall { - id: tc.id.clone(), - name: tc.name.clone(), - arguments: tc.arguments.clone(), - }) - .collect(); - - let tool_messages = execute_streaming_tools( - &request, - &calls, - session_id, - &on_chunk, - &mut all_chunks, - tool_registry, - message_builder, - ) - .await; - - messages.extend(tool_messages); - inject_passive_skills_stream( - &request, - message_builder, - &response.tool_calls, - &mut messages, - ) - .await; - - tool_depth += 1; - if tool_depth >= max_tool_depth { - let max_depth_text = format!( - "[AI reached maximum tool depth ({}) — no final answer produced]", - max_tool_depth - ); - on_chunk(AiStreamChunk { - content: max_depth_text.clone(), - done: true, - chunk_type: AiChunkType::Answer, - metadata: None, - children_id: None, - }) - .await; - all_chunks.push(StreamChunk { - chunk_type: StreamChunkType::Answer, - content: max_depth_text, - }); - record_ai_session( - &request.cache, - &request.db, - request.project.id, - request.sender.uid, - session_id, - request.room.id, - request.model.id, - version_id.unwrap_or_default(), - total_input_tokens, - total_output_tokens, - session_start.elapsed().as_millis() as i64, - ) - .await; - return Ok(StreamResult { - content: full_content, - reasoning_content: String::new(), - input_tokens: 0, - output_tokens: 0, - chunks: all_chunks, - }); - } - } -} - -async fn drain_tool_call_notifications( - rx: &mut tokio::sync::mpsc::UnboundedReceiver, - on_chunk: &SharedCallback, - all_chunks: &mut Vec, -) { - loop { - match rx.try_recv() { - Ok(tc) => { - let args_display = if tc.arguments.len() > 100 { - let end = tc - .arguments - .char_indices() - .map(|(i, _)| i) - .take_while(|&i| i <= 100) - .last() - .unwrap_or(100); - format!("{}...", &tc.arguments[..end]) - } else { - tc.arguments.clone() - }; - let tool_display = format!("🔧 {}({})", tc.name, args_display); - // Parse arguments JSON for structured metadata - let args_json = - serde_json::from_str(&tc.arguments).unwrap_or(serde_json::json!({})); - let metadata = serde_json::json!({ - "tool": tc.name, - "args": args_json, - "display": tool_display.clone(), - }); - on_chunk(AiStreamChunk { - content: tool_display.clone(), - done: false, - chunk_type: AiChunkType::ToolCall, - metadata: Some(metadata), - children_id: None, - }) - .await; - all_chunks.push(StreamChunk { - chunk_type: StreamChunkType::ToolCall, - content: tool_display, - }); - } - Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break, - Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break, - } - } -} - -async fn execute_streaming_tools( - request: &AiRequest, - calls: &[AgentToolCall], - session_id: Uuid, - on_chunk: &SharedCallback, - all_chunks: &mut Vec, - tool_registry: &Option, - message_builder: &MessageBuilder, -) -> Vec { - let mut tool_messages = Vec::new(); - let mut ctx = ToolContext::new( - request.db.clone(), - request.cache.clone(), - request.config.clone(), - request.room.id, - Some(request.sender.uid), - ) - .with_project(request.project.id); - if let Some(es) = &message_builder.embed_service { - ctx = ctx.with_embed_service(es.clone()); - } - if let Some(registry) = tool_registry { - ctx.registry_mut().merge(registry.clone()); - } - - let recorder = - crate::tool::recorder::ToolCallRecorder::with_session(request.db.clone(), session_id); - let mut join_set = tokio::task::JoinSet::new(); - - for call in calls { - let call_clone = call.clone(); - let mut ctx_clone = ctx.clone(); - let sender_uid = request.sender.uid; - let recorder_clone = recorder.clone(); - - join_set.spawn(async move { - let start = std::time::Instant::now(); - let executor = ToolExecutor::new(); - let res = executor - .execute_batch(vec![call_clone.clone()], &mut ctx_clone) - .await; - (call_clone, res, start.elapsed(), sender_uid, recorder_clone) - }); - } - - let heartbeat_dur = std::time::Duration::from_secs(10); - while !join_set.is_empty() { - tokio::select! { - Some(res) = join_set.join_next() => { - if let Ok((call, results, elapsed, sender_uid, recorder)) = res { - match results { - Ok(results) => { - for result in &results { - let text = match &result.result { crate::tool::ToolResult::Ok(v) => v.to_string(), crate::tool::ToolResult::Error(msg) => msg.clone() }; - let preview = if text.len() > 300 { - let end = text.char_indices().map(|(i, _)| i).take_while(|&i| i <= 300).last().unwrap_or(300); - format!("{}...", &text[..end]) - } else { text.clone() }; - tracing::debug!("tool_result: {} — {}", call.name, preview); - - let is_error = matches!(result.result, crate::tool::ToolResult::Error(_)); - let error_msg = match &result.result { crate::tool::ToolResult::Error(msg) => Some(msg.clone()), _ => None }; - recorder.record(crate::tool::recorder::ToolCallRecord { - tool_call_id: call.id.clone(), - session_id: recorder.session_id(), - tool_name: call.name.clone(), - caller: sender_uid, - arguments: call.arguments_json().unwrap_or_default(), - status: if is_error { models::ai::ToolCallStatus::Failed } else { models::ai::ToolCallStatus::Success }, - execution_time_ms: Some(elapsed.as_millis() as i64), - error_message: error_msg, - error_stack: None, - retry_count: 0 - }); - } - let success_display = format!("✅ {}", call.name); - let result_preview: Vec = results.iter().map(|r| { - match &r.result { crate::tool::ToolResult::Ok(v) => v.to_string(), crate::tool::ToolResult::Error(msg) => msg.clone() } - }).collect(); - let metadata = serde_json::json!({ - "tool": call.name, - "status": "ok", - "result": result_preview.join("\n").chars().take(500).collect::(), - "display": success_display.clone(), - }); - on_chunk(AiStreamChunk { - content: success_display.clone(), - done: false, - chunk_type: AiChunkType::ToolResult, - metadata: Some(metadata), - children_id: Some(call.id.clone()), - }).await; - all_chunks.push(StreamChunk { chunk_type: StreamChunkType::ToolCall, content: success_display }); - let msgs = ToolExecutor::to_tool_messages(&results); - tool_messages.extend(msgs); - } - Err(e) => { - recorder.record(crate::tool::recorder::ToolCallRecord { - tool_call_id: call.id.clone(), - session_id: recorder.session_id(), - tool_name: call.name.clone(), - caller: sender_uid, - arguments: call.arguments_json().unwrap_or_default(), - status: models::ai::ToolCallStatus::Failed, - execution_time_ms: Some(elapsed.as_millis() as i64), - error_message: Some(e.to_string()), - error_stack: None, - retry_count: 0 - }); - let err_text = format!("[Tool call failed: {}]", e); - tracing::warn!(tool = %call.name, args = %call.arguments, error = %e, "tool_call_failed"); - let err_display = format!("❌ {} (failed)", call.name); - let metadata = serde_json::json!({ - "tool": call.name, - "status": "error", - "result": e.to_string(), - "display": err_display.clone(), - }); - on_chunk(AiStreamChunk { - content: err_display.clone(), - done: false, - chunk_type: AiChunkType::ToolResult, - metadata: Some(metadata), - children_id: None, - }).await; - all_chunks.push(StreamChunk { chunk_type: StreamChunkType::ToolCall, content: err_display }); - tool_messages.push(ChatRequestMessage::tool(&call.id, &err_text)); - } - } - } - }, - _ = tokio::time::sleep(heartbeat_dur) => { - on_chunk(AiStreamChunk { content: String::new(), done: false, chunk_type: AiChunkType::ToolCall, metadata: None, children_id: None }).await; - } - } - } - tool_messages -} - -async fn handle_final_answer( - response: crate::client::StreamResponse, - all_chunks: Vec, - request: &AiRequest, - session_id: Uuid, - version_id: Option, - total_input_tokens: i64, - total_output_tokens: i64, - session_start: std::time::Instant, -) -> Result { - let full_content = response.content.clone(); - // Don't push full content as a chunk — incremental deltas in - // response.chunks (already accumulated above) sum to the same text. - // merge_consecutive_blocks would concatenate delta_sum + full = - // 2× full, causing duplicate content in DB persistence. - record_ai_session( - &request.cache, - &request.db, - request.project.id, - request.sender.uid, - session_id, - request.room.id, - request.model.id, - version_id.unwrap_or_default(), - total_input_tokens, - total_output_tokens, - session_start.elapsed().as_millis() as i64, - ) - .await; - Ok(StreamResult { - content: full_content, - reasoning_content: response.reasoning_content, - input_tokens: total_input_tokens, - output_tokens: total_output_tokens, - chunks: all_chunks, - }) -} - -async fn inject_passive_skills_stream( - request: &AiRequest, - message_builder: &MessageBuilder, - tool_calls: &[StreamedToolCall], - messages: &mut Vec, -) { - if let Ok(skills) = project_skill::Entity::find() - .filter(project_skill::Column::ProjectUuid.eq(request.project.id)) - .filter(project_skill::Column::Enabled.eq(true)) - .all(&request.db) - .await - { - let mut skill_entries: Vec = skills - .into_iter() - .map(|s| SkillEntry { - slug: s.slug, - name: s.name, - description: s.description, - content: s.content, - }) - .collect(); - for built_in in crate::skills::all_skills() { - if !skill_entries.iter().any(|s| s.slug == built_in.slug) { - skill_entries.push(SkillEntry { - slug: built_in.slug.to_string(), - name: built_in.name.to_string(), - description: Some(built_in.description.to_string()), - content: built_in.content.clone(), - }); - } - } - let tool_events: Vec = tool_calls - .iter() - .map(|tc| ToolCallEvent { - tool_name: tc.name.clone(), - arguments: tc.arguments.clone(), - }) - .collect(); - let mut contexts = Vec::new(); - for event in &tool_events { - if let Some(ctx) = message_builder - .perception_service - .passive - .detect(event, &skill_entries) - { - MessageBuilder::push_unique_skill_context(&mut contexts, ctx); - } - } - for ctx in contexts { - messages.push(ctx.to_system_message()); - } - } -} diff --git a/libs/agent/client/mod.rs b/libs/agent/client/mod.rs deleted file mode 100644 index 8bdd986..0000000 --- a/libs/agent/client/mod.rs +++ /dev/null @@ -1,831 +0,0 @@ -//! Unified AI client with built-in retry, token tracking, and session recording. -//! -//! Uses rig-core as the underlying AI provider library. - -pub mod types; -pub use types::{ChatRequestMessage, ToolCall as ClientToolCall}; - -use std::pin::Pin; -use std::sync::Arc; -use std::time::Instant; -use uuid::Uuid; - -use crate::error::{AgentError, Result}; - -use futures::StreamExt; -use rig::completion::message::{AssistantContent, Message as RigMessage}; -use rig::completion::{CompletionModel, GetTokenUsage, ToolDefinition}; -use rig::one_or_many::OneOrMany; -use rig::prelude::CompletionClient; -use rig::providers::openai; - -/// AI call metrics — increments metrics crate counters for all AI calls. -#[derive(Debug, Clone, Default)] -pub struct AiMetrics; - -impl AiMetrics { - pub fn new() -> Self { - Self - } - - pub fn record_success(&self, input_tokens: i64, output_tokens: i64, has_function_call: bool) { - metrics::counter!("ai_calls_total").increment(1); - metrics::counter!("ai_calls_success").increment(1); - if input_tokens > 0 { - metrics::counter!("ai_input_tokens_total").increment(input_tokens as u64); - } - if output_tokens > 0 { - metrics::counter!("ai_output_tokens_total").increment(output_tokens as u64); - } - if has_function_call { - metrics::counter!("ai_function_calls_total").increment(1); - } - } - - pub fn record_failure(&self) { - metrics::counter!("ai_calls_total").increment(1); - metrics::counter!("ai_calls_failure").increment(1); - } -} - -/// Configuration for the AI client. -#[derive(Clone)] -pub struct AiClientConfig { - pub api_key: String, - pub base_url: Option, -} - -impl AiClientConfig { - pub fn new(api_key: String) -> Self { - Self { - api_key, - base_url: None, - } - } - - pub fn with_base_url(mut self, base_url: impl Into) -> Self { - self.base_url = Some(base_url.into()); - self - } - - /// Build a rig OpenAI client from this config. - pub fn build_rig_client(&self) -> openai::Client { - let base = self - .base_url - .clone() - .unwrap_or_else(|| "https://api.openai.com".to_string()); - openai::Client::builder() - .api_key(&self.api_key) - .base_url(&base) - .build() - .expect("Failed to build rig OpenAI client") - } -} - -/// Response from an AI call, including usage statistics. -#[derive(Debug, Clone)] -pub struct AiCallResponse { - pub content: String, - pub input_tokens: i64, - pub output_tokens: i64, - pub latency_ms: i64, - pub tool_calls: Vec, - pub tool_calls_finished: Vec, -} - -impl AiCallResponse { - pub fn total_tokens(&self) -> i64 { - self.input_tokens + self.output_tokens - } -} - -/// Internal state for retry tracking. -#[derive(Debug)] -struct RetryState { - attempt: u32, - max_retries: u32, - max_backoff_ms: u64, -} - -impl RetryState { - fn new(max_retries: u32) -> Self { - Self { - attempt: 0, - max_retries, - max_backoff_ms: 5000, - } - } - fn should_retry(&self) -> bool { - self.attempt < self.max_retries - } - fn backoff_duration(&self) -> std::time::Duration { - let exp = self.attempt.min(5); - let base_ms = 500u64 - .saturating_mul(2u64.pow(exp)) - .min(self.max_backoff_ms); - let max_jitter = (base_ms / 2).max(base_ms); - let offset = fastrand_u64(max_jitter + 1).saturating_sub(base_ms / 2); - let total = base_ms.saturating_add(offset).min(self.max_backoff_ms); - std::time::Duration::from_millis(total) - } - fn next(&mut self) { - self.attempt += 1; - } -} - -fn fastrand_u64(n: u64) -> u64 { - use std::sync::atomic::{AtomicU64, Ordering}; - static STATE: AtomicU64 = AtomicU64::new(0x193_667_6a_5e_7c_57); - if n <= 1 { - return 0; - } - let mut current = STATE.load(Ordering::Relaxed); - loop { - let new_val = current.wrapping_mul(6364136223846793005).wrapping_add(1); - match STATE.compare_exchange_weak(current, new_val, Ordering::Relaxed, Ordering::Relaxed) { - Ok(_) => return new_val % n, - Err(actual) => current = actual, - } - } -} - -fn is_retryable_error(err: &AgentError) -> bool { - let msg = err.to_string(); - msg.contains("connection refused") - || msg.contains("connection timed out") - || msg.contains("network error") - || msg.contains("dns error") - || msg.contains("error sending request") - || msg.contains("Http client error") - || msg.contains("rate_limit") - || msg.contains("rate limit") - || msg.contains("429") - || msg.contains("500") - || msg.contains("502") - || msg.contains("503") - || msg.contains("504") - || msg.contains("internal_server_error") - || msg.contains("service_unavailable") - || msg.contains("gateway_timeout") - || msg.contains("bad_gateway") -} - -static AI_METRICS: std::sync::OnceLock = std::sync::OnceLock::new(); - -fn ai_metrics() -> &'static AiMetrics { - AI_METRICS.get_or_init(AiMetrics::new) -} - -// ── Type conversions ───────────────────────────────────────────────────────── - -pub(crate) fn to_rig_message(msg: &ChatRequestMessage) -> RigMessage { - match msg.role.as_str() { - "system" => { - // System messages are handled via preamble(), not passed as messages. - // We still need to return a valid RigMessage variant. - RigMessage::user(msg.content.as_deref().unwrap_or("")) - } - "user" => RigMessage::user(msg.content.as_deref().unwrap_or("")), - "assistant" => { - let mut parts: Vec = Vec::new(); - if let Some(ref content) = msg.content { - if !content.is_empty() { - parts.push(AssistantContent::text(content)); - } - } - if let Some(ref tool_calls) = msg.tool_calls { - for tc in tool_calls { - // GLM may return empty tool call IDs — fall back to a generated UUID. - let id = if tc.id.is_empty() { - Uuid::new_v4().to_string() - } else { - tc.id.clone() - }; - parts.push(AssistantContent::tool_call_with_call_id( - &id, - id.clone(), - &tc.function.name, - serde_json::from_str(&tc.function.arguments) - .unwrap_or(serde_json::Value::Null), - )); - } - } - if parts.is_empty() { - RigMessage::assistant("") - } else if parts.len() == 1 { - // Single part — use simpler constructors - match parts.pop().unwrap() { - AssistantContent::Text(t) => RigMessage::assistant(t.text), - ac => RigMessage::Assistant { - id: None, - content: OneOrMany::one(ac), - }, - } - } else { - let content = OneOrMany::many(parts).expect("non-empty parts"); - RigMessage::Assistant { id: None, content } - } - } - "tool" | "function" => { - let id = msg.tool_call_id.as_deref().unwrap_or("unknown").to_string(); - let call_id = msg.tool_call_id.clone().or_else(|| Some(id.clone())); - let content = msg.content.as_deref().unwrap_or(""); - RigMessage::tool_result_with_call_id(id, call_id, content) - } - "developer" => { - // Developer role maps to user/system in rig - RigMessage::user(msg.content.as_deref().unwrap_or("")) - } - _ => RigMessage::user(msg.content.as_deref().unwrap_or("")), - } -} - -fn to_rig_tool_def(tool_json: &serde_json::Value) -> Option { - let name = tool_json - .get("function") - .and_then(|f| f.get("name")) - .and_then(|n| n.as_str())? - .to_string(); - - let description = tool_json - .get("function") - .and_then(|f| f.get("description")) - .and_then(|d| d.as_str()) - .map(|s| s.to_string()) - .unwrap_or_default(); - - let parameters = tool_json - .get("function") - .and_then(|f| f.get("parameters")) - .cloned() - .unwrap_or(serde_json::json!({})); - - Some(ToolDefinition { - name, - description, - parameters, - }) -} - -// ── Call helpers ───────────────────────────────────────────────────────────── - -async fn do_completion( - model: &M, - messages: &[ChatRequestMessage], - temperature: Option, - max_tokens: Option, - tools: Option<&[serde_json::Value]>, - tool_choice: Option<&str>, -) -> Result<(String, u64, u64, Vec, Vec)> -where - M: CompletionModel, -{ - let preamble = messages - .iter() - .find(|m| m.role == "system") - .and_then(|m| m.content.as_deref()) - .unwrap_or("") - .to_string(); - - let non_system: Vec = messages - .iter() - .filter(|m| m.role != "system") - .map(to_rig_message) - .collect(); - - let tool_defs: Vec = tools - .map(|ts| ts.iter().filter_map(to_rig_tool_def).collect()) - .unwrap_or_default(); - - let mut builder = model.completion_request(""); - - if !preamble.is_empty() { - builder = builder.preamble(preamble); - } - - if !non_system.is_empty() { - builder = builder.messages(non_system); - } - - if let Some(t) = temperature { - builder = builder.temperature(t); - } - - if let Some(mt) = max_tokens { - builder = builder.max_tokens(mt as u64); - } - - if !tool_defs.is_empty() { - builder = builder.tools(tool_defs); - } - - // Only set tool_choice when explicitly provided (mirrors call_stream_once logic) - if let Some(tc) = tool_choice { - match tc { - "none" => { - builder = builder.tool_choice(rig::completion::message::ToolChoice::None); - } - "auto" => { - builder = builder.tool_choice(rig::completion::message::ToolChoice::Auto); - } - s => { - builder = builder.tool_choice(rig::completion::message::ToolChoice::Specific { - function_names: vec![s.to_string()], - }); - } - } - } - - let response = builder - .send() - .await - .map_err(|e| AgentError::OpenAi(e.to_string()))?; - - let mut content = String::new(); - let mut tool_names: Vec = Vec::new(); - let mut tool_calls: Vec = Vec::new(); - for item in response.choice { - match item { - AssistantContent::Text(t) => { - content.push_str(&t.text); - } - AssistantContent::ToolCall(tc) => { - tool_names.push(tc.function.name.clone()); - tool_calls.push(ClientToolCall { - id: tc.id, - type_: "function".into(), - function: types::ToolCallFunction { - name: tc.function.name, - arguments: serde_json::to_string(&tc.function.arguments) - .unwrap_or_else(|_| "{}".to_string()), - }, - }); - } - AssistantContent::Reasoning(_) => {} - AssistantContent::Image(_) => {} - } - } - - let input_tokens = response.usage.input_tokens; - let output_tokens = response.usage.output_tokens; - - Ok((content, input_tokens, output_tokens, tool_calls, tool_names)) -} - -// ── Public API ─────────────────────────────────────────────────────────────── - -/// Call the AI model with automatic retry (no custom params). -pub async fn call_with_retry( - messages: &[ChatRequestMessage], - model_name: &str, - config: &AiClientConfig, - max_retries: Option, -) -> Result { - let client = config.build_rig_client(); - let model = client.completion_model(model_name); - let mut state = RetryState::new(max_retries.unwrap_or(3)); - - loop { - let start = Instant::now(); - - let result = do_completion(&model, messages, None, None, None, None).await; - - match result { - Ok((content, input_tokens, output_tokens, tool_calls, tool_names)) => { - let latency_ms = start.elapsed().as_millis() as i64; - let has_function_call = !tool_names.is_empty(); - ai_metrics().record_success( - input_tokens as i64, - output_tokens as i64, - has_function_call, - ); - return Ok(AiCallResponse { - content, - input_tokens: input_tokens as i64, - output_tokens: output_tokens as i64, - latency_ms, - tool_calls, - tool_calls_finished: tool_names, - }); - } - Err(ref err) if state.should_retry() && is_retryable_error(err) => { - let duration = state.backoff_duration(); - tracing::warn!( - attempt = state.attempt + 1, - max_retries = state.max_retries, - backoff_ms = duration.as_millis() as u64, - model = %model_name, - error = %err, - "ai_call_retry" - ); - tokio::time::sleep(duration).await; - state.next(); - } - Err(err) => { - ai_metrics().record_failure(); - return Err(err); - } - } - } -} - -/// Call with custom parameters (temperature, max_tokens, optional tools, optional tool_choice). -pub async fn call_with_params( - messages: &[ChatRequestMessage], - model_name: &str, - config: &AiClientConfig, - temperature: f32, - max_tokens: u32, - max_retries: Option, - tools: Option<&[serde_json::Value]>, - tool_choice: Option<&str>, -) -> Result { - let client = config.build_rig_client(); - let model = client.completion_model(model_name); - let mut state = RetryState::new(max_retries.unwrap_or(3)); - - loop { - let start = Instant::now(); - - let result = do_completion( - &model, - messages, - Some(temperature as f64), - Some(max_tokens), - tools, - tool_choice, - ) - .await; - - match result { - Ok((content, input_tokens, output_tokens, tool_calls, tool_names)) => { - let latency_ms = start.elapsed().as_millis() as i64; - let has_function_call = !tool_names.is_empty(); - ai_metrics().record_success( - input_tokens as i64, - output_tokens as i64, - has_function_call, - ); - return Ok(AiCallResponse { - content, - input_tokens: input_tokens as i64, - output_tokens: output_tokens as i64, - latency_ms, - tool_calls, - tool_calls_finished: tool_names, - }); - } - Err(ref err) if state.should_retry() && is_retryable_error(err) => { - let duration = state.backoff_duration(); - tracing::warn!( - attempt = state.attempt + 1, - max_retries = state.max_retries, - backoff_ms = duration.as_millis() as u64, - model = %model_name, - error = %err, - "ai_call_retry" - ); - tokio::time::sleep(duration).await; - state.next(); - } - Err(err) => { - ai_metrics().record_failure(); - return Err(err); - } - } - } -} - -/// A tool call extracted from streaming response with accumulated arguments. -#[derive(Debug, Clone)] -pub struct StreamedToolCall { - /// Tool call ID - pub id: String, - /// Tool function name - pub name: String, - /// Accumulated JSON arguments string - pub arguments: String, -} - -/// Type of chunk in the streaming response, preserving arrival order. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum StreamChunkType { - Thinking, - Answer, - ToolCall, - ToolResult, -} - -/// A single chunk from the streaming response in arrival order. -#[derive(Debug, Clone)] -pub struct StreamChunk { - pub chunk_type: StreamChunkType, - pub content: String, -} - -/// Streaming result from rig. -#[derive(Debug)] -pub struct StreamResponse { - pub content: String, - pub input_tokens: i64, - pub output_tokens: i64, - /// Accumulated reasoning/thinking text from the model. - pub reasoning_content: String, - /// Full tool calls with accumulated arguments (not just names) - pub tool_calls: Vec, - /// All chunks in arrival order — preserves think/answer/tool interleaving. - pub chunks: Vec, -} - -/// Async callback: takes a string delta and broadcasts it to the WebSocket. -/// The returned Future must be awaited by the caller. -pub type StreamTextCb = - Arc Pin + Send>> + Send + Sync>; -pub type StreamReasoningCb = - Arc Pin + Send>> + Send + Sync>; -pub type StreamToolCallCb = Arc< - dyn Fn(&StreamedToolCall) -> Pin + Send>> - + Send - + Sync, ->; - -/// Run a streaming chat completion with 60s timeout and 5 retries. -pub async fn call_stream( - messages: &[ChatRequestMessage], - model_name: &str, - config: &AiClientConfig, - temperature: f32, - max_tokens: u32, - tools: Option<&[serde_json::Value]>, - tool_choice: Option<&str>, - on_text_delta: StreamTextCb, - on_reasoning_delta: StreamReasoningCb, - on_tool_call: StreamToolCallCb, -) -> Result { - let mut state = RetryState::new(5); - - loop { - let result = call_stream_once( - messages, - model_name, - config, - temperature, - max_tokens, - tools, - tool_choice, - on_text_delta.clone(), - on_reasoning_delta.clone(), - on_tool_call.clone(), - ) - .await; - - match result { - Ok(response) => return Ok(response), - Err(ref err) if state.should_retry() && is_retryable_error(err) => { - let duration = state.backoff_duration(); - tracing::warn!( - attempt = state.attempt + 1, - max_retries = 5, - backoff_ms = duration.as_millis() as u64, - model = %model_name, - error = %err, - "ai_stream_retry" - ); - tokio::time::sleep(duration).await; - state.next(); - } - Err(err) => { - ai_metrics().record_failure(); - return Err(err); - } - } - } -} - -/// Single attempt of streaming completion with 60s timeout. -async fn call_stream_once( - messages: &[ChatRequestMessage], - model_name: &str, - config: &AiClientConfig, - temperature: f32, - max_tokens: u32, - tools: Option<&[serde_json::Value]>, - tool_choice: Option<&str>, - on_text_delta: StreamTextCb, - on_reasoning_delta: StreamReasoningCb, - on_tool_call: StreamToolCallCb, -) -> Result { - let client = config.build_rig_client(); - let model = client.completion_model(model_name); - - let preamble = messages - .iter() - .find(|m| m.role == "system") - .and_then(|m| m.content.as_deref()) - .unwrap_or("") - .to_string(); - - let non_system: Vec = messages - .iter() - .filter(|m| m.role != "system") - .map(to_rig_message) - .collect(); - - let tool_defs: Vec = tools - .map(|ts| ts.iter().filter_map(to_rig_tool_def).collect()) - .unwrap_or_default(); - - let mut builder = model - .completion_request("") - .temperature(temperature as f64) - .max_tokens(max_tokens as u64); - - if !preamble.is_empty() { - builder = builder.preamble(preamble); - } - if !non_system.is_empty() { - builder = builder.messages(non_system); - } - if !tool_defs.is_empty() { - builder = builder.tools(tool_defs); - } - - if let Some(tc) = tool_choice { - match tc { - "none" => { - builder = builder.tool_choice(rig::completion::message::ToolChoice::None); - } - "auto" => { - builder = builder.tool_choice(rig::completion::message::ToolChoice::Auto); - } - s => { - builder = builder.tool_choice(rig::completion::message::ToolChoice::Specific { - function_names: vec![s.to_string()], - }); - } - } - } - - let stream_fut = async { - let mut stream = builder - .stream() - .await - .map_err(|e| AgentError::OpenAi(e.to_string()))?; - - let mut content = String::new(); - let mut reasoning_content = String::new(); - let mut tool_calls: Vec = Vec::new(); - let mut chunks: Vec = Vec::new(); - - // Some models (e.g. GLM) ignore tool_choice="none" and still emit tool_calls. - // Filter them out so they don't cause spurious tool execution attempts. - let skip_tool_calls = tool_choice == Some("none"); - - use std::collections::HashMap; - let mut partial_tool_calls: HashMap = HashMap::new(); - let mut stream_finished = false; - - use rig::streaming::StreamedAssistantContent; - - while let Some(item) = stream.next().await { - match item { - Ok(StreamedAssistantContent::Text(text)) => { - content.push_str(&text.text); - on_text_delta(&text.text).await; - chunks.push(StreamChunk { - chunk_type: StreamChunkType::Answer, - content: text.text, - }); - } - Ok(StreamedAssistantContent::ToolCall { - tool_call, - internal_call_id, - }) => { - if skip_tool_calls { - partial_tool_calls.remove(&internal_call_id); - continue; - } - let arguments = match &tool_call.function.arguments { - serde_json::Value::String(s) => s.clone(), - other => serde_json::to_string(other).unwrap_or_else(|_| "{}".to_string()), - }; - let tc = StreamedToolCall { - id: tool_call.id.clone(), - name: tool_call.function.name.clone(), - arguments, - }; - on_tool_call(&tc).await; - chunks.push(StreamChunk { - chunk_type: StreamChunkType::ToolCall, - content: serde_json::json!({ - "id": tc.id, - "name": tc.name, - "arguments": tc.arguments, - }) - .to_string(), - }); - tool_calls.push(tc); - partial_tool_calls.remove(&internal_call_id); - } - Ok(StreamedAssistantContent::ToolCallDelta { - id, - internal_call_id, - content: delta_content, - }) => { - if skip_tool_calls { - continue; - } - use rig::streaming::ToolCallDeltaContent; - match delta_content { - ToolCallDeltaContent::Name(name) => { - partial_tool_calls.insert( - internal_call_id.clone(), - StreamedToolCall { - id: id.clone(), - name, - arguments: String::new(), - }, - ); - } - ToolCallDeltaContent::Delta(delta) => { - if let Some(tc) = partial_tool_calls.get_mut(&internal_call_id) { - tc.arguments.push_str(&delta); - } - } - } - } - Ok(StreamedAssistantContent::Reasoning(reasoning)) => { - for part in &reasoning.content { - if let rig::completion::message::ReasoningContent::Text { text, .. } = part - { - reasoning_content.push_str(text); - on_reasoning_delta(text).await; - chunks.push(StreamChunk { - chunk_type: StreamChunkType::Thinking, - content: text.clone(), - }); - } - } - } - Ok(StreamedAssistantContent::ReasoningDelta { reasoning, .. }) => { - reasoning_content.push_str(&reasoning); - on_reasoning_delta(&reasoning).await; - chunks.push(StreamChunk { - chunk_type: StreamChunkType::Thinking, - content: reasoning.clone(), - }); - } - Ok(StreamedAssistantContent::Final(response)) => { - stream_finished = true; - if !skip_tool_calls { - for (_, tc) in partial_tool_calls.drain() { - tool_calls.push(tc); - } - } else { - partial_tool_calls.drain(); - } - if let Some(usage) = response.token_usage() { - let in_toks = usage.input_tokens as i64; - let out_toks = usage.output_tokens as i64; - ai_metrics().record_success(in_toks, out_toks, !tool_calls.is_empty()); - return Ok(StreamResponse { - content, - reasoning_content, - input_tokens: in_toks, - output_tokens: out_toks, - tool_calls, - chunks, - }); - } - // Usage not available from Final — fall through to flush - } - Err(e) => return Err(AgentError::OpenAi(e.to_string())), - } - } - - // Flush any remaining partial tool calls (if stream ended without Final or Final had no usage) - if !stream_finished && !skip_tool_calls { - for (_, tc) in partial_tool_calls.drain() { - tool_calls.push(tc); - } - } - ai_metrics().record_success(0, 0, !tool_calls.is_empty()); - Ok(StreamResponse { - content, - reasoning_content, - input_tokens: 0, - output_tokens: 0, - tool_calls, - chunks, - }) - }; - - // 120s timeout for the entire stream - match tokio::time::timeout(std::time::Duration::from_secs(120), stream_fut).await { - Ok(result) => result, - Err(_) => Err(AgentError::Timeout { - task_id: 0, - seconds: 120, - }), - } -} diff --git a/libs/agent/client/types.rs b/libs/agent/client/types.rs deleted file mode 100644 index b3554fe..0000000 --- a/libs/agent/client/types.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! Internal message types for OpenAI-compatible chat completion API. -//! -//! Uses plain structs with `role: String` instead of an enum — easier to serialize, -//! and the downstream code only constructs specific variants anyway. - -use serde::{Deserialize, Serialize}; - -/// A message in a chat completion request. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChatRequestMessage { - /// One of "system", "user", "assistant", "tool", "developer", "function" - pub role: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub content: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - /// Required for "tool" role messages - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_call_id: Option, - /// Tool calls for "assistant" role messages - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_calls: Option>, -} - -impl ChatRequestMessage { - pub fn system(content: impl Into) -> Self { - Self { - role: "system".into(), - content: Some(content.into()), - name: None, - tool_call_id: None, - tool_calls: None, - } - } - - pub fn user(content: impl Into) -> Self { - Self { - role: "user".into(), - content: Some(content.into()), - name: None, - tool_call_id: None, - tool_calls: None, - } - } - - pub fn assistant(content: Option, tool_calls: Option>) -> Self { - Self { - role: "assistant".into(), - content, - name: None, - tool_call_id: None, - tool_calls, - } - } - - pub fn tool(tool_call_id: impl Into, content: impl Into) -> Self { - Self { - role: "tool".into(), - content: Some(content.into()), - name: None, - tool_call_id: Some(tool_call_id.into()), - tool_calls: None, - } - } - - pub fn with_name(mut self, name: impl Into) -> Self { - self.name = Some(name.into()); - self - } - - pub fn developer(content: impl Into) -> Self { - Self { - role: "developer".into(), - content: Some(content.into()), - name: None, - tool_call_id: None, - tool_calls: None, - } - } - - /// Creates a function/assistant message with tool_calls (used to record the AI's tool call). - pub fn with_tool_calls(mut self, tool_calls: Vec) -> Self { - self.tool_calls = Some(tool_calls); - self - } -} - -/// A tool call within an assistant message. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ToolCall { - pub id: String, - #[serde(rename = "type")] - pub type_: String, - pub function: ToolCallFunction, -} - -/// Function details within a tool call. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ToolCallFunction { - pub name: String, - pub arguments: String, -} - -/// Chat completion request body (serialized to JSON for the HTTP API). -#[derive(Debug, Clone, Serialize)] -pub struct ChatCompletionRequest { - pub model: String, - pub messages: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub temperature: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_completion_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub top_p: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub frequency_penalty: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub presence_penalty: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub stream: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub reasoning_effort: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tools: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, -} - -impl ChatCompletionRequest { - pub fn with_stream(mut self) -> Self { - self.stream = Some(true); - self - } -} - -/// Reasoning effort level for supported models. -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum ReasoningEffort { - High, -} - -// ── Response types (non-streaming) ── - -/// Chat completion response (non-streaming). Deserialize-only from the API JSON. -#[derive(Debug, Clone, Deserialize)] -pub struct ChatCompletionResponse { - #[serde(default)] - pub id: Option, - #[serde(default)] - pub model: Option, - pub choices: Vec, - #[serde(default)] - pub usage: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Choice { - pub index: u32, - pub message: ResponseMessage, - pub finish_reason: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ResponseMessage { - pub role: Option, - pub content: Option, - #[serde(default)] - pub tool_calls: Option>, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ResponseToolCall { - pub id: String, - #[serde(rename = "type")] - pub type_: String, - pub function: ResponseToolCallFunction, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct ResponseToolCallFunction { - pub name: String, - pub arguments: String, -} - -/// Token usage from the response. -#[derive(Debug, Clone, Deserialize)] -pub struct Usage { - #[serde(rename = "prompt_tokens", alias = "input_tokens")] - pub prompt_tokens: u64, - #[serde(rename = "completion_tokens", alias = "output_tokens")] - pub completion_tokens: u64, -} - -// ── Streaming types ── - -/// A chunk from a streaming chat completion (SSE `data:` lines). -#[derive(Debug, Clone, Deserialize)] -pub struct StreamChunk { - #[serde(default)] - pub id: Option, - #[serde(default)] - pub model: Option, - pub choices: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct StreamChoice { - pub delta: Delta, - pub finish_reason: Option, - pub index: u32, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct Delta { - #[serde(default)] - pub role: Option, - #[serde(default)] - pub content: Option, - #[serde(default)] - pub tool_calls: Option>, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct StreamToolCall { - pub index: u32, - #[serde(default)] - pub id: Option, - #[serde(rename = "type", default)] - pub type_: Option, - pub function: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct StreamToolCallFunction { - #[serde(default)] - pub name: Option, - #[serde(default)] - pub arguments: Option, -} diff --git a/libs/agent/compact/auth_fetch.rs b/libs/agent/compact/auth_fetch.rs deleted file mode 100644 index 625d851..0000000 --- a/libs/agent/compact/auth_fetch.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::AgentError; -use models::Expr; -use models::rooms::room_message::{ - Column as RmCol, Entity as RoomMessage, Model as RoomMessageModel, -}; -use sea_orm::*; - -impl super::CompactService { - pub async fn fetch_room_messages_secure( - &self, - room_id: uuid::Uuid, - requester_id: uuid::Uuid, - ) -> Result, AgentError> { - use models::rooms::{RoomAccess, RoomUserState}; - - RoomMessage::find() - .filter(RmCol::Room.eq(room_id)) - .filter( - Condition::any() - .add(Expr::exists( - RoomUserState::find() - .filter(models::rooms::room_user_state::Column::Room.eq(room_id)) - .filter(models::rooms::room_user_state::Column::User.eq(requester_id)) - .into_query(), - )) - .add(Expr::exists( - RoomAccess::find() - .filter(models::rooms::room_access::Column::Room.eq(room_id)) - .filter(models::rooms::room_access::Column::User.eq(requester_id)) - .into_query(), - )), - ) - .order_by_asc(RmCol::Seq) - .limit(10000) - .all(&self.db) - .await - .map_err(|e| AgentError::Internal(e.to_string())) - } -} diff --git a/libs/agent/compact/helpers.rs b/libs/agent/compact/helpers.rs deleted file mode 100644 index 80b8091..0000000 --- a/libs/agent/compact/helpers.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::types::{CompactSummary, MessageSummary}; - -pub fn messages_to_text( - messages: &[models::rooms::room_message::Model], - sender_mapper: F, -) -> String -where - F: Fn(&models::rooms::room_message::Model) -> String, -{ - messages - .iter() - .map(|m| { - let sender = sender_mapper(m); - format!("[{}] {}: {}", m.send_at, sender, m.content) - }) - .collect::>() - .join("\n") -} - -pub fn retained_as_text(retained: &[MessageSummary]) -> String { - retained - .iter() - .map(|m| format!("[{}] {}: {}", m.send_at, m.sender_name, m.content)) - .collect::>() - .join("\n") -} - -pub fn summary_content(summary: &CompactSummary) -> String { - if summary.summary.is_empty() { - format!( - "## Recent conversation ({} messages)\n\n{}", - summary.retained.len(), - retained_as_text(&summary.retained) - ) - } else { - format!( - "## Earlier conversation ({} messages summarised)\n{}\n\n\ - ## Most recent {} messages\n\n{}", - summary.messages_compressed, - summary.summary, - summary.retained.len(), - retained_as_text(&summary.retained) - ) - } -} diff --git a/libs/agent/compact/mod.rs b/libs/agent/compact/mod.rs deleted file mode 100644 index 1916c15..0000000 --- a/libs/agent/compact/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Context compaction for AI sessions and room message history. - -pub mod auth_fetch; -pub mod helpers; -pub mod room_compactor; -pub mod summarizer; -pub mod types; - -use sea_orm::DatabaseConnection; - -pub use types::{ - CompactConfig, CompactLevel, CompactSummary, MessageSummary, RoomCompactContext, - RoomCompactRecord, ThresholdResult, -}; - -#[derive(Clone)] -pub struct CompactService { - db: DatabaseConnection, - ai_client_config: crate::client::AiClientConfig, - model: String, - model_context_limit: Option, -} - -impl CompactService { - pub fn new( - db: DatabaseConnection, - ai_client_config: crate::client::AiClientConfig, - model: String, - ) -> Self { - Self { - db, - ai_client_config, - model, - model_context_limit: None, - } - } - - pub fn for_model(&self, model: impl Into) -> Self { - Self { - db: self.db.clone(), - ai_client_config: self.ai_client_config.clone(), - model: model.into(), - model_context_limit: self.model_context_limit, - } - } - - pub fn with_model_context_limit(mut self, model_context_limit: Option) -> Self { - self.model_context_limit = model_context_limit.filter(|limit| *limit > 0); - self - } - - pub fn for_model_entry(&self, model: &models::agents::model::Model) -> Self { - self.for_model(model.name.clone()) - .with_model_context_limit(Some(model.context_length.max(0) as usize)) - } -} diff --git a/libs/agent/compact/room_compactor.rs b/libs/agent/compact/room_compactor.rs deleted file mode 100644 index b7470a8..0000000 --- a/libs/agent/compact/room_compactor.rs +++ /dev/null @@ -1,422 +0,0 @@ -use models::rooms::room_message::{ - Column as RmCol, Entity as RoomMessage, Model as RoomMessageModel, -}; -use sea_orm::ColumnTrait; -use sea_orm::{ConnectionTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect}; - -use crate::compact::types::{CompactConfig, CompactLevel, RoomCompactContext, RoomCompactRecord}; -use crate::tokent::resolve_usage; -use crate::{AgentError, CompactSummary, MessageSummary}; - -impl super::CompactService { - pub async fn latest_room_compact_record( - &self, - room_id: uuid::Uuid, - ) -> Result, AgentError> { - let stmt = sea_orm::Statement::from_sql_and_values( - sea_orm::DbBackend::Postgres, - "SELECT id, room, from_seq, to_seq, summary, message_count, source_message_ids, created_at \ - FROM room_compact_summary WHERE room = $1 ORDER BY to_seq DESC, created_at DESC LIMIT 1", - vec![room_id.into()], - ); - let Some(row) = self - .db - .query_one_raw(stmt) - .await - .map_err(|e| AgentError::Internal(e.to_string()))? - else { - return Ok(None); - }; - - let source_json: serde_json::Value = row - .try_get("", "source_message_ids") - .map_err(|e| AgentError::Internal(e.to_string()))?; - let source_message_ids = source_json - .as_array() - .map(|ids| { - ids.iter() - .filter_map(|v| v.as_str()) - .filter_map(|s| uuid::Uuid::parse_str(s).ok()) - .collect::>() - }) - .unwrap_or_default(); - - Ok(Some(RoomCompactRecord { - id: row - .try_get("", "id") - .map_err(|e| AgentError::Internal(e.to_string()))?, - room_id: row - .try_get("", "room") - .map_err(|e| AgentError::Internal(e.to_string()))?, - from_seq: row - .try_get("", "from_seq") - .map_err(|e| AgentError::Internal(e.to_string()))?, - to_seq: row - .try_get("", "to_seq") - .map_err(|e| AgentError::Internal(e.to_string()))?, - summary: row - .try_get("", "summary") - .map_err(|e| AgentError::Internal(e.to_string()))?, - message_count: row - .try_get("", "message_count") - .map_err(|e| AgentError::Internal(e.to_string()))?, - source_message_ids, - created_at: row - .try_get("", "created_at") - .map_err(|e| AgentError::Internal(e.to_string()))?, - })) - } - - async fn insert_room_compact_record( - &self, - room_id: uuid::Uuid, - from_seq: i64, - to_seq: i64, - summary: &str, - source_message_ids: &[uuid::Uuid], - ) -> Result { - let id = uuid::Uuid::new_v4(); - let now = chrono::Utc::now(); - let source_json = serde_json::Value::Array( - source_message_ids - .iter() - .map(|id| serde_json::Value::String(id.to_string())) - .collect(), - ); - let stmt = sea_orm::Statement::from_sql_and_values( - sea_orm::DbBackend::Postgres, - "INSERT INTO room_compact_summary \ - (id, room, from_seq, to_seq, summary, message_count, source_message_ids, created_at, updated_at) \ - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", - vec![ - id.into(), - room_id.into(), - from_seq.into(), - to_seq.into(), - summary.to_string().into(), - (source_message_ids.len() as i32).into(), - source_json.into(), - now.into(), - now.into(), - ], - ); - self.db - .execute_raw(stmt) - .await - .map_err(|e| AgentError::Internal(e.to_string()))?; - - Ok(RoomCompactRecord { - id, - room_id, - from_seq, - to_seq, - summary: summary.to_string(), - message_count: source_message_ids.len() as i32, - source_message_ids: source_message_ids.to_vec(), - created_at: now, - }) - } - - fn clean_dedupe_sort_messages(mut messages: Vec) -> Vec { - messages.retain(|m| { - m.revoked.is_none() - && !m.content.trim().is_empty() - && matches!(m.content_type, models::rooms::MessageContentType::Text) - }); - messages.sort_by_key(|m| (m.seq, m.send_at)); - - let mut seen = std::collections::HashSet::new(); - messages - .into_iter() - .filter(|m| { - let normalized = m - .content - .split_whitespace() - .collect::>() - .join(" ") - .to_lowercase(); - let key = format!("{}:{:?}:{}", m.sender_type, m.sender_id, normalized); - seen.insert(key) - }) - .collect() - } - - fn resolve_retain_count(config: CompactConfig, estimated_tokens: usize) -> usize { - let level = if config.auto_level { - CompactLevel::auto_select(estimated_tokens, config.token_threshold) - } else { - config.default_level - }; - level.retain_count() - } - - pub async fn prepare_room_compact_context( - &self, - room_id: uuid::Uuid, - requester_id: uuid::Uuid, - user_names: Option>, - config: CompactConfig, - ) -> Result { - let latest = self.latest_room_compact_record(room_id).await?; - let cutoff_seq = latest.as_ref().map(|r| r.to_seq); - let previous_summary = latest.as_ref().map(|r| r.summary.as_str()); - - let messages = self - .fetch_room_messages_secure(room_id, requester_id) - .await?; - let messages = messages - .into_iter() - .filter(|m| cutoff_seq.map(|seq| m.seq > seq).unwrap_or(true)) - .collect::>(); - let messages = Self::clean_dedupe_sort_messages(messages); - - let user_ids: Vec = messages - .iter() - .filter_map(|m| m.sender_id) - .collect::>() - .into_iter() - .collect(); - let user_name_map = match user_names { - Some(map) => map, - None => self.get_user_name_map(&user_ids).await?, - }; - - let sender_mapper = |m: &RoomMessageModel| { - if let Some(user_id) = m.sender_id { - if let Some(username) = user_name_map.get(&user_id) { - return username.clone(); - } - } - m.sender_type.to_string() - }; - let incremental_text = crate::compact::helpers::messages_to_text(&messages, sender_mapper); - let estimate_input = match previous_summary { - Some(summary) if !summary.is_empty() => format!("{}\n{}", summary, incremental_text), - _ => incremental_text.clone(), - }; - let estimated_tokens = crate::tokent::count_message_text(&estimate_input, &self.model) - .unwrap_or_else(|_| estimate_input.len() / 4); - - let retain_count = Self::resolve_retain_count(config, estimated_tokens); - if estimated_tokens >= config.token_threshold && messages.len() > retain_count { - let split_index = messages.len().saturating_sub(retain_count); - let (to_summarize, retained_messages) = messages.split_at(split_index); - let from_seq = to_summarize - .first() - .map(|m| m.seq) - .unwrap_or(cutoff_seq.unwrap_or(0) + 1); - let to_seq = to_summarize.last().map(|m| m.seq).unwrap_or(from_seq); - let source_ids: Vec = to_summarize.iter().map(|m| m.id).collect(); - let (summary, _usage) = self - .summarize_room_increment(previous_summary, to_summarize, config.max_summary_tokens) - .await?; - let record = self - .insert_room_compact_record(room_id, from_seq, to_seq, &summary, &source_ids) - .await?; - let retained = retained_messages - .iter() - .map(|m| Self::message_to_summary(m, &user_name_map)) - .collect(); - return Ok(RoomCompactContext { - room_id, - cutoff_seq: Some(record.to_seq), - summary: Some(record.summary), - retained, - estimated_tokens, - compacted: true, - }); - } - - let retained = messages - .iter() - .rev() - .take(50) - .collect::>() - .into_iter() - .rev() - .map(|m| Self::message_to_summary(m, &user_name_map)) - .collect(); - - Ok(RoomCompactContext { - room_id, - cutoff_seq, - summary: latest.map(|r| r.summary), - retained, - estimated_tokens, - compacted: false, - }) - } - - pub async fn compact_room( - &self, - room_id: uuid::Uuid, - level: CompactLevel, - user_names: Option>, - requester_id: uuid::Uuid, - context_window_tokens: i32, - compaction_max_summary_ratio: f32, - ) -> Result { - let messages = self - .fetch_room_messages_secure(room_id, requester_id) - .await?; - - if messages.is_empty() { - let room_exists = models::rooms::room::Entity::find_by_id(room_id) - .one(&self.db) - .await - .map_err(|e| AgentError::Internal(e.to_string()))? - .is_some(); - - if room_exists { - return Err(AgentError::Internal("Access denied or room empty".into())); - } else { - return Err(AgentError::Internal("Room not found".into())); - } - } - - let user_ids: Vec = messages - .iter() - .filter_map(|m| m.sender_id) - .collect::>() - .into_iter() - .collect(); - let user_name_map = match user_names { - Some(map) => map, - None => self.get_user_name_map(&user_ids).await?, - }; - - if messages.len() <= level.retain_count() { - let retained: Vec = messages - .iter() - .map(|m| Self::message_to_summary(m, &user_name_map)) - .collect(); - return Ok(CompactSummary { - session_id: uuid::Uuid::new_v4(), - room_id, - retained, - summary: String::new(), - compacted_at: chrono::Utc::now(), - messages_compressed: 0, - usage: None, - }); - } - - let retain_count = level.retain_count(); - let split_index = messages.len().saturating_sub(retain_count); - let (to_summarize, retained_messages) = messages.split_at(split_index); - - let retained: Vec = retained_messages - .iter() - .map(|m| Self::message_to_summary(m, &user_name_map)) - .collect(); - - let max_summary_tokens = CompactConfig::summary_token_budget( - context_window_tokens.max(0) as usize, - compaction_max_summary_ratio, - ); - - let (summary, remote_usage) = self - .summarize_messages(to_summarize, max_summary_tokens) - .await?; - - let summarized_text = to_summarize - .iter() - .map(|m| m.content.as_str()) - .collect::>() - .join("\n"); - let usage = resolve_usage(remote_usage, &self.model, &summarized_text, &summary); - - Ok(CompactSummary { - session_id: uuid::Uuid::new_v4(), - room_id, - retained, - summary, - compacted_at: chrono::Utc::now(), - messages_compressed: to_summarize.len(), - usage: Some(usage), - }) - } - - pub async fn compact_session( - &self, - session_id: uuid::Uuid, - level: CompactLevel, - user_names: Option>, - context_window_tokens: i32, - compaction_max_summary_ratio: f32, - ) -> Result { - let messages: Vec = RoomMessage::find() - .filter(RmCol::Room.eq(session_id)) - .order_by_asc(RmCol::Seq) - .limit(10000) - .all(&self.db) - .await - .map_err(|e| AgentError::Internal(e.to_string()))?; - - if messages.is_empty() { - return Err(AgentError::Internal("session has no messages".into())); - } - - let user_ids: Vec = messages - .iter() - .filter_map(|m| m.sender_id) - .collect::>() - .into_iter() - .collect(); - let user_name_map = match user_names { - Some(map) => map, - None => self.get_user_name_map(&user_ids).await?, - }; - - if messages.len() <= level.retain_count() { - let retained: Vec = messages - .iter() - .map(|m| Self::message_to_summary(m, &user_name_map)) - .collect(); - return Ok(CompactSummary { - session_id, - room_id: uuid::Uuid::nil(), - retained, - summary: String::new(), - compacted_at: chrono::Utc::now(), - messages_compressed: 0, - usage: None, - }); - } - - let retain_count = level.retain_count(); - let split_index = messages.len().saturating_sub(retain_count); - let (to_summarize, retained_messages) = messages.split_at(split_index); - - let retained: Vec = retained_messages - .iter() - .map(|m| Self::message_to_summary(m, &user_name_map)) - .collect(); - - let max_summary_tokens = CompactConfig::summary_token_budget( - context_window_tokens.max(0) as usize, - compaction_max_summary_ratio, - ); - - let (summary, remote_usage) = self - .summarize_messages(to_summarize, max_summary_tokens) - .await?; - - let summarized_text = to_summarize - .iter() - .map(|m| m.content.as_str()) - .collect::>() - .join("\n"); - let usage = resolve_usage(remote_usage, &self.model, &summarized_text, &summary); - - Ok(CompactSummary { - session_id, - room_id: uuid::Uuid::nil(), - retained, - summary, - compacted_at: chrono::Utc::now(), - messages_compressed: to_summarize.len(), - usage: Some(usage), - }) - } -} diff --git a/libs/agent/compact/summarizer.rs b/libs/agent/compact/summarizer.rs deleted file mode 100644 index 686866c..0000000 --- a/libs/agent/compact/summarizer.rs +++ /dev/null @@ -1,513 +0,0 @@ -use models::rooms::room_message::Model as RoomMessageModel; -use models::users::user::{Column as UserCol, Entity as User}; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; - -use crate::AgentError; -use crate::client::call_with_params; -use crate::client::types::ChatRequestMessage; -use crate::compact::types::{CompactConfig, MessageSummary}; -use crate::tokent::{TokenUsage, count_message_text}; - -const DEFAULT_MODEL_CONTEXT_LIMIT: usize = 128_000; -const MODEL_INPUT_RATIO_NUMERATOR: usize = 85; -const MODEL_INPUT_RATIO_DENOMINATOR: usize = 100; -const MIN_ROUND_SUMMARY_TOKENS: usize = 64; - -#[derive(Clone, Copy)] -enum SummaryKind { - Conversation, - RoomIncrement, -} - -impl super::CompactService { - pub async fn summarize_room_increment( - &self, - previous_summary: Option<&str>, - messages: &[RoomMessageModel], - max_summary_tokens: usize, - ) -> Result<(String, Option), AgentError> { - let user_ids: Vec = messages - .iter() - .filter_map(|m| m.sender_id) - .collect::>() - .into_iter() - .collect(); - - let user_name_map = self.get_user_name_map(&user_ids).await?; - let blocks = messages - .iter() - .map(|m| { - let sender = if let Some(user_id) = m.sender_id { - user_name_map - .get(&user_id) - .cloned() - .unwrap_or_else(|| m.sender_type.to_string()) - } else { - m.sender_type.to_string() - }; - format!("[{}] {}: {}", m.send_at, sender, m.content) - }) - .collect::>(); - - self.summarize_blocks_with_optional_previous( - blocks, - previous_summary, - max_summary_tokens, - SummaryKind::RoomIncrement, - ) - .await - } - - pub async fn summarize_messages( - &self, - messages: &[RoomMessageModel], - max_summary_tokens: usize, - ) -> Result<(String, Option), AgentError> { - let user_ids: Vec = messages - .iter() - .filter_map(|m| m.sender_id) - .collect::>() - .into_iter() - .collect(); - - let user_name_map = self.get_user_name_map(&user_ids).await?; - let blocks = messages - .iter() - .map(|m| { - let sender = if let Some(user_id) = m.sender_id { - user_name_map - .get(&user_id) - .cloned() - .unwrap_or_else(|| m.sender_type.to_string()) - } else { - m.sender_type.to_string() - }; - format!("[{}] {}: {}", m.send_at, sender, m.content) - }) - .collect::>(); - - self.summarize_blocks_with_optional_previous( - blocks, - None, - max_summary_tokens, - SummaryKind::Conversation, - ) - .await - } - - pub fn message_to_summary( - m: &RoomMessageModel, - user_name_map: &std::collections::HashMap, - ) -> MessageSummary { - let sender_name = if let Some(user_id) = m.sender_id { - user_name_map - .get(&user_id) - .cloned() - .unwrap_or_else(|| m.sender_type.to_string()) - } else { - m.sender_type.to_string() - }; - MessageSummary { - id: m.id, - sender_type: m.sender_type.clone(), - sender_id: m.sender_id, - sender_name, - content: m.content.clone(), - content_type: m.content_type.clone(), - tool_call_id: None, - send_at: m.send_at, - } - } - - pub async fn get_user_name_map( - &self, - user_ids: &[uuid::Uuid], - ) -> Result, AgentError> { - use std::collections::HashMap; - let mut map = HashMap::new(); - if !user_ids.is_empty() { - let users = User::find() - .filter(UserCol::Uid.is_in(user_ids.to_vec())) - .all(&self.db) - .await - .map_err(|e| AgentError::Internal(e.to_string()))?; - for user in users { - map.insert(user.uid, user.username); - } - } - Ok(map) - } - - async fn summarize_blocks_with_optional_previous( - &self, - blocks: Vec, - previous_summary: Option<&str>, - max_summary_tokens: usize, - kind: SummaryKind, - ) -> Result<(String, Option), AgentError> { - let final_budget = Self::final_summary_budget(max_summary_tokens); - let input_budget = self.safe_model_input_budget(); - let round_budget = Self::round_summary_budget(final_budget, input_budget); - let mut total_usage = TokenUsage::default(); - let mut has_usage = false; - - let fitted_chunks = - self.split_blocks_to_fit(blocks, input_budget, round_budget, kind, false)?; - - let mut partial_summaries = Vec::new(); - for chunk in fitted_chunks { - let prompt = self.build_prompt(kind, false, &chunk, round_budget); - let (summary, usage) = self - .invoke_summary_prompt(&prompt, round_budget, Self::temperature_for(kind)) - .await?; - Self::accumulate_usage(&mut total_usage, &mut has_usage, usage); - partial_summaries.push(summary); - } - - if let Some(previous) = previous_summary - .map(str::trim) - .filter(|summary| !summary.is_empty()) - { - partial_summaries.insert(0, previous.to_string()); - } - - if partial_summaries.is_empty() { - return Ok((String::new(), None)); - } - - if partial_summaries.len() == 1 && previous_summary.is_none() { - return Ok(( - partial_summaries.remove(0), - if has_usage { Some(total_usage) } else { None }, - )); - } - - let final_summary = self - .merge_summary_rounds( - partial_summaries, - final_budget, - round_budget, - kind, - &mut total_usage, - &mut has_usage, - ) - .await?; - - Ok(( - final_summary, - if has_usage { Some(total_usage) } else { None }, - )) - } - - async fn merge_summary_rounds( - &self, - mut summaries: Vec, - final_budget: usize, - round_budget: usize, - kind: SummaryKind, - total_usage: &mut TokenUsage, - has_usage: &mut bool, - ) -> Result { - let input_budget = self.safe_model_input_budget(); - - while summaries.len() > 1 { - let current_budget = if summaries.len() <= 2 { - final_budget - } else { - round_budget - }; - let mut next_round = Vec::new(); - let mut idx = 0usize; - - while idx < summaries.len() { - if idx + 1 >= summaries.len() { - next_round.push(summaries[idx].clone()); - idx += 1; - continue; - } - - let pair = vec![summaries[idx].clone(), summaries[idx + 1].clone()]; - let fitted_pairs = - self.split_blocks_to_fit(pair, input_budget, current_budget, kind, true)?; - - for pair_text in fitted_pairs { - let prompt = self.build_prompt(kind, true, &pair_text, current_budget); - let (summary, usage) = self - .invoke_summary_prompt(&prompt, current_budget, Self::temperature_for(kind)) - .await?; - Self::accumulate_usage(total_usage, has_usage, usage); - next_round.push(summary); - } - idx += 2; - } - - summaries = next_round; - } - - summaries - .pop() - .ok_or_else(|| AgentError::Internal("summary merge produced no output".into())) - } - - async fn invoke_summary_prompt( - &self, - prompt: &str, - max_summary_tokens: usize, - temperature: f32, - ) -> Result<(String, Option), AgentError> { - let response = call_with_params( - &[ChatRequestMessage::user(prompt.to_string())], - &self.model, - &self.ai_client_config, - temperature, - max_summary_tokens as u32, - None, - None, - None, - ) - .await - .map_err(|e| AgentError::OpenAi(e.to_string()))?; - - let usage = - TokenUsage::from_remote(response.input_tokens as u32, response.output_tokens as u32); - Ok((response.content, usage)) - } - - fn split_blocks_to_fit( - &self, - blocks: Vec, - input_budget: usize, - max_summary_tokens: usize, - kind: SummaryKind, - is_merge: bool, - ) -> Result, AgentError> { - let mut chunks = Vec::new(); - self.collect_fitting_chunks( - blocks, - input_budget, - max_summary_tokens, - kind, - is_merge, - &mut chunks, - )?; - Ok(chunks) - } - - fn collect_fitting_chunks( - &self, - blocks: Vec, - input_budget: usize, - max_summary_tokens: usize, - kind: SummaryKind, - is_merge: bool, - chunks: &mut Vec, - ) -> Result<(), AgentError> { - let body = Self::join_blocks(&blocks, is_merge); - let prompt = self.build_prompt(kind, is_merge, &body, max_summary_tokens); - if self.estimate_tokens(&prompt) <= input_budget { - chunks.push(body); - return Ok(()); - } - - if blocks.len() > 1 { - let mid = blocks.len() / 2; - self.collect_fitting_chunks( - blocks[..mid].to_vec(), - input_budget, - max_summary_tokens, - kind, - is_merge, - chunks, - )?; - self.collect_fitting_chunks( - blocks[mid..].to_vec(), - input_budget, - max_summary_tokens, - kind, - is_merge, - chunks, - )?; - return Ok(()); - } - - let single = blocks - .into_iter() - .next() - .ok_or_else(|| AgentError::Internal("cannot split empty summary block".into()))?; - let (left, right) = Self::split_text_in_half(&single)?; - self.collect_fitting_chunks( - vec![left], - input_budget, - max_summary_tokens, - kind, - is_merge, - chunks, - )?; - self.collect_fitting_chunks( - vec![right], - input_budget, - max_summary_tokens, - kind, - is_merge, - chunks, - )?; - Ok(()) - } - - fn build_prompt( - &self, - kind: SummaryKind, - is_merge: bool, - body: &str, - max_summary_tokens: usize, - ) -> String { - match (kind, is_merge) { - (SummaryKind::Conversation, false) => format!( - "Summarise the following conversation concisely, preserving all key facts, \ - decisions, and any pending or in-progress work. \ - The summary MUST NOT exceed {} tokens. \ - Use this format:\n\n\ - **Summary:** \n\ - **Key decisions:** \n\ - **Open items:** \n\n\ - Conversation:\n\n{}", - max_summary_tokens, body - ), - (SummaryKind::Conversation, true) => format!( - "Merge the following partial conversation summaries into a single concise summary. \ - Deduplicate overlap, preserve chronology, and keep all concrete decisions, \ - status updates, and unresolved work. The summary MUST NOT exceed {} tokens. \ - Use this format:\n\n\ - **Summary:** \n\ - **Key decisions:** \n\ - **Open items:** \n\n\ - Partial summaries:\n\n{}", - max_summary_tokens, body - ), - (SummaryKind::RoomIncrement, false) => format!( - "Create an incremental room summary from the new messages below. \ - Deduplicate repeated messages, clean noise, keep chronological order, and preserve \ - decisions, facts, assignments/owners, unresolved questions, and concrete next steps. \ - The result MUST NOT exceed {} tokens.\n\n\ - Format:\n\ - **Summary:** \n\ - **Decisions:** \n\ - **Owners:** task or 'none'>\n\ - **Open items:** \n\n\ - New messages:\n\n{}", - max_summary_tokens, body - ), - (SummaryKind::RoomIncrement, true) => format!( - "Merge the following partial room summaries into one room summary. Deduplicate overlap, \ - keep chronology, preserve decisions, facts, assignments/owners, unresolved questions, \ - and concrete next steps. The result MUST NOT exceed {} tokens.\n\n\ - Format:\n\ - **Summary:** \n\ - **Decisions:** \n\ - **Owners:** task or 'none'>\n\ - **Open items:** \n\n\ - Partial summaries:\n\n{}", - max_summary_tokens, body - ), - } - } - - fn join_blocks(blocks: &[String], is_merge: bool) -> String { - if is_merge { - blocks - .iter() - .enumerate() - .map(|(index, block)| format!("### Partial Summary {}\n{}", index + 1, block)) - .collect::>() - .join("\n\n") - } else { - blocks.join("\n") - } - } - - fn split_text_in_half(text: &str) -> Result<(String, String), AgentError> { - if text.chars().count() < 2 { - return Err(AgentError::Internal( - "single summary block exceeds input budget and cannot be split".into(), - )); - } - - let midpoint = text.len() / 2; - let mut split_at = text.floor_char_boundary(midpoint); - if split_at == 0 || split_at >= text.len() { - split_at = text.ceil_char_boundary(midpoint); - } - if split_at == 0 || split_at >= text.len() { - return Err(AgentError::Internal( - "failed to split oversized summary block".into(), - )); - } - - Ok((text[..split_at].to_string(), text[split_at..].to_string())) - } - - fn estimate_tokens(&self, text: &str) -> usize { - count_message_text(text, &self.model).unwrap_or_else(|_| (text.len() / 4).max(1)) - } - - fn safe_model_input_budget(&self) -> usize { - Self::safe_model_input_budget_from_limit(self.model_context_limit) - } - - fn final_summary_budget(max_summary_tokens: usize) -> usize { - max_summary_tokens.clamp( - CompactConfig::MIN_SUMMARY_TOKENS, - CompactConfig::MAX_SUMMARY_TOKENS, - ) - } - - fn round_summary_budget(final_budget: usize, input_budget: usize) -> usize { - final_budget.min((input_budget / 8).max(MIN_ROUND_SUMMARY_TOKENS)) - } - - fn temperature_for(kind: SummaryKind) -> f32 { - match kind { - SummaryKind::Conversation => 0.3, - SummaryKind::RoomIncrement => 0.2, - } - } - - fn safe_model_input_budget_from_limit(model_context_limit: Option) -> usize { - let context_limit = model_context_limit - .unwrap_or(DEFAULT_MODEL_CONTEXT_LIMIT) - .max(1); - context_limit - .saturating_mul(MODEL_INPUT_RATIO_NUMERATOR) - .saturating_div(MODEL_INPUT_RATIO_DENOMINATOR) - .max(1) - } - - fn accumulate_usage(total: &mut TokenUsage, has_usage: &mut bool, usage: Option) { - if let Some(usage) = usage { - total.input_tokens += usage.input_tokens; - total.output_tokens += usage.output_tokens; - *has_usage = true; - } - } -} - -#[cfg(test)] -mod tests { - use super::super::CompactService; - - #[test] - fn room_summary_uses_eighty_five_percent_input_budget() { - assert_eq!( - CompactService::safe_model_input_budget_from_limit(Some(1000)), - 850 - ); - } - - #[test] - fn oversized_text_is_split_in_half() { - let (left, right) = CompactService::split_text_in_half("abcdefgh").unwrap(); - assert_eq!(format!("{}{}", left, right), "abcdefgh"); - assert!(!left.is_empty()); - assert!(!right.is_empty()); - } -} diff --git a/libs/agent/compact/types.rs b/libs/agent/compact/types.rs deleted file mode 100644 index 0467f8b..0000000 --- a/libs/agent/compact/types.rs +++ /dev/null @@ -1,209 +0,0 @@ -use chrono::{DateTime, Utc}; -use models::rooms::{ - MessageContentType, MessageSenderType, room_message::Model as RoomMessageModel, -}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use uuid::Uuid; - -use crate::tokent::TokenUsage; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MessageSummary { - pub id: Uuid, - pub sender_type: MessageSenderType, - pub sender_id: Option, - pub sender_name: String, - pub content: String, - pub content_type: MessageContentType, - /// Tool call ID extracted from message content JSON, if present. - pub tool_call_id: Option, - pub send_at: DateTime, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CompactSummary { - pub session_id: Uuid, - pub room_id: Uuid, - pub retained: Vec, - pub summary: String, - pub compacted_at: DateTime, - pub messages_compressed: usize, - /// Token usage for the compaction AI call. `None` if usage data was unavailable. - pub usage: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RoomCompactRecord { - pub id: Uuid, - pub room_id: Uuid, - pub from_seq: i64, - pub to_seq: i64, - pub summary: String, - pub message_count: i32, - pub source_message_ids: Vec, - pub created_at: DateTime, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RoomCompactContext { - pub room_id: Uuid, - pub cutoff_seq: Option, - pub summary: Option, - pub retained: Vec, - pub estimated_tokens: usize, - pub compacted: bool, -} - -#[derive(Debug, Clone, Copy)] -pub enum CompactLevel { - Light, - Aggressive, -} - -impl CompactLevel { - pub fn retain_count(&self) -> usize { - match self { - CompactLevel::Light => 5, - CompactLevel::Aggressive => 2, - } - } - - /// Auto-select level based on estimated token count and config. - /// - /// - `Light` (retain 5): when tokens are moderately over threshold - /// - `Aggressive` (retain 2): when tokens are severely over threshold (2x+) - pub fn auto_select(estimated_tokens: usize, threshold: usize) -> Self { - if threshold == 0 { - return CompactLevel::Light; - } - if estimated_tokens >= threshold * 2 { - CompactLevel::Aggressive - } else { - CompactLevel::Light - } - } -} - -/// Configuration for automatic compaction. -#[derive(Debug, Clone, Copy)] -pub struct CompactConfig { - /// Only trigger compaction when estimated token count exceeds this. - /// Set to 0 to disable threshold (always compact when messages > retain_count). - pub token_threshold: usize, - /// If true, auto-select level based on how far over the threshold we are. - /// If false, always use `default_level`. - pub auto_level: bool, - /// Fallback level when `auto_level` is false. - pub default_level: CompactLevel, - /// Maximum tokens the summary may contain (enforced via prompt). - pub max_summary_tokens: usize, -} - -impl Default for CompactConfig { - fn default() -> Self { - Self { - token_threshold: 100_000, - auto_level: true, - default_level: CompactLevel::Light, - max_summary_tokens: 4096, - } - } -} - -impl CompactConfig { - pub const MIN_SUMMARY_TOKENS: usize = 256; - pub const MAX_SUMMARY_TOKENS: usize = 4096; - - /// Build config from project context settings. - pub fn from_project_setting( - context_window_tokens: i32, - compaction_threshold: f32, - compaction_max_summary_ratio: f32, - ) -> Self { - let context_window_tokens = context_window_tokens.max(0) as usize; - let threshold = (context_window_tokens as f32 * compaction_threshold.max(0.0)) as usize; - Self { - token_threshold: threshold, - auto_level: true, - default_level: CompactLevel::Light, - max_summary_tokens: Self::summary_token_budget( - context_window_tokens, - compaction_max_summary_ratio, - ), - } - } - - pub fn summary_token_budget( - context_window_tokens: usize, - compaction_max_summary_ratio: f32, - ) -> usize { - let ratio = compaction_max_summary_ratio.max(0.0); - let raw_budget = (context_window_tokens as f32 * ratio) as usize; - - if raw_budget == 0 { - Self::MIN_SUMMARY_TOKENS - } else { - raw_budget.clamp(Self::MIN_SUMMARY_TOKENS, Self::MAX_SUMMARY_TOKENS) - } - } -} - -/// Result of a threshold check before deciding whether to compact. -#[derive(Debug)] -pub enum ThresholdResult { - /// Token count is below threshold — skip compaction. - Skip { estimated_tokens: usize }, - /// Token count exceeds threshold — compact with this level. - Compact { - estimated_tokens: usize, - level: CompactLevel, - }, -} - -impl From for MessageSummary { - fn from(m: RoomMessageModel) -> Self { - let sender_type = m.sender_type.clone(); - let content = m.content.clone(); - Self { - id: m.id, - sender_type: sender_type.clone(), - sender_id: m.sender_id, - sender_name: sender_type.to_string(), - content, - content_type: m.content_type.clone(), - tool_call_id: Self::extract_tool_call_id(&m.content), - send_at: m.send_at, - } - } -} - -impl MessageSummary { - fn extract_tool_call_id(content: &str) -> Option { - let content = content.trim(); - if let Ok(v) = serde_json::from_str::(content) { - v.get("tool_call_id") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::CompactConfig; - - #[test] - fn summary_budget_has_minimum_floor() { - assert_eq!(CompactConfig::summary_token_budget(0, 0.0), 256); - assert_eq!(CompactConfig::summary_token_budget(128_000, 0.0), 256); - assert_eq!(CompactConfig::summary_token_budget(1_000, 0.01), 256); - } - - #[test] - fn summary_budget_is_capped() { - assert_eq!(CompactConfig::summary_token_budget(128_000, 0.2), 4096); - } -} diff --git a/libs/agent/embed/chunk.rs b/libs/agent/embed/chunk.rs deleted file mode 100644 index 7c848a2..0000000 --- a/libs/agent/embed/chunk.rs +++ /dev/null @@ -1,63 +0,0 @@ -/// Maximum characters per chunk for embedding (approximates token limit). -/// text-embedding-3-small: 8192 token limit. -/// For CJK ~1 char/token, for English ~4 chars/token. -/// Conservative limit: 7000 chars to leave room for all languages. -const MAX_CHUNK_CHARS: usize = 7000; - -/// Split long text into chunks at paragraph/sentence boundaries. -/// Returns at least one chunk even for empty text. -/// Safe for multi-byte characters (uses char indices, not byte indices). -pub fn chunk_text(text: &str) -> Vec { - if text.is_empty() { - return vec![String::new()]; - } - if text.len() <= MAX_CHUNK_CHARS { - return vec![text.to_string()]; - } - - let char_indices: Vec = text.char_indices().map(|(i, _)| i).collect(); - let total_chars = char_indices.len(); - - let mut chunks = Vec::new(); - let mut start_idx = 0; - - while start_idx < total_chars { - let byte_start = char_indices[start_idx]; - let end_char_idx = (start_idx + MAX_CHUNK_CHARS).min(total_chars); - let byte_end_candidate = char_indices[end_char_idx - 1] - + text[char_indices[end_char_idx - 1]..] - .chars() - .next() - .map(|c| c.len_utf8()) - .unwrap_or(1); - - if end_char_idx >= total_chars { - chunks.push(text[byte_start..].to_string()); - break; - } - - let search_range = &text[byte_start..byte_end_candidate]; - let break_at = search_range - .rfind("\n\n") - .map(|pos| pos + 2) - .or_else(|| search_range.rfind('\n').map(|pos| pos + 1)) - .or_else(|| search_range.rfind(". ").map(|pos| pos + 1)) - .or_else(|| search_range.rfind("! ").map(|pos| pos + 1)) - .or_else(|| search_range.rfind("? ").map(|pos| pos + 1)); - - if let Some(offset) = break_at { - let byte_end = byte_start + offset; - chunks.push(text[byte_start..byte_end].to_string()); - let mut advance = start_idx + 1; - while advance < total_chars && char_indices[advance] < byte_end { - advance += 1; - } - start_idx = advance; - } else { - chunks.push(text[byte_start..byte_end_candidate].to_string()); - start_idx = end_char_idx; - } - } - - chunks -} diff --git a/libs/agent/embed/client.rs b/libs/agent/embed/client.rs deleted file mode 100644 index 6a11863..0000000 --- a/libs/agent/embed/client.rs +++ /dev/null @@ -1,291 +0,0 @@ -use rig::client::EmbeddingsClient; -use rig::embeddings::EmbeddingModel; -use rig::providers::openai::Client as OpenAiClient; -use serde::{Deserialize, Serialize}; - -use crate::embed::qdrant::QdrantClient; - -pub struct EmbedClient { - openai: OpenAiClient, - qdrant: QdrantClient, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EmbedVector { - pub id: String, - pub vector: Vec, - pub payload: EmbedPayload, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EmbedPayload { - pub entity_type: String, - pub entity_id: String, - pub text: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub extra: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SearchResult { - pub id: String, - pub score: f32, - pub payload: EmbedPayload, -} - -impl EmbedClient { - pub fn new(openai: OpenAiClient, qdrant: QdrantClient) -> Self { - Self { openai, qdrant } - } - - pub async fn embed_text(&self, text: &str, model: &str) -> crate::Result> { - let model = self.openai.embedding_model(model); - let embeddings = model - .embed_texts(vec![text.to_string()]) - .await - .map_err(|e| crate::AgentError::OpenAi(format!("embedding failed: {}", e)))?; - - embeddings - .first() - .map(|e| e.vec.iter().map(|v| *v as f32).collect()) - .ok_or_else(|| crate::AgentError::OpenAi("no embedding returned".into())) - } - - pub async fn embed_batch(&self, texts: &[String], model: &str) -> crate::Result>> { - let model = self.openai.embedding_model(model); - let embeddings = model - .embed_texts(texts.to_vec()) - .await - .map_err(|e| crate::AgentError::OpenAi(format!("embedding batch failed: {}", e)))?; - - tracing::debug!( - input_count = texts.len(), - returned_count = embeddings.len(), - "embed_batch: API returned" - ); - - let mut result = vec![Vec::new(); texts.len()]; - for (idx, embedding) in embeddings.into_iter().enumerate() { - if idx < result.len() { - result[idx] = embedding.vec.iter().map(|v| *v as f32).collect(); - continue; - } - tracing::warn!( - idx, - "embed_batch: provider returned more embeddings than requested" - ); - break; - } - - // Check for empty results - let empty_count = result.iter().filter(|v| v.is_empty()).count(); - if empty_count > 0 { - tracing::warn!( - empty_count = empty_count, - total = texts.len(), - "embed_batch: some embeddings returned empty vectors" - ); - } - - Ok(result) - } - - pub async fn upsert(&self, points: Vec) -> crate::Result<()> { - self.qdrant.upsert_points(points).await - } - - /// Upsert points into a named collection (bypasses entity_type routing). - pub async fn upsert_to_collection( - &self, - collection_name: &str, - points: Vec, - ) -> crate::Result<()> { - self.qdrant - .upsert_to_collection(collection_name, points) - .await - } - - pub async fn search( - &self, - query: &str, - entity_type: &str, - model: &str, - limit: usize, - ) -> crate::Result> { - let vector = self.embed_text(query, model).await?; - self.qdrant.search(&vector, entity_type, limit).await - } - - pub async fn search_with_filter( - &self, - query: &str, - entity_type: &str, - model: &str, - limit: usize, - filter: qdrant_client::qdrant::Filter, - ) -> crate::Result> { - let vector = self.embed_text(query, model).await?; - self.qdrant - .search_with_filter(&vector, entity_type, limit, filter) - .await - } - - pub async fn delete_by_entity_id( - &self, - entity_type: &str, - entity_id: &str, - ) -> crate::Result<()> { - self.qdrant.delete_by_filter(entity_type, entity_id).await - } - - pub async fn ensure_collection(&self, entity_type: &str, dimensions: u64) -> crate::Result<()> { - self.qdrant.ensure_collection(entity_type, dimensions).await - } - - pub async fn ensure_skill_collection(&self, dimensions: u64) -> crate::Result<()> { - self.qdrant.ensure_skill_collection(dimensions).await - } - - /// Ensure a room-specific memory collection exists. - pub async fn ensure_room_memory_collection( - &self, - project_name: &str, - room_id: &str, - dimensions: u64, - ) -> crate::Result<()> { - self.qdrant - .ensure_room_memory_collection(project_name, room_id, dimensions) - .await - } - - /// Embed and store a conversation memory (message) in Qdrant. - /// Uses per-room collection: `room:{project_name}:{room_id}`. - pub async fn embed_memory( - &self, - id: &str, - text: &str, - project_name: &str, - room_id: &str, - user_id: Option<&str>, - model: &str, - ) -> crate::Result<()> { - // Compute embedding first to know dimensions - let vector = self.embed_text(text, model).await?; - let collection = - crate::embed::qdrant::QdrantClient::room_memory_collection_name(project_name, room_id); - // Auto-create the room collection with correct dimensions - self.qdrant - .ensure_room_memory_collection(project_name, room_id, vector.len() as u64) - .await?; - let point = EmbedVector { - id: id.to_string(), - vector, - payload: EmbedPayload { - entity_type: "memory".to_string(), - entity_id: room_id.to_string(), - text: text.to_string(), - extra: serde_json::json!({ "user_id": user_id }).into(), - }, - }; - self.qdrant - .upsert_to_collection(&collection, vec![point]) - .await - } - - /// Search memory embeddings by semantic similarity within a room. - /// Searches the per-room collection directly — no post-filtering needed. - pub async fn search_memories( - &self, - query: &str, - model: &str, - project_name: &str, - room_id: &str, - limit: usize, - dimensions: u64, - ) -> crate::Result> { - let vector = self.embed_text(query, model).await?; - let collection = - crate::embed::qdrant::QdrantClient::room_memory_collection_name(project_name, room_id); - // Ensure collection exists (will be no-op if already created) - self.qdrant - .ensure_room_memory_collection(project_name, room_id, dimensions) - .await?; - self.qdrant - .search_collection(&collection, &vector, limit) - .await - } - - pub async fn search_memories_after_seq( - &self, - query: &str, - model: &str, - project_name: &str, - room_id: &str, - limit: usize, - dimensions: u64, - after_seq: Option, - ) -> crate::Result> { - let fetch_limit = if after_seq.is_some() { - limit.saturating_mul(4).max(limit) - } else { - limit - }; - let mut results = self - .search_memories(query, model, project_name, room_id, fetch_limit, dimensions) - .await?; - - if let Some(cutoff) = after_seq { - results.retain(|r| { - r.payload - .extra - .as_ref() - .and_then(|v| v.get("seq")) - .and_then(|v| v.as_i64()) - .map(|seq| seq > cutoff) - .unwrap_or(false) - }); - } - results.truncate(limit); - Ok(results) - } - - /// Embed and store a skill in Qdrant. - pub async fn embed_skill( - &self, - id: &str, - name: &str, - description: &str, - content: &str, - project_uuid: &str, - model: &str, - ) -> crate::Result<()> { - let text = format!("{}: {} {}", name, description, content); - let vector = self.embed_text(&text, model).await?; - let point = EmbedVector { - id: id.to_string(), - vector, - payload: EmbedPayload { - entity_type: "skill".to_string(), - entity_id: project_uuid.to_string(), - text, - extra: serde_json::json!({ "name": name, "description": description }).into(), - }, - }; - self.qdrant.upsert_points(vec![point]).await - } - - /// Search skill embeddings by semantic similarity within a project. - pub async fn search_skills( - &self, - query: &str, - model: &str, - project_uuid: &str, - limit: usize, - ) -> crate::Result> { - let vector = self.embed_text(query, model).await?; - let mut results = self.qdrant.search_skill(&vector, limit + 1).await?; - results.retain(|r| r.payload.entity_id == project_uuid); - results.truncate(limit); - Ok(results) - } -} diff --git a/libs/agent/embed/embeddable.rs b/libs/agent/embed/embeddable.rs deleted file mode 100644 index 59530aa..0000000 --- a/libs/agent/embed/embeddable.rs +++ /dev/null @@ -1,24 +0,0 @@ -use async_trait::async_trait; - -/// Trait for entities that can be embedded as vectors into Qdrant. -#[async_trait] -pub trait Embeddable { - fn entity_type(&self) -> &'static str; - fn to_text(&self) -> String; - fn entity_id(&self) -> String; -} - -/// Input struct for batch memory embedding into per-room Qdrant collections. -#[derive(Debug, Clone)] -pub struct EmbedMemoryInput { - pub message_id: String, - pub seq: i64, - pub content: String, - pub project_name: String, - pub room_id: String, - pub user_id: Option, - pub sender_type: String, -} - -/// Input struct for batch tag embedding. -pub use models::TagEmbedInput; diff --git a/libs/agent/embed/entity_embed.rs b/libs/agent/embed/entity_embed.rs deleted file mode 100644 index e54df21..0000000 --- a/libs/agent/embed/entity_embed.rs +++ /dev/null @@ -1,369 +0,0 @@ -use std::collections::HashMap; - -use super::chunk::chunk_text; -use super::client::{EmbedPayload, EmbedVector}; -use super::embeddable::{EmbedMemoryInput, Embeddable}; - -/// Embedding and upsert operations for entity vectors in Qdrant. -impl super::EmbedService { - pub async fn embed_issue( - &self, - id: &str, - title: &str, - body: Option<&str>, - ) -> crate::Result<()> { - let text = match body { - Some(b) if !b.is_empty() => format!("{}\n\n{}", title, b), - _ => title.to_string(), - }; - - tracing::debug!(issue_id = %id, text_len = text.len(), "embed_issue: calling embedding API"); - let vector = self.client.embed_text(&text, &self.model_name).await?; - tracing::debug!(issue_id = %id, vec_dim = vector.len(), "embed_issue: embedding done"); - - let point = EmbedVector { - id: id.to_string(), - vector, - payload: EmbedPayload { - entity_type: "issue".to_string(), - entity_id: id.to_string(), - text, - extra: None, - }, - }; - - self.client.upsert(vec![point]).await?; - tracing::info!(issue_id = %id, "embed_issue: upsert complete"); - Ok(()) - } - - pub async fn embed_repo( - &self, - id: &str, - name: &str, - description: Option<&str>, - ) -> crate::Result<()> { - let text = match description { - Some(d) if !d.is_empty() => format!("{}: {}", name, d), - _ => name.to_string(), - }; - - tracing::debug!(repo_id = %id, text_len = text.len(), "embed_repo: calling embedding API"); - let vector = self.client.embed_text(&text, &self.model_name).await?; - tracing::debug!(repo_id = %id, vec_dim = vector.len(), "embed_repo: embedding done"); - - let point = EmbedVector { - id: id.to_string(), - vector, - payload: EmbedPayload { - entity_type: "repo".to_string(), - entity_id: id.to_string(), - text, - extra: None, - }, - }; - - self.client.upsert(vec![point]).await?; - tracing::info!(repo_id = %id, "embed_repo: upsert complete"); - Ok(()) - } - - pub async fn embed_issues( - &self, - items: Vec, - ) -> crate::Result<()> { - if items.is_empty() { - return Ok(()); - } - - let texts: Vec = items.iter().map(|i| i.to_text()).collect(); - tracing::debug!(count = texts.len(), "embed_issues: calling embed_batch"); - let embeddings = self.client.embed_batch(&texts, &self.model_name).await?; - tracing::debug!(count = embeddings.len(), "embed_issues: batch done"); - - let points: Vec = items - .into_iter() - .zip(embeddings.into_iter()) - .map(|(item, vector)| EmbedVector { - id: item.entity_id(), - vector, - payload: EmbedPayload { - entity_type: item.entity_type().to_string(), - entity_id: item.entity_id(), - text: item.to_text(), - extra: None, - }, - }) - .collect(); - - let count = points.len(); - self.client.upsert(points).await?; - tracing::info!(count = count, "embed_issues: upsert complete"); - Ok(()) - } - - pub async fn embed_skill( - &self, - skill_id: i64, - name: &str, - description: Option<&str>, - content: &str, - project_uuid: &str, - ) -> crate::Result<()> { - let desc = description.unwrap_or_default(); - let id = skill_id.to_string(); - - tracing::debug!(skill_id = %skill_id, name = %name, content_len = content.len(), "embed_skill: starting"); - - let texts = chunk_text(content); - tracing::debug!(skill_id = %skill_id, chunks = texts.len(), "embed_skill: chunked"); - - if texts.len() == 1 { - self.client - .embed_skill(&id, name, desc, content, project_uuid, &self.model_name) - .await?; - } else { - let full_texts: Vec = texts - .iter() - .map(|t| format!("{}: {} {}", name, desc, t)) - .collect(); - tracing::debug!(skill_id = %skill_id, "embed_skill: calling embed_batch"); - let embeddings = self - .client - .embed_batch(&full_texts, &self.model_name) - .await?; - - let points: Vec = embeddings - .into_iter() - .enumerate() - .map(|(i, vector)| EmbedVector { - id: format!("{}:chunk:{}", id, i), - vector, - payload: EmbedPayload { - entity_type: "skill".to_string(), - entity_id: project_uuid.to_string(), - text: texts[i].clone(), - extra: serde_json::json!({ - "name": name, - "description": desc, - "chunk_index": i, - "total_chunks": texts.len(), - }) - .into(), - }, - }) - .collect(); - - self.client.upsert(points).await?; - } - tracing::info!(skill_id = %skill_id, chunks = texts.len(), "embed_skill: complete"); - Ok(()) - } - - pub async fn embed_issue_chunked( - &self, - id: &str, - title: &str, - body: Option<&str>, - ) -> crate::Result<()> { - let text = match body { - Some(b) if !b.is_empty() => format!("{}\n\n{}", title, b), - _ => title.to_string(), - }; - - let chunks = chunk_text(&text); - if chunks.len() == 1 { - return self.embed_issue(id, title, body).await; - } - - let embeddings = self.client.embed_batch(&chunks, &self.model_name).await?; - - let points: Vec = embeddings - .into_iter() - .enumerate() - .map(|(i, vector)| EmbedVector { - id: format!("{}:chunk:{}", id, i), - vector, - payload: EmbedPayload { - entity_type: "issue".to_string(), - entity_id: id.to_string(), - text: chunks[i].clone(), - extra: serde_json::json!({ - "chunk_index": i, - "total_chunks": chunks.len(), - }) - .into(), - }, - }) - .collect(); - - self.client.upsert(points).await - } - - pub async fn embed_memories_batch(&self, messages: Vec) -> crate::Result<()> { - if messages.is_empty() { - return Ok(()); - } - - let mut by_room: HashMap)>> = HashMap::new(); - - for msg in messages { - let chunks = chunk_text(&msg.content); - if chunks.is_empty() || chunks.iter().all(|c| c.trim().is_empty()) { - continue; - } - let collection = super::qdrant::QdrantClient::room_memory_collection_name( - &msg.project_name, - &msg.room_id, - ); - by_room.entry(collection).or_default().push((msg, chunks)); - } - - for (collection, entries) in &by_room { - let all_texts: Vec = entries - .iter() - .flat_map(|(_, chunks)| chunks.iter().cloned()) - .collect(); - - if all_texts.is_empty() { - continue; - } - - let embeddings = self - .client - .embed_batch(&all_texts, &self.model_name) - .await?; - - if let Some((first, _)) = entries.first() { - let _ = self - .client - .ensure_room_memory_collection( - &first.project_name, - &first.room_id, - self.dimensions, - ) - .await; - } - - let mut points = Vec::new(); - let mut embed_idx = 0; - for (msg, chunks) in entries { - for (chunk_i, chunk) in chunks.iter().enumerate() { - if embed_idx >= embeddings.len() { - break; - } - let point_id = if chunks.len() == 1 { - msg.message_id.clone() - } else { - format!("{}:chunk:{}", msg.message_id, chunk_i) - }; - points.push(EmbedVector { - id: point_id, - vector: embeddings[embed_idx].clone(), - payload: EmbedPayload { - entity_type: "memory".to_string(), - entity_id: msg.room_id.clone(), - text: chunk.clone(), - extra: serde_json::json!({ - "message_id": msg.message_id, - "seq": msg.seq, - "user_id": msg.user_id, - "sender_type": msg.sender_type, - "chunk_index": if chunks.len() > 1 { - Some(chunk_i) - } else { - None - }, - "total_chunks": if chunks.len() > 1 { - Some(chunks.len()) - } else { - None - }, - }) - .into(), - }, - }); - embed_idx += 1; - } - } - - if let Err(e) = self.client.upsert_to_collection(collection, points).await { - tracing::warn!(collection = %collection, error = %e, "batch memory embed failed"); - } - } - - Ok(()) - } - - pub async fn embed_tags_batch( - &self, - tags: Vec, - ) -> crate::Result<()> { - if tags.is_empty() { - return Ok(()); - } - - let texts: Vec = tags - .iter() - .map(|t| { - if let Some(ref desc) = t.description { - if !desc.is_empty() { - format!("{}: {}", t.name, desc) - } else { - t.name.clone() - } - } else { - t.name.clone() - } - }) - .collect(); - - let embeddings = self.client.embed_batch(&texts, &self.model_name).await?; - - let points: Vec = tags - .into_iter() - .zip(embeddings.into_iter()) - .map(|(tag, vector)| { - let point_id = format!("{}:{}", tag.repo_id, tag.name); - EmbedVector { - id: point_id, - vector, - payload: EmbedPayload { - entity_type: "repo_tag".to_string(), - entity_id: tag.project_id.clone(), - text: tag.name.clone(), - extra: serde_json::json!({ - "repo_id": tag.repo_id, - "repo_name": tag.repo_name, - "tag_name": tag.name, - "description": tag.description, - }) - .into(), - }, - } - }) - .collect(); - - self.client.upsert(points).await - } - - pub async fn embed_memory( - &self, - message_id: &str, - text: &str, - project_name: &str, - room_id: &str, - user_id: Option<&str>, - ) -> crate::Result<()> { - self.client - .embed_memory( - message_id, - text, - project_name, - room_id, - user_id, - &self.model_name, - ) - .await - } -} diff --git a/libs/agent/embed/mod.rs b/libs/agent/embed/mod.rs deleted file mode 100644 index 37541cb..0000000 --- a/libs/agent/embed/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -pub mod chunk; -pub mod client; -pub mod embeddable; -pub mod entity_embed; -pub mod qdrant; -pub mod search; - -pub use client::{EmbedClient, EmbedPayload, EmbedVector, SearchResult}; -pub use embeddable::{EmbedMemoryInput, Embeddable, TagEmbedInput}; -pub use qdrant::QdrantClient; - -use std::sync::Arc; - -#[derive(Clone)] -pub struct EmbedService { - client: Arc, - db: sea_orm::DatabaseConnection, - model_name: String, - dimensions: u64, -} - -impl EmbedService { - pub fn new( - client: EmbedClient, - db: sea_orm::DatabaseConnection, - model_name: String, - dimensions: u64, - ) -> Self { - Self { - client: Arc::new(client), - db, - model_name, - dimensions, - } - } - - pub async fn ensure_collections(&self) -> crate::Result<()> { - self.client - .ensure_collection("issue", self.dimensions) - .await?; - self.client - .ensure_collection("repo", self.dimensions) - .await?; - self.client.ensure_skill_collection(self.dimensions).await?; - self.client - .ensure_collection("repo_tag", self.dimensions) - .await?; - Ok(()) - } - - pub fn db(&self) -> &sea_orm::DatabaseConnection { - &self.db - } - - pub fn client(&self) -> &Arc { - &self.client - } - - pub fn model_name(&self) -> &str { - &self.model_name - } - - pub fn dimensions(&self) -> u64 { - self.dimensions - } -} - -pub async fn new_embed_client(config: &config::AppConfig) -> crate::Result { - let base_url = config - .get_embed_model_base_url() - .map_err(|e| crate::AgentError::Internal(e.to_string()))?; - let api_key = config - .get_embed_model_api_key() - .map_err(|e| crate::AgentError::Internal(e.to_string()))?; - let qdrant_url = config - .get_qdrant_url() - .map_err(|e| crate::AgentError::Internal(e.to_string()))?; - let qdrant_api_key = config.get_qdrant_api_key(); - - let openai = rig::providers::openai::Client::builder() - .api_key(&api_key) - .base_url(&base_url) - .build() - .map_err(|e| { - crate::AgentError::Internal(format!("failed to build rig openai client: {}", e)) - })?; - - let qdrant = QdrantClient::new(&qdrant_url, qdrant_api_key.as_deref()).await?; - Ok(EmbedClient::new(openai, qdrant)) -} diff --git a/libs/agent/embed/qdrant.rs b/libs/agent/embed/qdrant.rs deleted file mode 100644 index 6cfee6a..0000000 --- a/libs/agent/embed/qdrant.rs +++ /dev/null @@ -1,373 +0,0 @@ -use qdrant_client::Qdrant; -use qdrant_client::qdrant::{ - Condition, CreateCollectionBuilder, DeletePointsBuilder, Distance, FieldCondition, Filter, - Match, PointStruct, SearchPointsBuilder, UpsertPointsBuilder, VectorParamsBuilder, Vectors, - condition::ConditionOneOf, r#match::MatchValue, point_id::PointIdOptions, value, -}; -use std::collections::HashMap; -use std::sync::Arc; - -use super::client::{EmbedPayload, SearchResult}; -use crate::embed::client::EmbedVector; - -pub struct QdrantClient { - inner: Arc, -} - -impl Clone for QdrantClient { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } -} - -impl QdrantClient { - pub async fn new(url: &str, api_key: Option<&str>) -> crate::Result { - let mut builder = Qdrant::from_url(url); - if let Some(key) = api_key { - builder = builder.api_key(key); - } - let inner = builder - .build() - .map_err(|e| crate::AgentError::Qdrant(e.to_string()))?; - Ok(Self { - inner: Arc::new(inner), - }) - } - - fn collection_name(entity_type: &str) -> String { - format!("embed_{}", entity_type) - } - - /// Generate the collection name for a room's memory vectors. - pub fn room_memory_collection_name(project_name: &str, room_id: &str) -> String { - let _ = project_name; - format!("room_memory_{}", room_id.replace('-', "_")) - } - - pub async fn ensure_collection(&self, entity_type: &str, dimensions: u64) -> crate::Result<()> { - let name = Self::collection_name(entity_type); - self.ensure_collection_named(&name, dimensions).await - } - - async fn ensure_collection_named(&self, name: &str, dimensions: u64) -> crate::Result<()> { - let exists = self - .inner - .collection_exists(name) - .await - .map_err(|e| crate::AgentError::Qdrant(e.to_string()))?; - - if exists { - return Ok(()); - } - - let create_collection = CreateCollectionBuilder::new(name) - .vectors_config(VectorParamsBuilder::new(dimensions, Distance::Cosine)) - .build(); - - self.inner - .create_collection(create_collection) - .await - .map_err(|e| crate::AgentError::Qdrant(e.to_string()))?; - - Ok(()) - } - - /// Ensure a room-specific memory collection exists. - pub async fn ensure_room_memory_collection( - &self, - project_name: &str, - room_id: &str, - dimensions: u64, - ) -> crate::Result<()> { - let name = Self::room_memory_collection_name(project_name, room_id); - self.ensure_collection_named(&name, dimensions).await - } - - pub async fn upsert_points(&self, points: Vec) -> crate::Result<()> { - if points.is_empty() { - return Ok(()); - } - - // Reject empty vectors — they cause Qdrant to reject the entire batch - let empty_vectors = points.iter().filter(|p| p.vector.is_empty()).count(); - if empty_vectors > 0 { - tracing::error!( - empty_count = empty_vectors, - total = points.len(), - "upsert_points: REJECTING points with empty vectors" - ); - return Err(crate::AgentError::Qdrant(format!( - "refusing to upsert {} points with empty vectors", - empty_vectors - ))); - } - - let collection_name = Self::collection_name(&points[0].payload.entity_type); - self.upsert_to_collection(&collection_name, points).await - } - - /// Upsert points into a specific collection by name. - pub async fn upsert_to_collection( - &self, - collection_name: &str, - points: Vec, - ) -> crate::Result<()> { - if points.is_empty() { - return Ok(()); - } - - let qdrant_points: Vec = points - .into_iter() - .map(|p| { - let mut payload: HashMap = HashMap::new(); - payload.insert("entity_type".to_string(), p.payload.entity_type.into()); - payload.insert("entity_id".to_string(), p.payload.entity_id.into()); - payload.insert("text".to_string(), p.payload.text.into()); - if let Some(extra) = p.payload.extra { - let extra_str = serde_json::to_string(&extra).unwrap_or_default(); - payload.insert( - "extra".to_string(), - qdrant_client::qdrant::Value { - kind: Some(qdrant_client::qdrant::value::Kind::StringValue(extra_str)), - }, - ); - } - - PointStruct::new(p.id, Vectors::from(p.vector), payload) - }) - .collect(); - - let upsert = UpsertPointsBuilder::new(collection_name, qdrant_points).build(); - - self.inner - .upsert_points(upsert) - .await - .map_err(|e| crate::AgentError::Qdrant(e.to_string()))?; - - Ok(()) - } - - fn extract_string(value: &qdrant_client::qdrant::Value) -> String { - match &value.kind { - Some(value::Kind::StringValue(s)) => s.clone(), - _ => String::new(), - } - } - - pub async fn search( - &self, - vector: &[f32], - entity_type: &str, - limit: usize, - ) -> crate::Result> { - let collection_name = Self::collection_name(entity_type); - self.search_collection(&collection_name, vector, limit) - .await - } - - /// Search a specific collection by name. - pub async fn search_collection( - &self, - collection_name: &str, - vector: &[f32], - limit: usize, - ) -> crate::Result> { - let search_req = SearchPointsBuilder::new(collection_name, vector.to_vec(), limit as u64) - .with_payload(true) - .build(); - - let results = self - .inner - .search_points(search_req) - .await - .map_err(|e| crate::AgentError::Qdrant(e.to_string()))?; - - Ok(results - .result - .into_iter() - .filter_map(|p| { - let entity_type = p - .payload - .get(&"entity_type".to_string()) - .map(Self::extract_string) - .unwrap_or_default(); - - let entity_id = p - .payload - .get(&"entity_id".to_string()) - .map(Self::extract_string) - .unwrap_or_default(); - - let text = p - .payload - .get(&"text".to_string()) - .map(Self::extract_string) - .unwrap_or_default(); - - let extra = p.payload.get(&"extra".to_string()).and_then(|v| { - let s = Self::extract_string(v); - if s.is_empty() { - None - } else { - serde_json::from_str::(&s).ok() - } - }); - - let id = - p.id.and_then(|id| id.point_id_options) - .map(|opts| match opts { - PointIdOptions::Uuid(s) => s, - PointIdOptions::Num(n) => n.to_string(), - }) - .unwrap_or_default(); - - Some(SearchResult { - id, - score: p.score, - payload: EmbedPayload { - entity_type, - entity_id, - text, - extra, - }, - }) - }) - .collect()) - } - - pub async fn search_with_filter( - &self, - vector: &[f32], - entity_type: &str, - limit: usize, - filter: Filter, - ) -> crate::Result> { - let collection_name = Self::collection_name(entity_type); - - let search = SearchPointsBuilder::new(collection_name, vector.to_vec(), limit as u64) - .with_payload(true) - .filter(filter) - .build(); - - let results = self - .inner - .search_points(search) - .await - .map_err(|e| crate::AgentError::Qdrant(e.to_string()))?; - - Ok(results - .result - .into_iter() - .filter_map(|p| { - let entity_type = p - .payload - .get(&"entity_type".to_string()) - .map(Self::extract_string) - .unwrap_or_default(); - - let entity_id = p - .payload - .get(&"entity_id".to_string()) - .map(Self::extract_string) - .unwrap_or_default(); - - let text = p - .payload - .get(&"text".to_string()) - .map(Self::extract_string) - .unwrap_or_default(); - - let extra = p.payload.get(&"extra".to_string()).and_then(|v| { - let s = Self::extract_string(v); - if s.is_empty() { - None - } else { - serde_json::from_str::(&s).ok() - } - }); - - let id = - p.id.and_then(|id| id.point_id_options) - .map(|opts| match opts { - PointIdOptions::Uuid(s) => s, - PointIdOptions::Num(n) => n.to_string(), - }) - .unwrap_or_default(); - - Some(SearchResult { - id, - score: p.score, - payload: EmbedPayload { - entity_type, - entity_id, - text, - extra, - }, - }) - }) - .collect()) - } - - pub async fn delete_by_filter(&self, entity_type: &str, entity_id: &str) -> crate::Result<()> { - let collection_name = Self::collection_name(entity_type); - - let filter = Filter { - must: vec![Condition { - condition_one_of: Some(ConditionOneOf::Field(FieldCondition { - key: "entity_id".to_string(), - r#match: Some(Match { - match_value: Some(MatchValue::Keyword(entity_id.to_string())), - }), - ..Default::default() - })), - }], - ..Default::default() - }; - - let delete = DeletePointsBuilder::new(collection_name) - .points(filter) - .build(); - - self.inner - .delete_points(delete) - .await - .map_err(|e| crate::AgentError::Qdrant(e.to_string()))?; - - Ok(()) - } - - pub async fn delete_collection(&self, entity_type: &str) -> crate::Result<()> { - let name = Self::collection_name(entity_type); - self.inner - .delete_collection(name) - .await - .map_err(|e| crate::AgentError::Qdrant(e.to_string()))?; - Ok(()) - } - - pub async fn ensure_memory_collection(&self, dimensions: u64) -> crate::Result<()> { - self.ensure_collection("memory", dimensions).await - } - - pub async fn ensure_skill_collection(&self, dimensions: u64) -> crate::Result<()> { - self.ensure_collection("skill", dimensions).await - } - - pub async fn search_memory( - &self, - vector: &[f32], - limit: usize, - ) -> crate::Result> { - self.search(vector, "memory", limit).await - } - - pub async fn search_skill( - &self, - vector: &[f32], - limit: usize, - ) -> crate::Result> { - self.search(vector, "skill", limit).await - } -} diff --git a/libs/agent/embed/search.rs b/libs/agent/embed/search.rs deleted file mode 100644 index 74d8228..0000000 --- a/libs/agent/embed/search.rs +++ /dev/null @@ -1,107 +0,0 @@ -use qdrant_client::qdrant::Filter; - -use super::client::SearchResult; - -/// Vector search operations for Qdrant-backed entity retrieval. -impl super::EmbedService { - pub async fn search_issues( - &self, - query: &str, - limit: usize, - ) -> crate::Result> { - self.client - .search(query, "issue", &self.model_name, limit) - .await - } - - pub async fn search_repos( - &self, - query: &str, - limit: usize, - ) -> crate::Result> { - self.client - .search(query, "repo", &self.model_name, limit) - .await - } - - pub async fn search_issues_filtered( - &self, - query: &str, - limit: usize, - filter: Filter, - ) -> crate::Result> { - self.client - .search_with_filter(query, "issue", &self.model_name, limit, filter) - .await - } - - /// Search repo tags by semantic similarity within a project. - /// Filters by project_id (stored in entity_id) for project isolation. - pub async fn search_tags( - &self, - query: &str, - project_id: &str, - limit: usize, - ) -> crate::Result> { - let mut results = self - .client - .search(query, "repo_tag", &self.model_name, limit + 1) - .await?; - results.retain(|r| r.payload.entity_id == project_id); - results.truncate(limit); - Ok(results) - } - - /// Search skills by semantic similarity within a project. - pub async fn search_skills( - &self, - query: &str, - project_uuid: &str, - limit: usize, - ) -> crate::Result> { - self.client - .search_skills(query, &self.model_name, project_uuid, limit) - .await - } - - /// Search past conversation messages by semantic similarity within a room. - pub async fn search_memories( - &self, - query: &str, - project_name: &str, - room_id: &str, - limit: usize, - ) -> crate::Result> { - self.client - .search_memories( - query, - &self.model_name, - project_name, - room_id, - limit, - self.dimensions, - ) - .await - } - - pub async fn search_memories_after_seq( - &self, - query: &str, - project_name: &str, - room_id: &str, - limit: usize, - after_seq: Option, - ) -> crate::Result> { - self.client - .search_memories_after_seq( - query, - &self.model_name, - project_name, - room_id, - limit, - self.dimensions, - after_seq, - ) - .await - } -} diff --git a/libs/agent/error.rs b/libs/agent/error.rs deleted file mode 100644 index 9ea5a31..0000000 --- a/libs/agent/error.rs +++ /dev/null @@ -1,63 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum AgentError { - #[error("openai error: {0}")] - OpenAi(String), - - #[error("qdrant error: {0}")] - Qdrant(String), - - #[error("internal error: {0}")] - Internal(String), - - #[error("not found: {0}")] - NotFound(String), - - /// The task exceeded its timeout limit. - #[error("task {task_id} timed out after {seconds}s")] - Timeout { task_id: i64, seconds: u64 }, - - /// The agent has been rate-limited; retry after the indicated delay. - #[error("rate limited, retry after {retry_after_secs}s")] - RateLimited { retry_after_secs: u64 }, - - /// A transient error that can be retried. - #[error("retryable error (attempt {attempt}): {message}")] - Retryable { attempt: u32, message: String }, - - /// The requested tool is not registered in the tool registry. - #[error("tool not found: {tool}")] - ToolNotFound { tool: String }, - - /// A tool execution failed. - #[error("tool '{tool}' execution failed: {cause}")] - ToolExecutionFailed { tool: String, cause: String }, - - /// The request contains invalid input. - #[error("invalid input in '{field}': {reason}")] - InvalidInput { field: String, reason: String }, -} - -pub type Result = std::result::Result; - -impl From for AgentError { - fn from(e: qdrant_client::QdrantError) -> Self { - AgentError::Qdrant(e.to_string()) - } -} - -impl From for AgentError { - fn from(e: sea_orm::DbErr) -> Self { - AgentError::Internal(e.to_string()) - } -} - -impl From for AgentError { - fn from(e: crate::tool::ToolError) -> Self { - AgentError::ToolExecutionFailed { - tool: String::new(), - cause: e.to_string(), - } - } -} diff --git a/libs/agent/lib.rs b/libs/agent/lib.rs deleted file mode 100644 index b228fdd..0000000 --- a/libs/agent/lib.rs +++ /dev/null @@ -1,60 +0,0 @@ -pub mod agent; -pub mod billing; -pub mod chat; -pub mod client; -pub mod compact; -pub mod embed; -pub mod error; -pub mod model; -pub mod orao; -pub mod perception; -pub mod react; -pub mod skills; -pub mod sync; -pub mod task; -pub mod tokent; -pub mod tool; -pub use billing::{ - BillingRecord, BillingResult, check_balance, check_user_balance, initialize_project_billing, - initialize_user_billing, persist_billing_error, record_ai_usage, record_user_ai_usage, -}; -pub use chat::{ - AgentExecutionProfile, AgentRole, AiContextSenderType, AiRequest, AiStreamChunk, ChatService, - Mention, RoomMessageContext, StreamCallback, -}; -pub use client::types::ChatRequestMessage; -pub use client::{AiCallResponse, AiClientConfig, call_with_params, call_with_retry}; -pub use compact::{ - CompactConfig, CompactLevel, CompactService, CompactSummary, MessageSummary, - RoomCompactContext, RoomCompactRecord, -}; -pub use embed::{ - EmbedClient, EmbedMemoryInput, EmbedService, QdrantClient, SearchResult, TagEmbedInput, - new_embed_client, -}; -pub use error::{AgentError, Result}; -pub use orao::{ - ActionExecutor, ActionResult, ActionType, ActionVerdict, OraoConfig, OraoExecutor, - OraoExecutorBuilder, OraoOutcome, OraoStep, PerceptionSnapshot, PlannedAction, ReasoningOutput, - RoundRecord, SafetyLevel, -}; -pub use perception::{PerceptionService, SkillContext, SkillEntry, ToolCallEvent}; -pub use react::{ - DEFAULT_SYSTEM_PROMPT, PERSONAL_CONTEXT_PROMPT, ROOM_CONTEXT_PROMPT, ReactConfig, ReactStep, -}; -pub use skills::{ - BuiltInSkill, SKILL_TEMPLATES, all_skill_slugs, all_skills, get_skill, get_skill_by_tool, - is_built_in_skill, match_skill_by_keyword, skills_by_category, -}; -pub use sync::list_accessible_models; -pub use task::TaskService; -pub use tokent::{TokenUsage, resolve_usage}; -pub use tool::{ - ToolCall, ToolCallRecord, ToolCallRecorder, ToolCallResult, ToolContext, ToolDefinition, - ToolError, ToolExecutor, ToolHandler, ToolParam, ToolRegistry, ToolResult, ToolSchema, -}; - -#[cfg(feature = "rig")] -pub use agent::RigAgentService; -#[cfg(feature = "rig")] -pub use tool::{RecordingTool, RigToolSet, is_retryable_tool_error}; diff --git a/libs/agent/model/capability.rs b/libs/agent/model/capability.rs deleted file mode 100644 index 5a5ff7a..0000000 --- a/libs/agent/model/capability.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Model capability management — CRUD. - -use chrono::Utc; -use db::database::AppDatabase; -use models::agents::CapabilityType; -use models::agents::model_capability; -use sea_orm::*; - -use crate::error::AgentError; - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct CreateModelCapabilityRequest { - pub model_version_id: i64, - pub capability: String, - #[serde(default)] - pub is_supported: bool, -} - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct UpdateModelCapabilityRequest { - pub is_supported: Option, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct ModelCapabilityResponse { - pub id: i64, - pub model_version_id: i64, - pub capability: String, - pub is_supported: bool, - pub created_at: chrono::DateTime, -} - -impl From for ModelCapabilityResponse { - fn from(mc: model_capability::Model) -> Self { - Self { - id: mc.id, - model_version_id: mc.model_version_id, - capability: mc.capability, - is_supported: mc.is_supported, - created_at: mc.created_at, - } - } -} - -pub async fn list_capabilities( - db: &AppDatabase, - model_version_id: i64, -) -> Result, AgentError> { - let caps = model_capability::Entity::find() - .filter(model_capability::Column::ModelVersionId.eq(model_version_id)) - .order_by_asc(model_capability::Column::Capability) - .all(db) - .await?; - Ok(caps - .into_iter() - .map(ModelCapabilityResponse::from) - .collect()) -} - -pub async fn get_capability( - db: &AppDatabase, - id: i64, -) -> Result { - let cap = model_capability::Entity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Capability record not found: {}", id)))?; - Ok(ModelCapabilityResponse::from(cap)) -} - -pub async fn create_capability( - db: &AppDatabase, - request: CreateModelCapabilityRequest, -) -> Result { - let _ = request - .capability - .parse::() - .map_err(|_| AgentError::InvalidInput { - field: "capability".into(), - reason: "Invalid capability type".into(), - })?; - - let now = Utc::now(); - let active = model_capability::ActiveModel { - model_version_id: Set(request.model_version_id), - capability: Set(request.capability), - is_supported: Set(request.is_supported), - created_at: Set(now), - ..Default::default() - }; - let cap = active.insert(db).await?; - Ok(ModelCapabilityResponse::from(cap)) -} - -pub async fn update_capability( - db: &AppDatabase, - id: i64, - request: UpdateModelCapabilityRequest, -) -> Result { - let cap = model_capability::Entity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Capability record not found: {}", id)))?; - - let mut active: model_capability::ActiveModel = cap.into(); - if let Some(is_supported) = request.is_supported { - active.is_supported = Set(is_supported); - } - - let cap = active.update(db).await?; - Ok(ModelCapabilityResponse::from(cap)) -} - -pub async fn delete_capability(db: &AppDatabase, id: i64) -> Result<(), AgentError> { - model_capability::Entity::delete_by_id(id).exec(db).await?; - Ok(()) -} diff --git a/libs/agent/model/mod.rs b/libs/agent/model/mod.rs deleted file mode 100644 index e315f23..0000000 --- a/libs/agent/model/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod capability; -pub mod model_entry; -pub mod parameter_profile; -pub mod pricing; -pub mod provider; -pub mod version; diff --git a/libs/agent/model/model_entry.rs b/libs/agent/model/model_entry.rs deleted file mode 100644 index 353a52a..0000000 --- a/libs/agent/model/model_entry.rs +++ /dev/null @@ -1,332 +0,0 @@ -//! AI model management — CRUD. -//! -//! All functions take `&DatabaseConnection` instead of `&AppService`. - -use chrono::Utc; -use db::database::AppDatabase; -use models::agents::model; -use models::agents::model_pricing; -use models::agents::model_version; -use models::agents::{ - ModelCapability, ModelModality, ModelStatus, - model::{Column as MColumn, Entity as MEntity}, - model_provider::Entity as ProviderEntity, -}; -use sea_orm::*; -use uuid::Uuid; - -use crate::error::AgentError; - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct CreateModelRequest { - pub provider_id: Uuid, - pub name: String, - pub modality: String, - pub capability: String, - pub context_length: i64, - pub max_output_tokens: Option, - pub training_cutoff: Option>, - #[serde(default)] - pub is_open_source: bool, -} - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct UpdateModelRequest { - pub display_name: Option, - pub modality: Option, - pub capability: Option, - pub context_length: Option, - pub max_output_tokens: Option, - pub training_cutoff: Option>, - pub is_open_source: Option, - pub status: Option, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct ModelResponse { - pub id: Uuid, - pub provider_id: Uuid, - pub name: String, - pub modality: String, - pub capability: String, - pub context_length: i64, - pub max_output_tokens: Option, - pub training_cutoff: Option>, - pub is_open_source: bool, - pub status: String, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -impl From for ModelResponse { - fn from(m: model::Model) -> Self { - Self { - id: m.id, - provider_id: m.provider_id, - name: m.name, - modality: m.modality, - capability: m.capability, - context_length: m.context_length, - max_output_tokens: m.max_output_tokens, - training_cutoff: m.training_cutoff, - is_open_source: m.is_open_source, - status: m.status, - created_at: m.created_at, - updated_at: m.updated_at, - } - } -} - -/// List models, optionally filtered by provider. -pub async fn list_models( - db: &AppDatabase, - provider_id: Option, -) -> Result, AgentError> { - let mut query = MEntity::find().order_by_asc(MColumn::Name); - if let Some(pid) = provider_id { - query = query.filter(MColumn::ProviderId.eq(pid)); - } - let models = query.all(db).await?; - Ok(models.into_iter().map(ModelResponse::from).collect()) -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct ModelWithPricingResponse { - pub id: Uuid, - pub provider_id: Uuid, - pub name: String, - pub modality: String, - pub capability: String, - pub context_length: i64, - pub max_output_tokens: Option, - pub training_cutoff: Option>, - pub is_open_source: bool, - pub status: String, - pub input_price: Option, - pub output_price: Option, - pub currency: Option, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct ModelListResponse { - pub data: Vec, - pub total: u64, - pub page: u64, - pub per_page: u64, -} - -/// List models with pricing, pagination, search, and deprecation filter. -pub async fn list_models_with_pricing( - db: &AppDatabase, - provider_id: Option, - search: Option<&str>, - page: u64, - per_page: u64, -) -> Result { - let mut query = MEntity::find() - .filter(MColumn::Status.ne("deprecated")) - .order_by_asc(MColumn::Name); - - if let Some(pid) = provider_id { - query = query.filter(MColumn::ProviderId.eq(pid)); - } - if let Some(q) = search { - if !q.is_empty() { - query = query.filter(MColumn::Name.contains(q)); - } - } - - let total = query.clone().count(db).await? as u64; - let offset = (page.saturating_sub(1)) * per_page; - let models = query.offset(offset).limit(per_page).all(db).await?; - - // Batch-fetch default versions for these models - let model_ids: Vec = models.iter().map(|m| m.id).collect(); - let versions = if model_ids.is_empty() { - vec![] - } else { - model_version::Entity::find() - .filter(model_version::Column::ModelId.is_in(model_ids)) - .filter(model_version::Column::IsDefault.eq(true)) - .all(db) - .await - .unwrap_or_default() - }; - - let version_ids: Vec = versions.iter().map(|v| v.id).collect(); - let pricings = if version_ids.is_empty() { - vec![] - } else { - model_pricing::Entity::find() - .filter(model_pricing::Column::ModelVersionId.is_in(version_ids)) - .all(db) - .await - .unwrap_or_default() - }; - - // Build lookup: model_id → latest pricing (by effective_from DESC) - let mut pricing_map: std::collections::HashMap = - std::collections::HashMap::new(); - let version_to_model: std::collections::HashMap = - versions.iter().map(|v| (v.id, v.model_id)).collect(); - - for p in &pricings { - if let Some(model_id) = version_to_model.get(&p.model_version_id) { - match pricing_map.get(model_id) { - Some(existing) => { - if p.effective_from > existing.effective_from { - pricing_map.insert(*model_id, p); - } - } - None => { - pricing_map.insert(*model_id, p); - } - } - } - } - - let data = models - .into_iter() - .map(|m| { - let pricing = pricing_map.get(&m.id); - ModelWithPricingResponse { - id: m.id, - provider_id: m.provider_id, - name: m.name, - modality: m.modality, - capability: m.capability, - context_length: m.context_length, - max_output_tokens: m.max_output_tokens, - training_cutoff: m.training_cutoff, - is_open_source: m.is_open_source, - status: m.status, - input_price: pricing.map(|p| p.input_price_per_1k_tokens.clone()), - output_price: pricing.map(|p| p.output_price_per_1k_tokens.clone()), - currency: pricing.map(|p| p.currency.clone()), - created_at: m.created_at, - updated_at: m.updated_at, - } - }) - .collect(); - - Ok(ModelListResponse { - data, - total, - page, - per_page, - }) -} - -/// Get a single model by ID. -pub async fn get_model(db: &AppDatabase, id: Uuid) -> Result { - let model = MEntity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Model not found: {}", id)))?; - Ok(ModelResponse::from(model)) -} - -/// Create a new model. -pub async fn create_model( - db: &AppDatabase, - request: CreateModelRequest, -) -> Result { - ProviderEntity::find_by_id(request.provider_id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound("Provider not found".to_string()))?; - - let _ = request - .modality - .parse::() - .map_err(|_| AgentError::InvalidInput { - field: "modality".into(), - reason: "Invalid modality".into(), - })?; - let _ = - request - .capability - .parse::() - .map_err(|_| AgentError::InvalidInput { - field: "capability".into(), - reason: "Invalid capability".into(), - })?; - - let now = Utc::now(); - let active = model::ActiveModel { - id: Set(Uuid::now_v7()), - provider_id: Set(request.provider_id), - name: Set(request.name), - modality: Set(request.modality), - capability: Set(request.capability), - context_length: Set(request.context_length), - max_output_tokens: Set(request.max_output_tokens), - training_cutoff: Set(request.training_cutoff), - is_open_source: Set(request.is_open_source), - status: Set(ModelStatus::Active.to_string()), - created_at: Set(now), - updated_at: Set(now), - ..Default::default() - }; - let model = active.insert(db).await?; - Ok(ModelResponse::from(model)) -} - -/// Update an existing model. -pub async fn update_model( - db: &AppDatabase, - id: Uuid, - request: UpdateModelRequest, -) -> Result { - let model = MEntity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Model not found: {}", id)))?; - - let mut active: model::ActiveModel = model.into(); - if let Some(modality) = request.modality { - let _ = modality - .parse::() - .map_err(|_| AgentError::InvalidInput { - field: "modality".into(), - reason: "Invalid modality".into(), - })?; - active.modality = Set(modality); - } - if let Some(capability) = request.capability { - let _ = capability - .parse::() - .map_err(|_| AgentError::InvalidInput { - field: "capability".into(), - reason: "Invalid capability".into(), - })?; - active.capability = Set(capability); - } - if let Some(context_length) = request.context_length { - active.context_length = Set(context_length); - } - if let Some(max_output_tokens) = request.max_output_tokens { - active.max_output_tokens = Set(Some(max_output_tokens)); - } - if let Some(training_cutoff) = request.training_cutoff { - active.training_cutoff = Set(Some(training_cutoff)); - } - if let Some(is_open_source) = request.is_open_source { - active.is_open_source = Set(is_open_source); - } - if let Some(status) = request.status { - active.status = Set(status); - } - active.updated_at = Set(Utc::now()); - - let model = active.update(db).await?; - Ok(ModelResponse::from(model)) -} - -/// Delete a model by ID. -pub async fn delete_model(db: &AppDatabase, id: Uuid) -> Result<(), AgentError> { - MEntity::delete_by_id(id).exec(db).await?; - Ok(()) -} diff --git a/libs/agent/model/parameter_profile.rs b/libs/agent/model/parameter_profile.rs deleted file mode 100644 index 233e411..0000000 --- a/libs/agent/model/parameter_profile.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Model parameter profile management — CRUD. - -use db::database::AppDatabase; -use models::agents::model_parameter_profile; -use sea_orm::*; -use uuid::Uuid; - -use crate::error::AgentError; - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct CreateModelParameterProfileRequest { - pub model_version_id: Uuid, - pub temperature_min: f64, - pub temperature_max: f64, - pub top_p_min: f64, - pub top_p_max: f64, - #[serde(default)] - pub frequency_penalty_supported: bool, - #[serde(default)] - pub presence_penalty_supported: bool, -} - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct UpdateModelParameterProfileRequest { - pub temperature_min: Option, - pub temperature_max: Option, - pub top_p_min: Option, - pub top_p_max: Option, - pub frequency_penalty_supported: Option, - pub presence_penalty_supported: Option, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct ModelParameterProfileResponse { - pub id: i64, - pub model_version_id: Uuid, - pub temperature_min: f64, - pub temperature_max: f64, - pub top_p_min: f64, - pub top_p_max: f64, - pub frequency_penalty_supported: bool, - pub presence_penalty_supported: bool, -} - -impl From for ModelParameterProfileResponse { - fn from(p: model_parameter_profile::Model) -> Self { - Self { - id: p.id, - model_version_id: p.model_version_id, - temperature_min: p.temperature_min, - temperature_max: p.temperature_max, - top_p_min: p.top_p_min, - top_p_max: p.top_p_max, - frequency_penalty_supported: p.frequency_penalty_supported, - presence_penalty_supported: p.presence_penalty_supported, - } - } -} - -pub async fn list_parameter_profiles( - db: &AppDatabase, - model_version_id: Uuid, -) -> Result, AgentError> { - let profiles = model_parameter_profile::Entity::find() - .filter(model_parameter_profile::Column::ModelVersionId.eq(model_version_id)) - .all(db) - .await?; - Ok(profiles - .into_iter() - .map(ModelParameterProfileResponse::from) - .collect()) -} - -pub async fn get_parameter_profile( - db: &AppDatabase, - id: i64, -) -> Result { - let profile = model_parameter_profile::Entity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Parameter profile not found: {}", id)))?; - Ok(ModelParameterProfileResponse::from(profile)) -} - -pub async fn create_parameter_profile( - db: &AppDatabase, - request: CreateModelParameterProfileRequest, -) -> Result { - let active = model_parameter_profile::ActiveModel { - model_version_id: Set(request.model_version_id), - temperature_min: Set(request.temperature_min), - temperature_max: Set(request.temperature_max), - top_p_min: Set(request.top_p_min), - top_p_max: Set(request.top_p_max), - frequency_penalty_supported: Set(request.frequency_penalty_supported), - presence_penalty_supported: Set(request.presence_penalty_supported), - ..Default::default() - }; - let profile = active.insert(db).await?; - Ok(ModelParameterProfileResponse::from(profile)) -} - -pub async fn update_parameter_profile( - db: &AppDatabase, - id: i64, - request: UpdateModelParameterProfileRequest, -) -> Result { - let profile = model_parameter_profile::Entity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Parameter profile not found: {}", id)))?; - - let mut active: model_parameter_profile::ActiveModel = profile.into(); - if let Some(v) = request.temperature_min { - active.temperature_min = Set(v); - } - if let Some(v) = request.temperature_max { - active.temperature_max = Set(v); - } - if let Some(v) = request.top_p_min { - active.top_p_min = Set(v); - } - if let Some(v) = request.top_p_max { - active.top_p_max = Set(v); - } - if let Some(v) = request.frequency_penalty_supported { - active.frequency_penalty_supported = Set(v); - } - if let Some(v) = request.presence_penalty_supported { - active.presence_penalty_supported = Set(v); - } - - let profile = active.update(db).await?; - Ok(ModelParameterProfileResponse::from(profile)) -} - -pub async fn delete_parameter_profile(db: &AppDatabase, id: i64) -> Result<(), AgentError> { - model_parameter_profile::Entity::delete_by_id(id) - .exec(db) - .await?; - Ok(()) -} diff --git a/libs/agent/model/pricing.rs b/libs/agent/model/pricing.rs deleted file mode 100644 index 4ae0509..0000000 --- a/libs/agent/model/pricing.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! Model pricing management — CRUD. -//! -//! All functions take `&DatabaseConnection` instead of `&AppService`. - -use chrono::Utc; -use db::database::AppDatabase; -use models::agents::PricingCurrency; -use models::agents::model_pricing; -use sea_orm::*; -use uuid::Uuid; - -use crate::error::AgentError; - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct CreateModelPricingRequest { - pub model_version_id: Uuid, - pub input_price_per_1k_tokens: String, - pub output_price_per_1k_tokens: String, - pub currency: String, - pub effective_from: chrono::DateTime, -} - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct UpdateModelPricingRequest { - pub input_price_per_1k_tokens: Option, - pub output_price_per_1k_tokens: Option, - pub currency: Option, - pub effective_from: Option>, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct ModelPricingResponse { - pub id: i64, - pub model_version_id: Uuid, - pub input_price_per_1k_tokens: String, - pub output_price_per_1k_tokens: String, - pub currency: String, - pub effective_from: chrono::DateTime, -} - -impl From for ModelPricingResponse { - fn from(p: model_pricing::Model) -> Self { - Self { - id: p.id, - model_version_id: p.model_version_id, - input_price_per_1k_tokens: p.input_price_per_1k_tokens, - output_price_per_1k_tokens: p.output_price_per_1k_tokens, - currency: p.currency, - effective_from: p.effective_from, - } - } -} - -/// List pricing records for a model version. -pub async fn list_pricing( - db: &AppDatabase, - model_version_id: Uuid, -) -> Result, AgentError> { - let records = model_pricing::Entity::find() - .filter(model_pricing::Column::ModelVersionId.eq(model_version_id)) - .order_by_desc(model_pricing::Column::EffectiveFrom) - .all(db) - .await?; - Ok(records - .into_iter() - .map(ModelPricingResponse::from) - .collect()) -} - -/// Get a single pricing record by ID. -pub async fn get_pricing(db: &AppDatabase, id: i64) -> Result { - let record = model_pricing::Entity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Pricing record not found: {}", id)))?; - Ok(ModelPricingResponse::from(record)) -} - -/// Create a new pricing record. -pub async fn create_pricing( - db: &AppDatabase, - request: CreateModelPricingRequest, -) -> Result { - let _ = request - .currency - .parse::() - .map_err(|_| AgentError::InvalidInput { - field: "currency".into(), - reason: "Invalid pricing currency".into(), - })?; - - let active = model_pricing::ActiveModel { - model_version_id: Set(request.model_version_id), - input_price_per_1k_tokens: Set(request.input_price_per_1k_tokens), - output_price_per_1k_tokens: Set(request.output_price_per_1k_tokens), - currency: Set(request.currency), - effective_from: Set(request.effective_from), - ..Default::default() - }; - let record = active.insert(db).await?; - Ok(ModelPricingResponse::from(record)) -} - -/// Update a pricing record. -pub async fn update_pricing( - db: &AppDatabase, - id: i64, - request: UpdateModelPricingRequest, -) -> Result { - let record = model_pricing::Entity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Pricing record not found: {}", id)))?; - - let mut active: model_pricing::ActiveModel = record.into(); - if let Some(v) = request.input_price_per_1k_tokens { - active.input_price_per_1k_tokens = Set(v); - } - if let Some(v) = request.output_price_per_1k_tokens { - active.output_price_per_1k_tokens = Set(v); - } - if let Some(v) = request.currency { - let _ = v - .parse::() - .map_err(|_| AgentError::InvalidInput { - field: "currency".into(), - reason: "Invalid pricing currency".into(), - })?; - active.currency = Set(v); - } - if let Some(v) = request.effective_from { - active.effective_from = Set(v); - } - - let record = active.update(db).await?; - Ok(ModelPricingResponse::from(record)) -} - -/// Delete a pricing record. -pub async fn delete_pricing(db: &AppDatabase, id: i64) -> Result<(), AgentError> { - model_pricing::Entity::delete_by_id(id).exec(db).await?; - Ok(()) -} diff --git a/libs/agent/model/provider.rs b/libs/agent/model/provider.rs deleted file mode 100644 index b36cff6..0000000 --- a/libs/agent/model/provider.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! AI provider management — CRUD for model providers. -//! -//! All functions take `&DatabaseConnection` instead of `&AppService` -//! so they can live in the agent crate. - -use chrono::Utc; -use db::database::AppDatabase; -use models::agents::model_provider; -use models::agents::{ModelStatus, model_provider::Entity as ProviderEntity}; -use sea_orm::*; -use uuid::Uuid; - -use crate::error::AgentError; - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct CreateProviderRequest { - pub name: String, - pub display_name: String, - pub website: Option, -} - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct UpdateProviderRequest { - pub display_name: Option, - pub website: Option, - pub status: Option, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct ProviderResponse { - pub id: Uuid, - pub name: String, - pub display_name: String, - pub website: Option, - pub status: String, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -impl From for ProviderResponse { - fn from(p: model_provider::Model) -> Self { - Self { - id: p.id, - name: p.name, - display_name: p.display_name, - website: p.website, - status: p.status, - created_at: p.created_at, - updated_at: p.updated_at, - } - } -} - -/// List all providers ordered by display name. -pub async fn list_providers(db: &AppDatabase) -> Result, AgentError> { - let providers = ProviderEntity::find() - .order_by_asc(model_provider::Column::DisplayName) - .all(db) - .await?; - Ok(providers.into_iter().map(ProviderResponse::from).collect()) -} - -/// Get a single provider by ID. -pub async fn get_provider(db: &AppDatabase, id: Uuid) -> Result { - let provider = ProviderEntity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Provider not found: {}", id)))?; - Ok(ProviderResponse::from(provider)) -} - -/// Create a new provider. -pub async fn create_provider( - db: &AppDatabase, - request: CreateProviderRequest, -) -> Result { - let now = Utc::now(); - let active = model_provider::ActiveModel { - id: Set(Uuid::now_v7()), - name: Set(request.name), - display_name: Set(request.display_name), - website: Set(request.website), - status: Set(ModelStatus::Active.to_string()), - created_at: Set(now), - updated_at: Set(now), - ..Default::default() - }; - let model = active.insert(db).await?; - Ok(ProviderResponse::from(model)) -} - -/// Update an existing provider. -pub async fn update_provider( - db: &AppDatabase, - id: Uuid, - request: UpdateProviderRequest, -) -> Result { - let provider = ProviderEntity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Provider not found: {}", id)))?; - - let mut active: model_provider::ActiveModel = provider.into(); - if let Some(display_name) = request.display_name { - active.display_name = Set(display_name); - } - if let Some(website) = request.website { - active.website = Set(Some(website)); - } - if let Some(status) = request.status { - active.status = Set(status); - } - active.updated_at = Set(Utc::now()); - - let model = active.update(db).await?; - Ok(ProviderResponse::from(model)) -} - -/// Delete a provider by ID. -pub async fn delete_provider(db: &AppDatabase, id: Uuid) -> Result<(), AgentError> { - ProviderEntity::delete_by_id(id).exec(db).await?; - Ok(()) -} diff --git a/libs/agent/model/version.rs b/libs/agent/model/version.rs deleted file mode 100644 index 672988f..0000000 --- a/libs/agent/model/version.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Model version management — CRUD. -//! -//! All functions take `&DatabaseConnection` instead of `&AppService`. - -use chrono::Utc; -use db::database::AppDatabase; -use models::agents::model_version; -use models::agents::{ - ModelStatus, - model_version::{Column as MVColumn, Entity as MVEntity, Model as ModelVersionModel}, -}; -use sea_orm::*; -use uuid::Uuid; - -use crate::error::AgentError; - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct CreateModelVersionRequest { - pub model_id: Uuid, - pub version: String, - pub release_date: Option>, - pub change_log: Option, - #[serde(default)] - pub is_default: bool, -} - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct UpdateModelVersionRequest { - pub version: Option, - pub release_date: Option>, - pub change_log: Option, - pub is_default: Option, - pub status: Option, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct ModelVersionResponse { - pub id: Uuid, - pub model_id: Uuid, - pub version: String, - pub release_date: Option>, - pub change_log: Option, - pub is_default: bool, - pub status: String, - pub created_at: chrono::DateTime, -} - -impl From for ModelVersionResponse { - fn from(mv: ModelVersionModel) -> Self { - Self { - id: mv.id, - model_id: mv.model_id, - version: mv.version, - release_date: mv.release_date, - change_log: mv.change_log, - is_default: mv.is_default, - status: mv.status, - created_at: mv.created_at, - } - } -} - -/// List model versions, optionally filtered by model. -pub async fn list_versions( - db: &AppDatabase, - model_id: Option, -) -> Result, AgentError> { - let mut query = MVEntity::find().order_by_asc(MVColumn::Version); - if let Some(mid) = model_id { - query = query.filter(MVColumn::ModelId.eq(mid)); - } - let versions = query.all(db).await?; - Ok(versions - .into_iter() - .map(ModelVersionResponse::from) - .collect()) -} - -/// Get a single version by ID. -pub async fn get_version(db: &AppDatabase, id: Uuid) -> Result { - let version = MVEntity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Model version not found: {}", id)))?; - Ok(ModelVersionResponse::from(version)) -} - -/// Create a new model version. -pub async fn create_version( - db: &AppDatabase, - request: CreateModelVersionRequest, -) -> Result { - let now = Utc::now(); - let active = model_version::ActiveModel { - id: Set(Uuid::now_v7()), - model_id: Set(request.model_id), - version: Set(request.version), - release_date: Set(request.release_date), - change_log: Set(request.change_log), - is_default: Set(request.is_default), - status: Set(ModelStatus::Active.to_string()), - created_at: Set(now), - ..Default::default() - }; - let version = active.insert(db).await?; - Ok(ModelVersionResponse::from(version)) -} - -/// Update an existing model version. -pub async fn update_version( - db: &AppDatabase, - id: Uuid, - request: UpdateModelVersionRequest, -) -> Result { - let version = MVEntity::find_by_id(id) - .one(db) - .await? - .ok_or_else(|| AgentError::NotFound(format!("Model version not found: {}", id)))?; - - let mut active: model_version::ActiveModel = version.into(); - if let Some(v) = request.version { - active.version = Set(v); - } - if let Some(v) = request.release_date { - active.release_date = Set(Some(v)); - } - if let Some(v) = request.change_log { - active.change_log = Set(Some(v)); - } - if let Some(v) = request.is_default { - active.is_default = Set(v); - } - if let Some(v) = request.status { - active.status = Set(v); - } - - let version = active.update(db).await?; - Ok(ModelVersionResponse::from(version)) -} - -/// Delete a model version by ID. -pub async fn delete_version(db: &AppDatabase, id: Uuid) -> Result<(), AgentError> { - MVEntity::delete_by_id(id).exec(db).await?; - Ok(()) -} diff --git a/libs/agent/orao/act.rs b/libs/agent/orao/act.rs deleted file mode 100644 index 26ca24f..0000000 --- a/libs/agent/orao/act.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Act phase: execute planned actions with safety checks. -//! -//! Actions are executed through a caller-provided executor callback, which -//! typically dispatches to the [`ToolRegistry`] or runs shell commands. -//! All file access must go through function calls (tools), never direct -//! filesystem operations. -//! -//! [`ToolRegistry`]: crate::tool::ToolRegistry - -use std::future::Future; -use std::pin::Pin; -use std::process::Command; -use std::time::Duration; - -use super::types::{ - ActionResult, ActionType, ActionVerdict, OraoConfig, PlannedAction, SafetyLevel, -}; - -/// Callback for executing a planned action. -/// -/// The caller (service layer) provides this to wire up tool execution. -/// Returns `ActionResult` on completion. -pub type ActionExecutor = - Box Pin + Send>> + Send + Sync>; - -/// Check whether an action is allowed under the given safety configuration. -/// -/// Returns `None` if allowed, or `Some(reason)` if blocked. -pub fn check_safety(action: &PlannedAction, config: &OraoConfig) -> Option { - let safety = SafetyLevel::classify_command(&action.command_or_content); - - if safety > config.max_safety_level { - return Some(format!( - "Action denied: safety level {:?} exceeds max allowed {:?}", - safety, config.max_safety_level - )); - } - - // Check for dangerous command patterns - if let Some(reason) = check_dangerous_command(&action.command_or_content) { - return Some(reason); - } - - None -} - -/// Execute a single planned action via the provided executor. -/// -/// Applies safety checks and timeout, then delegates to the executor. -pub async fn execute_action( - action: PlannedAction, - config: &OraoConfig, - executor: &ActionExecutor, -) -> ActionResult { - // ── Safety gate ──────────────────────────────────────────────────── - if let Some(reason) = check_safety(&action, config) { - return ActionResult { - action, - exit_code: Some(1), - stdout: String::new(), - stderr: reason, - file_changes: Vec::new(), - verdict: ActionVerdict::Failure, - }; - } - - // ── Execute with timeout ────────────────────────────────────────── - let action_clone = action.clone(); - let exec_future = executor(action); - - match tokio::time::timeout(Duration::from_secs(config.action_timeout_secs), exec_future).await { - Ok(result) => result, - Err(_elapsed) => ActionResult { - action: action_clone, - exit_code: None, - stdout: String::new(), - stderr: format!( - "Action timed out after {} seconds", - config.action_timeout_secs - ), - file_changes: Vec::new(), - verdict: ActionVerdict::Failure, - }, - } -} - -/// Build a default action executor that runs shell commands directly. -/// -/// This is suitable for `shell_command` and `git_operation` action types. -/// For `tool_invoke`, the caller should provide a custom executor that -/// dispatches to the [`ToolRegistry`]. -/// -/// [`ToolRegistry`]: crate::tool::ToolRegistry -pub fn shell_executor(working_dir: String) -> ActionExecutor { - Box::new(move |action: PlannedAction| { - let dir = working_dir.clone(); - Box::pin(async move { - match action.action_type { - ActionType::ShellCommand | ActionType::GitOperation | ActionType::ToolInvoke => { - run_shell_command(&action, &dir).await - } - ActionType::FileWrite | ActionType::FileEdit => { - // File operations should use tool_invoke with a file-writing tool. - // Direct file access is discouraged; return an error directing to tools. - ActionResult { - exit_code: Some(1), - stdout: String::new(), - stderr: "File operations must use tool_invoke with registered file tools. Use shell_command with sed/echo for inline edits.".to_string(), - file_changes: Vec::new(), - verdict: ActionVerdict::Failure, - action, - } - } - ActionType::UserDialog => ActionResult { - exit_code: None, - stdout: "User dialog requested".to_string(), - stderr: String::new(), - file_changes: Vec::new(), - verdict: ActionVerdict::Success, - action, - }, - } - }) - }) -} - -async fn run_shell_command(action: &PlannedAction, working_dir: &str) -> ActionResult { - let cmd = &action.command_or_content; - - let output = Command::new("sh") - .args(["-c", cmd]) - .current_dir(working_dir) - .output(); - - match output { - Ok(out) => { - let exit_code = out.status.code(); - let stdout = String::from_utf8_lossy(&out.stdout).to_string(); - let stderr = String::from_utf8_lossy(&out.stderr).to_string(); - - let verdict = match exit_code { - Some(0) if !stderr_has_errors(&stderr) => ActionVerdict::Success, - Some(0) => ActionVerdict::SuccessWithWarnings, - _ => ActionVerdict::Failure, - }; - - ActionResult { - action: action.clone(), - exit_code, - stdout, - stderr, - file_changes: Vec::new(), - verdict, - } - } - Err(e) => ActionResult { - action: action.clone(), - exit_code: None, - stdout: String::new(), - stderr: format!("Failed to spawn command: {}", e), - file_changes: Vec::new(), - verdict: ActionVerdict::Failure, - }, - } -} - -fn stderr_has_errors(stderr: &str) -> bool { - let lower = stderr.to_lowercase(); - lower.contains("error") || lower.contains("fail") || lower.contains("panic") -} - -/// Check whether a shell command contains dangerous patterns. -/// -/// Returns `Some(reason)` if the command is blocked, `None` if it's safe. -pub fn check_dangerous_command(cmd: &str) -> Option { - let dangerous = [ - ("rm -rf /", "Recursive root deletion"), - ("rm -rf ~", "Recursive home deletion"), - (":(){ :|:& };:", "Fork bomb"), - ("mkfs.", "Filesystem format"), - ("dd if=", "Raw device write"), - ("> /dev/sda", "Raw device write"), - ("chmod 777 /", "World-writable root"), - ]; - - for (pattern, reason) in &dangerous { - if cmd.contains(pattern) { - return Some(format!("Blocked: {} — {}", pattern, reason)); - } - } - - None -} diff --git a/libs/agent/orao/mod.rs b/libs/agent/orao/mod.rs deleted file mode 100644 index c5b7dad..0000000 --- a/libs/agent/orao/mod.rs +++ /dev/null @@ -1,427 +0,0 @@ -//! ORAO (Observe–Reason–Act–Observe) — a single-agent loop for complex engineering tasks. -//! -//! ORAO extends the ReAct paradigm with: -//! - **Multi-channel perception**: LLM-driven observation via read-only tools -//! - **Structured reasoning**: analysis + step-by-step action plan -//! - **Safety levels**: L0–L4 permission grading for every action -//! - **Deadlock detection**: terminates after 3 rounds with no progress -//! - **Plan mode**: optional user-approval gate before execution -//! - **Round recording**: full audit trail for debugging and resumption -//! -//! # Architecture -//! -//! The [`OraoExecutor`] runs the O→R→A→O loop: -//! 1. **Observe** — LLM explores environment via observation tools, produces snapshot -//! 2. **Reason** — LLM analyzes snapshot, generates structured plan -//! 3. **Act** — Execute each planned action via [`ActionExecutor`] with safety checks -//! 4. **Observe** — Collect results, feed into next round -//! -//! All file access goes through function calls (tools), never direct filesystem operations. -//! -//! [`ActionExecutor`]: act::ActionExecutor - -pub mod act; -pub mod observe; -pub mod reason; -pub mod types; - -use std::time::Instant; - -use crate::client::AiClientConfig; -use crate::error::{AgentError, Result}; - -pub use act::ActionExecutor; -pub use types::{ - ActionResult, ActionType, ActionVerdict, FileChange, FileChangeType, OraoConfig, OraoStep, - PerceptionSnapshot, PlannedAction, ReasoningOutput, RoundRecord, SafetyLevel, -}; - -// ── ORAO Executor ─────────────────────────────────────────────────────────── - -/// Executes the ORAO loop for a single task. -/// -/// All environment interaction goes through: -/// - **Observation tools** (read-only) for the Observe phase -/// - **Action executor** callback for the Act phase -/// -/// No direct filesystem access — everything is mediated through function calls. -pub struct OraoExecutor { - config: AiClientConfig, - model_name: String, - action_executor: ActionExecutor, -} - -impl OraoExecutor { - /// Create a new ORAO executor. - /// - /// `action_executor` is called to execute each planned action. Wire it to - /// your [`ToolRegistry`] for tool-based execution, or use - /// [`act::shell_executor`] for simple shell-command execution. - /// - /// [`ToolRegistry`]: crate::tool::ToolRegistry - pub fn new( - config: AiClientConfig, - model_name: impl Into, - action_executor: ActionExecutor, - ) -> Self { - Self { - config, - model_name: model_name.into(), - action_executor, - } - } - - /// Run the ORAO loop to completion. - /// - /// # Parameters - /// - `task_goal`: Description of what to accomplish. - /// - `orao_config`: ORAO-specific settings (max rounds, safety level, etc.). - /// - `tool_factory`: Called each round to produce read-only observation tools - /// (e.g. `git_diff`, `git_blob`, `repo_search`, `git_grep`). This allows - /// callers to provide fresh tool instances each round. - /// - `on_step`: Called with each [`OraoStep`] event for streaming/persistence. - /// - `on_plan_approval`: Called in plan mode; return `true` to proceed. - pub async fn execute( - &self, - task_goal: &str, - orao_config: &OraoConfig, - tool_factory: TF, - on_step: C, - on_plan_approval: PA, - ) -> Result - where - C: Fn(OraoStep) -> Fut + Send, - Fut: Future + Send, - PA: Fn(ReasoningOutput) -> PAFut + Send, - PAFut: Future + Send, - TF: Fn() -> Vec> + Send + Sync, - { - let mut round = 0usize; - let mut round_records: Vec = Vec::new(); - let mut previous_result: Option = None; - let mut previous_snapshot: Option = None; - let mut no_change_count: usize = 0; - - // Observation turns: limit tool calls during exploration - let observe_max_turns = 10; - - loop { - round += 1; - let round_start = Instant::now(); - let round_input_tokens: u64 = 0; - let round_output_tokens: u64 = 0; - - // ── Phase 1: Observe ─────────────────────────────────────── - let snapshot = observe::observe( - &self.config, - &self.model_name, - task_goal, - previous_result.take(), - tool_factory(), - observe_max_turns, - ) - .await?; - - on_step(OraoStep::Observe { - round, - snapshot: snapshot.clone(), - }) - .await; - - // ── Deadlock detection ───────────────────────────────────── - if let Some(ref prev) = previous_snapshot { - if !observe::has_environment_changed(prev, &snapshot) { - no_change_count += 1; - if no_change_count >= orao_config.deadlock_threshold { - let reason = format!( - "Deadlock detected: no environmental change for {} consecutive rounds", - no_change_count - ); - on_step(OraoStep::Failed { - total_rounds: round, - reason: reason.clone(), - }) - .await; - return Ok(OraoOutcome::Failed { - reason, - rounds: round, - records: round_records, - }); - } - } else { - no_change_count = 0; - } - } - previous_snapshot = Some(snapshot.clone()); - - // ── Phase 2: Reason ──────────────────────────────────────── - let reasoning = reason::reason( - &self.config, - &self.model_name, - orao_config, - task_goal, - &snapshot, - round, - &round_records, - ) - .await?; - - on_step(OraoStep::Reason { - round, - reasoning: reasoning.clone(), - }) - .await; - - // ── Plan mode gate ───────────────────────────────────────── - if orao_config.plan_mode { - on_step(OraoStep::PlanProposed { - round, - reasoning: reasoning.clone(), - }) - .await; - - if !on_plan_approval(reasoning.clone()).await { - return Ok(OraoOutcome::Cancelled { - rounds: round, - records: round_records, - }); - } - } - - // ── Phase 3: Act ─────────────────────────────────────────── - let mut round_result: Option = None; - let mut all_success = true; - - for planned in &reasoning.plan { - let safety = SafetyLevel::classify_command(&planned.command_or_content); - - on_step(OraoStep::Act { - round, - action: planned.clone(), - safety_level: safety, - }) - .await; - - let result = - act::execute_action(planned.clone(), orao_config, &self.action_executor).await; - - on_step(OraoStep::ObserveResult { - round, - result: result.clone(), - }) - .await; - - match &result.verdict { - ActionVerdict::Failure => { - all_success = false; - round_result = Some(result); - break; // Stop executing further steps on failure - } - ActionVerdict::SuccessWithWarnings => { - round_result = Some(result); - } - ActionVerdict::Success => { - round_result = Some(result); - } - } - } - - // ── Phase 4: Record round ────────────────────────────────── - let duration_ms = round_start.elapsed().as_millis() as u64; - let record = RoundRecord { - round, - observe_summary: summarize_snapshot(&snapshot), - reasoning_summary: reasoning.analysis.clone(), - action: reasoning.plan.first().cloned(), - result_summary: round_result - .as_ref() - .map(|r| format!("{:?}: {}", r.verdict, truncate(&r.stdout, 200))), - tokens_input: round_input_tokens, - tokens_output: round_output_tokens, - duration_ms, - }; - round_records.push(record); - - // ── Check termination ────────────────────────────────────── - if all_success && !reasoning.plan.is_empty() { - let summary = format!( - "Task completed in {} round(s). Last action: {}", - round, - round_result - .as_ref() - .map(|r| truncate(&r.stdout, 500)) - .unwrap_or_default() - ); - on_step(OraoStep::Completed { - total_rounds: round, - summary: summary.clone(), - }) - .await; - return Ok(OraoOutcome::Completed { - summary, - rounds: round, - records: round_records, - }); - } - - // Max rounds exceeded - if round >= orao_config.max_rounds { - let reason = format!("Reached max rounds ({})", orao_config.max_rounds); - on_step(OraoStep::Failed { - total_rounds: round, - reason: reason.clone(), - }) - .await; - return Ok(OraoOutcome::Failed { - reason, - rounds: round, - records: round_records, - }); - } - - // Prepare for next round - previous_result = round_result; - } - } -} - -// ── Outcome ───────────────────────────────────────────────────────────────── - -/// Final outcome of an ORAO execution. -#[derive(Debug, Clone)] -pub enum OraoOutcome { - /// Task completed successfully. - Completed { - summary: String, - rounds: usize, - records: Vec, - }, - /// Task failed (max rounds, deadlock, or unrecoverable error). - Failed { - reason: String, - rounds: usize, - records: Vec, - }, - /// User cancelled the task (plan mode rejection or explicit interrupt). - Cancelled { - rounds: usize, - records: Vec, - }, -} - -impl OraoOutcome { - /// Number of rounds executed. - pub fn rounds(&self) -> usize { - match self { - Self::Completed { rounds, .. } - | Self::Failed { rounds, .. } - | Self::Cancelled { rounds, .. } => *rounds, - } - } - - /// Whether the task was successful. - pub fn is_success(&self) -> bool { - matches!(self, Self::Completed { .. }) - } - - /// Round records for audit/debugging. - pub fn records(&self) -> &[RoundRecord] { - match self { - Self::Completed { records, .. } - | Self::Failed { records, .. } - | Self::Cancelled { records, .. } => records, - } - } -} - -// ── Helpers ───────────────────────────────────────────────────────────────── - -fn summarize_snapshot(snapshot: &PerceptionSnapshot) -> String { - let mut parts: Vec = Vec::new(); - - if let Some(ref gs) = snapshot.git_status { - let first_line = gs.lines().next().unwrap_or(""); - parts.push(format!("git: {}", truncate(first_line, 80))); - } - - if !snapshot.files.is_empty() { - parts.push(format!("{} files", snapshot.files.len())); - } - - if !snapshot.errors.is_empty() { - parts.push(format!("{} errors", snapshot.errors.len())); - } - - if parts.is_empty() { - "no changes".to_string() - } else { - parts.join(", ") - } -} - -fn truncate(s: &str, max_len: usize) -> String { - if s.len() <= max_len { - s.to_string() - } else { - format!("{}...", &s[..max_len]) - } -} - -// ── Convenience builder ───────────────────────────────────────────────────── - -/// Builder for [`OraoExecutor`] with chainable configuration. -pub struct OraoExecutorBuilder { - config: Option, - model_name: Option, - action_executor: Option, -} - -impl OraoExecutorBuilder { - pub fn new() -> Self { - Self { - config: None, - model_name: None, - action_executor: None, - } - } - - pub fn ai_config(mut self, config: AiClientConfig) -> Self { - self.config = Some(config); - self - } - - pub fn model(mut self, name: impl Into) -> Self { - self.model_name = Some(name.into()); - self - } - - pub fn action_executor(mut self, executor: ActionExecutor) -> Self { - self.action_executor = Some(executor); - self - } - - pub fn build(self) -> Result { - let config = self.config.ok_or_else(|| AgentError::InvalidInput { - field: "config".to_string(), - reason: "AI client config is required".to_string(), - })?; - let model_name = self.model_name.ok_or_else(|| AgentError::InvalidInput { - field: "model_name".to_string(), - reason: "Model name is required".to_string(), - })?; - let action_executor = self - .action_executor - .ok_or_else(|| AgentError::InvalidInput { - field: "action_executor".to_string(), - reason: "Action executor is required".to_string(), - })?; - - Ok(OraoExecutor::new(config, model_name, action_executor)) - } -} - -impl Default for OraoExecutorBuilder { - fn default() -> Self { - Self::new() - } -} diff --git a/libs/agent/orao/observe.rs b/libs/agent/orao/observe.rs deleted file mode 100644 index 108d2be..0000000 --- a/libs/agent/orao/observe.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! Observe phase: LLM-driven multi-channel environment perception. -//! -//! The Observe phase gives the LLM a set of read-only observation tools and -//! instructs it to explore the environment. All file/git/system access goes -//! through function calls (tools), never direct filesystem operations. -//! -//! After exploration, the LLM produces a structured [`PerceptionSnapshot`] -//! summarizing the current state of the project. - -use rig::agent::AgentBuilder; -use rig::client::CompletionClient; -use rig::completion::Prompt; - -use crate::client::AiClientConfig; -use crate::error::AgentError; - -use super::types::{ActionResult, PerceptionSnapshot}; - -/// Prompt for the ORAO Observe phase. -const OBSERVE_SYSTEM_PROMPT: &str = r#"You are an expert software engineering agent using the ORAO (Observe-Reason-Act-Observe) framework. - -## Your Role: OBSERVE Phase - -You are currently in the OBSERVE phase. Your task is to explore the project environment -and gather all relevant information using the available tools. - -## What to Observe - -Use the tools provided to you to check: - -1. **Git status**: What branch are we on? What files have changed? Any uncommitted work? -2. **Project structure**: What directories and key files exist? -3. **Code content**: Read relevant source files to understand the codebase state. -4. **Errors/warnings**: Check build output, test results, linter output for issues. -5. **Configuration**: Check project config files (Cargo.toml, package.json, etc.) if relevant. - -## Rules - -- Use tools to explore — do NOT guess or assume file contents. -- Focus on information relevant to the task at hand. -- Be thorough but efficient: 3-8 tool calls is typical. -- After gathering information, summarize your findings clearly. - -## Output Format - -After you have finished observing, provide a summary with these sections: - -### Git Status -[Current branch, changed files, commit status] - -### Project Structure -[Key directories and files relevant to the task] - -### Key Files -[Important files you read, with brief notes on their content] - -### Errors / Issues -[Any errors, warnings, or problems detected] - -### Previous Action Result -[If a previous action was executed, describe its outcome]"#; - -/// Run the Observe phase: let the LLM explore the environment via tools. -/// -/// Returns a structured [`PerceptionSnapshot`] built from the LLM's observations. -/// All environment access goes through the provided `tools` — no direct -/// filesystem operations. -/// -/// Takes ownership of `tools` (caller must clone if they need to reuse them). -pub async fn observe( - config: &AiClientConfig, - model_name: &str, - task_goal: &str, - previous_result: Option, - tools: Vec>, - max_turns: usize, -) -> Result { - let user_prompt = build_observe_prompt(task_goal, previous_result.as_ref()); - - let client = config.build_rig_client(); - let model = client.completion_model(model_name); - - let agent = AgentBuilder::new(model) - .preamble(OBSERVE_SYSTEM_PROMPT) - .tools(tools) - .default_max_turns(max_turns) - .build(); - - let response = agent - .prompt(&user_prompt) - .max_turns(max_turns) - .extended_details() - .await - .map_err(|e: rig::completion::PromptError| AgentError::OpenAi(e.to_string()))?; - - // Build snapshot from the LLM's final summary - let summary = response.output; - let snapshot = parse_observation_summary(&summary, previous_result); - - Ok(snapshot) -} - -/// Build the user prompt for the Observe phase. -fn build_observe_prompt(task_goal: &str, previous_result: Option<&ActionResult>) -> String { - let mut prompt = format!( - "## Task Goal\n\n{}\n\n## Instructions\n\n\ - Explore the project environment using the available tools. \ - Gather all information relevant to the task above. \ - After you have gathered sufficient information, provide a structured summary.", - task_goal - ); - - if let Some(prev) = previous_result { - prompt.push_str(&format!( - "\n\n## Previous Action Result\n\n\ - - Action: {}\n\ - - Verdict: {:?}\n\ - - Exit code: {:?}\n\ - - stdout: {}\n\ - - stderr: {}", - prev.action.description, - prev.verdict, - prev.exit_code, - truncate_str(&prev.stdout, 2000), - truncate_str(&prev.stderr, 2000), - )); - } - - prompt -} - -/// Parse the LLM's observation summary into a structured snapshot. -fn parse_observation_summary( - summary: &str, - previous_result: Option, -) -> PerceptionSnapshot { - let mut snapshot = PerceptionSnapshot::default(); - - // Extract sections from the markdown summary - let mut current_section = ""; - let mut section_content: Vec<&str> = Vec::new(); - - for line in summary.lines() { - if line.starts_with("### ") { - // Save previous section - store_section(&mut snapshot, current_section, §ion_content); - current_section = line.trim_start_matches("### ").trim(); - section_content.clear(); - } else { - section_content.push(line); - } - } - // Save last section - store_section(&mut snapshot, current_section, §ion_content); - - snapshot.previous_action_result = previous_result; - - // If no structured data was parsed, store the raw summary - if snapshot.git_status.is_none() - && snapshot.project_structure.is_none() - && snapshot.files.is_empty() - && snapshot.errors.is_empty() - { - snapshot - .notes - .insert("raw_observation".to_string(), summary.to_string()); - } - - snapshot -} - -fn store_section(snapshot: &mut PerceptionSnapshot, section: &str, content: &[&str]) { - let text = content.join("\n").trim().to_string(); - if text.is_empty() { - return; - } - - match section.to_lowercase().as_str() { - s if s.contains("git") => { - snapshot.git_status = Some(text); - } - s if s.contains("project") && s.contains("structure") => { - snapshot.project_structure = Some(text); - } - s if s.contains("file") => { - // Parse file references from the text - for line in content { - let line = line.trim(); - if let Some(path) = extract_file_path(line) { - snapshot.files.push(super::types::PerceivedFile { - path, - size_bytes: 0, - content_preview: None, - }); - } - } - } - s if s.contains("error") || s.contains("issue") || s.contains("warning") => { - for line in content { - let line = line.trim(); - if !line.is_empty() && !line.starts_with('#') { - snapshot.errors.push(line.to_string()); - } - } - } - _ => { - // Store unknown sections as notes - snapshot.notes.insert(section.to_string(), text); - } - } -} - -/// Extract a file path from a markdown list item or code reference. -fn extract_file_path(line: &str) -> Option { - // Match patterns like: - `src/main.rs` or - src/main.rs or `src/main.rs` - let line = line.trim(); - - // Backtick-wrapped path - if let Some(start) = line.find('`') { - let rest = &line[start + 1..]; - if let Some(end) = rest.find('`') { - let path = rest[..end].to_string(); - if path.contains('.') || path.contains('/') || path.contains('\\') { - return Some(path); - } - } - } - - // Bare path pattern (word chars, slashes, dots) - if line.starts_with('-') || line.starts_with('*') { - let rest = line.trim_start_matches(&['-', '*', ' ']); - if rest.contains('/') || (rest.contains('.') && !rest.starts_with("http")) { - return Some(rest.to_string()); - } - } - - None -} - -fn truncate_str(s: &str, max_len: usize) -> String { - if s.len() <= max_len { - s.to_string() - } else { - format!("{}...", &s[..max_len]) - } -} - -/// Determine whether the environment has changed since the last snapshot. -/// -/// Used for deadlock detection: if 3 consecutive rounds show no change, -/// the loop is terminated. -pub fn has_environment_changed( - previous: &PerceptionSnapshot, - current: &PerceptionSnapshot, -) -> bool { - if previous.git_status != current.git_status { - return true; - } - - let prev_files: Vec<&str> = previous.files.iter().map(|f| f.path.as_str()).collect(); - let curr_files: Vec<&str> = current.files.iter().map(|f| f.path.as_str()).collect(); - if prev_files != curr_files { - return true; - } - - if previous.errors != current.errors { - return true; - } - - let prev_has_result = previous.previous_action_result.is_some(); - let curr_has_result = current.previous_action_result.is_some(); - if prev_has_result != curr_has_result { - return true; - } - - false -} diff --git a/libs/agent/orao/reason.rs b/libs/agent/orao/reason.rs deleted file mode 100644 index d151ac4..0000000 --- a/libs/agent/orao/reason.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! Reason phase: structured reasoning and plan generation via LLM. -//! -//! Takes the current [`PerceptionSnapshot`] and the task goal, sends them to -//! the configured LLM, and produces a structured [`ReasoningOutput`] with an -//! executable plan. - -use crate::client::AiClientConfig; -use crate::error::AgentError; -use rig::agent::AgentBuilder; -use rig::client::CompletionClient; -use rig::completion::Prompt; - -use super::types::{OraoConfig, PerceptionSnapshot, ReasoningOutput}; - -/// Prompt template for the ORAO reasoning phase. -const REASON_SYSTEM_PROMPT: &str = r#"You are an expert software engineering agent using the ORAO (Observe-Reason-Act-Observe) framework. - -## Your Role: REASON Phase - -You are currently in the REASON phase. You will receive: -1. A **task goal** — what needs to be accomplished -2. An **observation snapshot** — the current state of the environment - -Your job is to produce a structured analysis and a step-by-step action plan. - -## Output Format - -You MUST respond with a valid JSON object matching this schema: - -```json -{ - "analysis": "", - "plan": [ - { - "step_id": 1, - "description": "", - "action_type": "shell_command | file_write | file_edit | git_operation | tool_invoke | user_dialog", - "command_or_content": "", - "expected_result": "", - "fallback_on_failure": "" - } - ] -} -``` - -## Rules - -1. **Be specific**: commands must be exact and complete (include necessary flags, paths, etc.) -2. **One action per step**: each step should do one thing -3. **Validate assumptions**: don't assume dependencies are installed or files exist -4. **Prefer small, safe steps**: each change should be minimal and reversible -5. **Include verification**: build/test after changes to verify correctness -6. **Plan size**: 1-10 steps is typical. Don't plan more than 15 steps without asking the user. -7. **Fallbacks**: for risky steps, specify what to try if the step fails - -## Safety - -- Prefer read-only verification before making changes -- Use version control (git) for reversible operations -- Flag any step that requires network access or system-level privileges"#; - -/// Run the reasoning phase: send observation + task to the LLM and get a plan. -pub async fn reason( - config: &AiClientConfig, - model_name: &str, - _orao_config: &OraoConfig, - task_goal: &str, - snapshot: &PerceptionSnapshot, - round: usize, - history_rounds: &[super::types::RoundRecord], -) -> Result { - let user_prompt = build_reason_prompt(task_goal, snapshot, round, history_rounds); - - let client = config.build_rig_client(); - let model = client.completion_model(model_name); - - let agent = AgentBuilder::new(model) - .preamble(REASON_SYSTEM_PROMPT) - .build(); - - let response = agent - .prompt(&user_prompt) - .extended_details() - .await - .map_err(|e: rig::completion::PromptError| AgentError::OpenAi(e.to_string()))?; - - let output = response.output.trim().to_string(); - - // Extract JSON from the response (it may be wrapped in ```json fences) - let json_str = extract_json(&output); - - serde_json::from_str::(json_str).map_err(|e| { - AgentError::Internal(format!( - "Failed to parse reasoning output as JSON: {}. Raw output: {}", - e, output - )) - }) -} - -/// Build the user prompt for the reason phase. -fn build_reason_prompt( - task_goal: &str, - snapshot: &PerceptionSnapshot, - round: usize, - history_rounds: &[super::types::RoundRecord], -) -> String { - let mut prompt = format!( - "## Task Goal\n\n{}\n\n## Round Number\n\n{}\n\n", - task_goal, round - ); - - // ── Observation snapshot ──────────────────────────────────────────── - prompt.push_str("## Current Observation\n\n"); - - if let Some(ref git_status) = snapshot.git_status { - prompt.push_str(&format!("### Git Status\n```\n{}\n```\n\n", git_status)); - } - - if let Some(ref project_structure) = snapshot.project_structure { - prompt.push_str(&format!( - "### Project Structure\n```\n{}\n```\n\n", - project_structure - )); - } - - if !snapshot.files.is_empty() { - prompt.push_str("### Relevant Files\n\n"); - for file in &snapshot.files { - prompt.push_str(&format!("- `{}` ({} bytes)\n", file.path, file.size_bytes)); - if let Some(ref preview) = file.content_preview { - let truncated = if preview.len() > 2000 { - format!("{}... [truncated]", &preview[..2000]) - } else { - preview.clone() - }; - prompt.push_str(&format!("```\n{}\n```\n\n", truncated)); - } - } - } - - if !snapshot.errors.is_empty() { - prompt.push_str("### Errors / Warnings\n\n"); - for err in &snapshot.errors { - prompt.push_str(&format!("- {}\n", err)); - } - prompt.push('\n'); - } - - if let Some(ref prev_result) = snapshot.previous_action_result { - prompt.push_str(&format!( - "### Previous Action Result\n- Verdict: {:?}\n- Exit code: {:?}\n- stdout: {}\n- stderr: {}\n\n", - prev_result.verdict, - prev_result.exit_code, - truncate_str(&prev_result.stdout, 1000), - truncate_str(&prev_result.stderr, 1000), - )); - } - - // ── History summary ───────────────────────────────────────────────── - if !history_rounds.is_empty() { - prompt.push_str("## Previous Rounds\n\n"); - for record in history_rounds.iter().rev().take(3) { - prompt.push_str(&format!( - "- Round {}: {} | {} tokens | {}ms\n", - record.round, - truncate_str(&record.reasoning_summary, 120), - record.tokens_input + record.tokens_output, - record.duration_ms, - )); - } - prompt.push('\n'); - } - - prompt.push_str( - "## Instructions\n\nProduce a structured analysis and action plan in JSON format as specified.", - ); - - prompt -} - -/// Extract JSON content from a string that may be wrapped in markdown fences. -fn extract_json(s: &str) -> &str { - let s = s.trim(); - - if s.starts_with("```json") { - let inner = &s[7..]; - if let Some(end) = inner.rfind("```") { - return inner[..end].trim(); - } - return inner.trim(); - } - - if s.starts_with("```") { - let inner = &s[3..]; - if let Some(end) = inner.rfind("```") { - return inner[..end].trim(); - } - return inner.trim(); - } - - // Heuristic: find first { and last } - if let (Some(start), Some(end)) = (s.find('{'), s.rfind('}')) { - return &s[start..=end]; - } - - s -} - -fn truncate_str(s: &str, max_len: usize) -> String { - if s.len() <= max_len { - s.to_string() - } else { - format!("{}...", &s[..max_len]) - } -} diff --git a/libs/agent/orao/types.rs b/libs/agent/orao/types.rs deleted file mode 100644 index c899cf5..0000000 --- a/libs/agent/orao/types.rs +++ /dev/null @@ -1,445 +0,0 @@ -//! ORAO core types. -//! -//! ORAO (Observe–Reason–Act–Observe) is a single-agent loop paradigm for complex -//! engineering tasks. It extends ReAct with structured multi-channel perception, -//! safety permission levels, plan mode, and deadlock detection. - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -// ── Safety levels ─────────────────────────────────────────────────────────── - -/// Permission level for actions executed by ORAO. -/// -/// L0 (read-only) → auto-allow. -/// L1 (local write) → confirm on first use. -/// L2 (build) → confirm on first use. -/// L3 (network) → explicit user approval required. -/// L4 (system) → denied by default. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -pub enum SafetyLevel { - /// L0 — Read-only: `ls`, `cat`, `grep`, `git status` - ReadOnly = 0, - /// L1 — Local write: edit source files, create new files - LocalWrite = 1, - /// L2 — Build/test: `cargo build`, `npm test` - Build = 2, - /// L3 — Network: `pip install`, `curl` - Network = 3, - /// L4 — System: `sudo`, global config changes - System = 4, -} - -impl SafetyLevel { - /// Classify a shell command into a safety level. - pub fn classify_command(cmd: &str) -> Self { - let cmd_trimmed = cmd.trim(); - // L0: read-only commands - let l0_prefixes = [ - "ls", - "cat", - "head", - "tail", - "less", - "file", - "stat", - "wc", - "grep", - "rg", - "find", - "which", - "type", - "echo", - "printf", - "pwd", - "env", - "printenv", - "date", - "uname", - "hostname", - "git status", - "git log", - "git diff", - "git show", - "git branch", - "git tag", - "git remote", - "git config --get", - "git blame", - "cargo metadata", - "cargo tree", - "cargo read-manifest", - "tree", - "du", - "df", - ]; - for p in &l0_prefixes { - if cmd_trimmed.starts_with(p) { - return Self::ReadOnly; - } - } - - // L4: system-level commands (denied by default) - let l4_patterns = [ - "sudo", - "su ", - "chown", - "chmod 777", - "mkfs", - "mkswap", - "mount", - "umount", - "fdisk", - "parted", - "dd if=", - "systemctl", - "service ", - "chkconfig", - "update-rc.d", - "passwd", - "useradd", - "userdel", - "usermod", - "groupadd", - "iptables", - "ufw", - "firewall-cmd", - "shutdown", - "reboot", - "halt", - "poweroff", - "rm -rf /", - "rm -rf ~", - "rm -rf .", - ":(){ :|:& };:", - ]; - for p in &l4_patterns { - if cmd_trimmed.starts_with(p) || cmd_trimmed.contains(p) { - return Self::System; - } - } - - // L3: network commands - let l3_prefixes = [ - "curl", - "wget", - "nc ", - "ncat", - "telnet", - "ssh ", - "scp", - "rsync", - "pip install", - "pip3 install", - "npm install", - "npm i ", - "yarn add", - "cargo install", - "gem install", - "go get", - "go install", - "apt-get", - "apt ", - "yum ", - "dnf ", - "brew ", - "pacman ", - "zypper", - "docker pull", - "docker run", - "git clone", - "git fetch", - "git push", - "git pull", - "gh ", - "glab ", - "aws ", - "gcloud ", - "az ", - ]; - for p in &l3_prefixes { - if cmd_trimmed.starts_with(p) { - return Self::Network; - } - } - - // L2: build/test commands - let l2_prefixes = [ - "cargo build", - "cargo test", - "cargo check", - "cargo clippy", - "cargo fmt", - "cargo run", - "cargo bench", - "cargo doc", - "npm test", - "npm run", - "npx ", - "yarn test", - "yarn run", - "pnpm test", - "pnpm run", - "bun test", - "bun run", - "make", - "cmake", - "ninja", - "meson", - "bazel", - "pytest", - "python -m pytest", - "python3 -m pytest", - "go test", - "go build", - "go vet", - "go fmt", - "rustc", - "rustfmt", - "clippy", - "miri", - "eslint", - "prettier", - "tsc", - "jest", - "vitest", - "docker build", - "docker compose", - "docker-compose", - "kubectl apply", - "kubectl delete", - "helm ", - ]; - for p in &l2_prefixes { - if cmd_trimmed.starts_with(p) { - return Self::Build; - } - } - - // Default to L1 (local write) for anything else - Self::LocalWrite - } -} - -// ── Action types ──────────────────────────────────────────────────────────── - -/// The type of action to execute. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ActionType { - /// Execute a shell command in a controlled terminal. - ShellCommand, - /// Create or overwrite a file. - FileWrite, - /// Make a localized edit to an existing file. - FileEdit, - /// Version-control operation (commit, add, etc.). - GitOperation, - /// Invoke an external tool or API. - ToolInvoke, - /// Ask the user for input or a decision. - UserDialog, -} - -// ── Action plan ───────────────────────────────────────────────────────────── - -/// A single planned action from the reasoning phase. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PlannedAction { - /// Step number within the plan. - pub step_id: usize, - /// Human-readable description. - pub description: String, - /// The type of action. - pub action_type: ActionType, - /// The command or content to execute/write. - pub command_or_content: String, - /// What success should look like. - pub expected_result: String, - /// What to try if this step fails. - pub fallback_on_failure: Option, -} - -/// Structured reasoning output from the Reason phase. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReasoningOutput { - /// Analysis of the current state. - pub analysis: String, - /// The plan to execute. - pub plan: Vec, -} - -// ── Perception snapshot ───────────────────────────────────────────────────── - -/// Structured observation collected during the Observe phase. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct PerceptionSnapshot { - /// Project directory tree summary. - pub project_structure: Option, - /// Relevant file paths and contents. - pub files: Vec, - /// Current errors/warnings in the environment. - pub errors: Vec, - /// Git status summary. - pub git_status: Option, - /// Result of the previous action (if any). - pub previous_action_result: Option, - /// Free-form context notes. - pub notes: HashMap, -} - -/// A file observed during perception. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PerceivedFile { - pub path: String, - pub size_bytes: u64, - pub content_preview: Option, -} - -// ── Action result ─────────────────────────────────────────────────────────── - -/// The result of executing an action. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ActionResult { - /// The action that was executed. - pub action: PlannedAction, - /// Exit code (0 = success for shell commands). - pub exit_code: Option, - /// Captured stdout. - pub stdout: String, - /// Captured stderr. - pub stderr: String, - /// Summary of file changes (if applicable). - pub file_changes: Vec, - /// Preliminary assessment. - pub verdict: ActionVerdict, -} - -/// A file change detected after an action. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileChange { - pub path: String, - pub change_type: FileChangeType, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum FileChangeType { - Created, - Modified, - Deleted, -} - -/// Preliminary verdict on an action's outcome. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ActionVerdict { - Success, - SuccessWithWarnings, - Failure, -} - -// ── ORAO step events ──────────────────────────────────────────────────────── - -/// A single event emitted during an ORAO round, analogous to `ReactStep`. -/// -/// These are yielded via the streaming callback so the caller can persist -/// them or forward them to a frontend. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum OraoStep { - /// Initial observation: environment snapshot before any action. - Observe { - round: usize, - snapshot: PerceptionSnapshot, - }, - /// The reasoning/analysis output, including the plan. - Reason { - round: usize, - reasoning: ReasoningOutput, - }, - /// An action is about to be executed. - Act { - round: usize, - action: PlannedAction, - safety_level: SafetyLevel, - }, - /// The result observed after executing an action. - ObserveResult { round: usize, result: ActionResult }, - /// Plan mode: a plan has been generated and is awaiting user approval. - PlanProposed { - round: usize, - reasoning: ReasoningOutput, - }, - /// The task completed successfully. - Completed { - total_rounds: usize, - summary: String, - }, - /// The task failed (max rounds, deadlock, or explicit failure). - Failed { total_rounds: usize, reason: String }, -} - -// ── Round record (audit) ──────────────────────────────────────────────────── - -/// A persistent record of one ORAO round, used for audit and resumption. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RoundRecord { - /// Round number (1-indexed). - pub round: usize, - /// Summary of the Observe phase. - pub observe_summary: String, - /// Summary of the Reasoning phase. - pub reasoning_summary: String, - /// The action that was executed. - pub action: Option, - /// Result observed after the action. - pub result_summary: Option, - /// Tokens consumed this round. - pub tokens_input: u64, - pub tokens_output: u64, - /// Wall-clock duration of this round in milliseconds. - pub duration_ms: u64, -} - -// ── ORAO configuration ────────────────────────────────────────────────────── - -/// Configuration for an ORAO execution. -#[derive(Clone)] -pub struct OraoConfig { - /// Maximum number of ORAO rounds before giving up. - pub max_rounds: usize, - /// Maximum allowed safety level. Actions above this level are denied. - pub max_safety_level: SafetyLevel, - /// Whether to run in plan mode (generate plan first, wait for approval). - pub plan_mode: bool, - /// Whether to enable extended thinking for the reasoning phase. - pub extended_thinking: bool, - /// Per-action timeout in seconds. - pub action_timeout_secs: u64, - /// Number of consecutive no-change rounds before deadlock detection triggers. - pub deadlock_threshold: usize, -} - -impl Default for OraoConfig { - fn default() -> Self { - Self { - max_rounds: 50, - max_safety_level: SafetyLevel::Network, - plan_mode: false, - extended_thinking: false, - action_timeout_secs: 120, - deadlock_threshold: 3, - } - } -} - -impl std::fmt::Debug for OraoConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OraoConfig") - .field("max_rounds", &self.max_rounds) - .field("max_safety_level", &self.max_safety_level) - .field("plan_mode", &self.plan_mode) - .field("extended_thinking", &self.extended_thinking) - .field("action_timeout_secs", &self.action_timeout_secs) - .field("deadlock_threshold", &self.deadlock_threshold) - .finish() - } -} diff --git a/libs/agent/perception/active.rs b/libs/agent/perception/active.rs deleted file mode 100644 index fabc650..0000000 --- a/libs/agent/perception/active.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Active skill awareness: explicit user intent. -//! -//! Active detection has the highest priority. It only fires when the user -//! directly references a skill by slug, name, mention, or clear "use this" -//! wording. - -use super::{SkillActivation, SkillContext, SkillEntry, normalize_skill_key}; -use once_cell::sync::Lazy; -use regex::Regex; - -#[derive(Debug, Clone, Default)] -pub struct ActiveSkillAwareness; - -impl ActiveSkillAwareness { - pub fn new() -> Self { - Self - } - - pub fn detect(&self, input: &str, skills: &[SkillEntry]) -> Option { - let input_lower = input.to_lowercase(); - - self.match_by_prefix_pattern(&input_lower, skills) - .or_else(|| self.match_by_name(&input_lower, skills)) - .or_else(|| self.match_by_slug_substring(&input_lower, skills)) - } - - fn match_by_prefix_pattern(&self, input: &str, skills: &[SkillEntry]) -> Option { - static USE_PAT: Lazy = - Lazy::new(|| Regex::new(r"(?i)^\s*(?:use|using|apply|with)\s+([a-z0-9/_-]+)").unwrap()); - static SKILL_COLON_PAT: Lazy = - Lazy::new(|| Regex::new(r"(?i)skill\s*:\s*([a-z0-9/_-]+)").unwrap()); - static AT_PAT: Lazy = - Lazy::new(|| Regex::new(r"@([a-z0-9][a-z0-9_/-]*[a-z0-9])").unwrap()); - static ZH_PAT: Lazy = Lazy::new(|| { - Regex::new( - r"(?i)(?:使用|用|应用|启用|调用|帮我|帮忙|做一次|执行|进行)\s*([a-z0-9][a-z0-9_/\-]{0,60})", - ) - .unwrap() - }); - - for pattern in [&USE_PAT, &SKILL_COLON_PAT, &AT_PAT, &ZH_PAT] { - if let Some(caps) = pattern.captures(input) { - let slug = caps.get(1)?.as_str().trim(); - if let Some(skill) = self.find_skill_by_slug(slug, skills) { - return Some(skill); - } - if let Some(skill) = self.find_skill_by_name(slug, skills) { - return Some(skill); - } - } - } - - None - } - - fn match_by_name(&self, input: &str, skills: &[SkillEntry]) -> Option { - for skill in skills { - let name_lower = skill.name.to_lowercase(); - let normalized_name = name_lower.replace(['-', '_'], " "); - if input.contains(&name_lower) || input.contains(&normalized_name) { - return Some(Self::context_from_skill(skill)); - } - } - None - } - - fn match_by_slug_substring(&self, input: &str, skills: &[SkillEntry]) -> Option { - let cleaned = input - .replace("please ", "") - .replace("帮我", "") - .replace("帮忙", "") - .replace("使用", "") - .replace("调用", "") - .replace("启用", ""); - - for skill in skills { - let slug = normalize_skill_key(&skill.slug); - if cleaned.contains(&slug) - || slug - .split(['/', '-']) - .any(|seg| seg.len() > 3 && cleaned.contains(seg)) - { - return Some(Self::context_from_skill(skill)); - } - } - None - } - - fn find_skill_by_slug(&self, slug: &str, skills: &[SkillEntry]) -> Option { - let slug_key = normalize_skill_key(slug); - skills - .iter() - .find(|s| normalize_skill_key(&s.slug) == slug_key) - .map(Self::context_from_skill) - } - - fn find_skill_by_name(&self, name: &str, skills: &[SkillEntry]) -> Option { - let name_lower = name.to_lowercase().replace(['-', '_'], " "); - skills - .iter() - .find(|s| s.name.to_lowercase().replace(['-', '_'], " ") == name_lower) - .map(Self::context_from_skill) - } - - fn context_from_skill(skill: &SkillEntry) -> SkillContext { - SkillContext::new( - skill, - SkillActivation::Active, - None, - format!("# {} (actively invoked)\n\n{}", skill.name, skill.content), - None, - ) - } -} diff --git a/libs/agent/perception/auto.rs b/libs/agent/perception/auto.rs deleted file mode 100644 index 94c92f5..0000000 --- a/libs/agent/perception/auto.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! Auto skill awareness: ambient relevance matching. - -use super::{SkillActivation, SkillContext, SkillEntry}; -use std::collections::HashSet; - -#[derive(Debug, Clone)] -pub struct AutoSkillAwareness { - /// Minimum overlap score to consider a skill relevant. - min_score: f32, - /// Maximum number of auto-selected skills. - max_skills: usize, -} - -impl Default for AutoSkillAwareness { - fn default() -> Self { - Self { - min_score: 0.20, - max_skills: 3, - } - } -} - -impl AutoSkillAwareness { - pub fn new(min_score: f32, max_skills: usize) -> Self { - Self { - min_score, - max_skills, - } - } - - pub async fn detect( - &self, - current_input: &str, - history: &[String], - skills: &[SkillEntry], - ) -> Vec { - if skills.is_empty() { - return Vec::new(); - } - - let history_text = history - .iter() - .rev() - .take(5) - .map(String::as_str) - .collect::>() - .join(" "); - let corpus = format!("{} {}", current_input, history_text).to_lowercase(); - let corpus_keywords = Self::extract_keywords(&corpus); - if corpus_keywords.is_empty() { - return Vec::new(); - } - - let mut scored = skills - .iter() - .filter_map(|skill| { - let score = Self::score_skill(&corpus_keywords, skill); - (score >= self.min_score).then_some((score, skill)) - }) - .collect::>(); - - scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); - - scored - .into_iter() - .take(self.max_skills) - .map(|(score, skill)| { - let excerpt = Self::best_excerpt(&corpus, skill); - SkillContext::new(skill, SkillActivation::Auto, None, excerpt, Some(score)) - }) - .collect() - } - - fn extract_keywords(text: &str) -> HashSet { - const STOPWORDS: &[&str] = &[ - "the", "a", "an", "is", "are", "was", "were", "be", "been", "being", "have", "has", - "had", "do", "does", "did", "will", "would", "could", "should", "may", "might", "can", - "to", "of", "in", "for", "on", "with", "at", "by", "from", "as", "or", "and", "but", - "if", "not", "no", "so", "this", "that", "these", "those", "it", "its", "i", "you", - "we", "they", "what", "which", "who", "when", "where", "why", "how", "all", "each", - "every", "more", "most", "some", "such", "only", "same", "than", "too", "very", "just", - "also", "now", "here", "there", "then", - ]; - - let mut terms = HashSet::new(); - let mut ascii = String::new(); - let mut cjk_run = String::new(); - - for ch in text.chars() { - if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' { - if !cjk_run.is_empty() { - Self::push_cjk_terms(&mut terms, &cjk_run); - cjk_run.clear(); - } - ascii.push(ch); - } else if ('\u{4e00}'..='\u{9fff}').contains(&ch) { - if !ascii.is_empty() { - Self::push_ascii_term(&mut terms, &ascii, STOPWORDS); - ascii.clear(); - } - cjk_run.push(ch); - } else { - if !ascii.is_empty() { - Self::push_ascii_term(&mut terms, &ascii, STOPWORDS); - ascii.clear(); - } - if !cjk_run.is_empty() { - Self::push_cjk_terms(&mut terms, &cjk_run); - cjk_run.clear(); - } - } - } - - if !ascii.is_empty() { - Self::push_ascii_term(&mut terms, &ascii, STOPWORDS); - } - if !cjk_run.is_empty() { - Self::push_cjk_terms(&mut terms, &cjk_run); - } - - terms - } - - fn push_ascii_term(terms: &mut HashSet, raw: &str, stopwords: &[&str]) { - let term = raw - .trim_matches(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '-') - .to_lowercase(); - if term.len() >= 3 && !stopwords.contains(&term.as_str()) { - terms.insert(term); - } - } - - fn push_cjk_terms(terms: &mut HashSet, raw: &str) { - let chars = raw.chars().collect::>(); - if chars.len() < 2 { - return; - } - for window in chars.windows(2) { - terms.insert(window.iter().collect()); - } - if chars.len() >= 4 { - terms.insert(chars.iter().collect()); - } - } - - fn score_skill(corpus_keywords: &HashSet, skill: &SkillEntry) -> f32 { - let skill_text = format!( - "{} {} {}", - skill.name, - skill.description.as_deref().unwrap_or(""), - skill.content.chars().take(800).collect::() - ) - .to_lowercase(); - let skill_keywords = Self::extract_keywords(&skill_text); - - if skill_keywords.is_empty() { - return 0.0; - } - - let overlap = corpus_keywords - .iter() - .filter(|kw| { - skill_keywords - .iter() - .any(|sk| sk == *kw || (kw.len() >= 4 && sk.contains(kw.as_str()))) - }) - .count(); - let denominator = corpus_keywords.len().min(skill_keywords.len()).max(1); - overlap as f32 / denominator as f32 - } - - fn best_excerpt(corpus: &str, skill: &SkillEntry) -> String { - let corpus_kws = Self::extract_keywords(corpus); - let best_para = skill - .content - .split('\n') - .filter(|para| !para.trim().is_empty()) - .map(|para| { - let para_kws = Self::extract_keywords(¶.to_lowercase()); - let overlap = corpus_kws - .iter() - .filter(|kw| { - para_kws - .iter() - .any(|pk| pk == *kw || pk.contains(kw.as_str())) - }) - .count(); - (overlap, para) - }) - .filter(|(score, _)| *score > 0) - .max_by_key(|(score, _)| *score); - - if let Some((_, para)) = best_para { - format!("# {} (auto-matched)\n\n{}", skill.name, para.trim()) - } else { - let excerpt = skill.content.chars().take(300).collect::(); - format!("# {} (auto-matched)\n\n{}...", skill.name, excerpt.trim()) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn skill(slug: &str, name: &str, description: &str, content: &str) -> SkillEntry { - SkillEntry { - slug: slug.to_string(), - name: name.to_string(), - description: Some(description.to_string()), - content: content.to_string(), - } - } - - #[tokio::test] - async fn auto_detects_chinese_without_spaces() { - let skills = vec![skill( - "code-review", - "代码审查", - "检查代码安全和性能问题", - "审查变更,发现 bug、安全漏洞和性能风险。", - )]; - let found = AutoSkillAwareness::new(0.10, 3) - .detect("帮我检查这次代码安全问题", &[], &skills) - .await; - assert_eq!(found[0].slug, "code-review"); - } -} diff --git a/libs/agent/perception/mod.rs b/libs/agent/perception/mod.rs deleted file mode 100644 index eaf70a5..0000000 --- a/libs/agent/perception/mod.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! Skill perception system for the AI agent. -//! -//! Skills are injected through three modes: -//! - Active: explicit user invocation, highest priority. -//! - Passive: tool-call or event driven activation. -//! - Auto: ambient keyword relevance. -//! -//! Vector search is merged by the message builder as a semantic auto signal. - -pub mod active; -pub mod auto; -pub mod passive; -pub mod vector; - -pub use active::ActiveSkillAwareness; -pub use auto::AutoSkillAwareness; -pub use passive::PassiveSkillAwareness; -pub use vector::{VectorActiveAwareness, VectorPassiveAwareness}; - -use crate::client::ChatRequestMessage; -use std::collections::HashSet; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum SkillActivation { - Active, - Passive, - Vector, - Auto, -} - -impl SkillActivation { - fn label(self) -> &'static str { - match self { - SkillActivation::Active => "Active", - SkillActivation::Passive => "Passive", - SkillActivation::Vector => "Vector", - SkillActivation::Auto => "Auto", - } - } - - pub fn rank(self) -> u8 { - match self { - SkillActivation::Active => 0, - SkillActivation::Passive => 1, - SkillActivation::Vector => 2, - SkillActivation::Auto => 3, - } - } -} - -/// A chunk of skill context ready to be injected into the message list. -#[derive(Debug, Clone)] -pub struct SkillContext { - /// Stable skill identifier used for de-duplication across trigger sources. - pub slug: String, - /// Human-readable label shown to the AI, e.g. "Active skill: code-review" - pub label: String, - /// The actual skill content to inject. - pub content: String, - /// How this skill was selected. - pub activation: SkillActivation, - /// Optional relevance score. Active/passive matches use `None`. - pub score: Option, -} - -/// Converts skill context into a system message for injection. -impl SkillContext { - pub fn new( - skill: &SkillEntry, - activation: SkillActivation, - reason: Option<&str>, - content: String, - score: Option, - ) -> Self { - let label = match reason { - Some(reason) => format!("{} skill: {} ({})", activation.label(), skill.name, reason), - None => format!("{} skill: {}", activation.label(), skill.name), - }; - Self { - slug: skill.slug.clone(), - label, - content, - activation, - score, - } - } - - pub fn dedupe_key(&self) -> String { - if !self.slug.trim().is_empty() { - return normalize_skill_key(&self.slug); - } - normalize_skill_key(&self.label) - } - - pub fn to_system_message(self) -> ChatRequestMessage { - ChatRequestMessage::system(format!("[{}]\n{}", self.label, self.content)) - } -} - -pub fn normalize_skill_key(value: &str) -> String { - value - .trim() - .to_lowercase() - .replace(['_', ' '], "-") - .chars() - .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '/') - .collect() -} - -/// Unified perception service combining all three modes. -#[derive(Debug, Clone)] -pub struct PerceptionService { - pub auto: AutoSkillAwareness, - pub active: ActiveSkillAwareness, - pub passive: PassiveSkillAwareness, -} - -impl Default for PerceptionService { - fn default() -> Self { - Self { - auto: AutoSkillAwareness::default(), - active: ActiveSkillAwareness::default(), - passive: PassiveSkillAwareness::default(), - } - } -} - -impl PerceptionService { - /// Inject relevant skill context into the message list based on current conversation state. - /// - /// - **auto**: Scans the current input and conversation history for skill-relevant keywords - /// and injects matching skills that are enabled. - /// - **active**: Checks if the user explicitly invoked a skill by slug (e.g. "用 code-review") - /// and injects it. - /// - **passive**: Checks if any tool-call events or prior observations mention a skill - /// slug and injects the matching skill. - /// - /// Returns a list of system messages to prepend to the conversation. - pub async fn inject_skills( - &self, - input: &str, - history: &[String], - tool_calls: &[ToolCallEvent], - enabled_skills: &[SkillEntry], - ) -> Vec { - let mut results = Vec::new(); - let mut seen = HashSet::new(); - - // Active: explicit skill invocation (highest priority) - if let Some(skill) = self.active.detect(input, enabled_skills) { - seen.insert(skill.dedupe_key()); - results.push(skill); - } - - // Passive: triggered by tool-call events - for tc in tool_calls { - if let Some(skill) = self.passive.detect(tc, enabled_skills) { - if seen.insert(skill.dedupe_key()) { - results.push(skill); - } - } - } - - // Auto: keyword-based relevance matching - let auto_results = self.auto.detect(input, history, enabled_skills).await; - for skill in auto_results { - if seen.insert(skill.dedupe_key()) { - results.push(skill); - } - } - - results - } -} - -/// A tool-call event used for passive skill detection. -#[derive(Debug, Clone)] -pub struct ToolCallEvent { - pub tool_name: String, - pub arguments: String, -} - -/// A skill entry from the database, used for matching. -#[derive(Debug, Clone)] -pub struct SkillEntry { - pub slug: String, - pub name: String, - pub description: Option, - pub content: String, -} diff --git a/libs/agent/perception/passive.rs b/libs/agent/perception/passive.rs deleted file mode 100644 index 488761b..0000000 --- a/libs/agent/perception/passive.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Passive skill awareness: tool-call and event driven activation. - -use super::{SkillActivation, SkillContext, SkillEntry, ToolCallEvent, normalize_skill_key}; - -#[derive(Debug, Clone, Default)] -pub struct PassiveSkillAwareness; - -impl PassiveSkillAwareness { - pub fn new() -> Self { - Self - } - - pub fn detect(&self, event: &ToolCallEvent, skills: &[SkillEntry]) -> Option { - let tool_name = event.tool_name.to_lowercase(); - let args = event.arguments.to_lowercase(); - - if let Some(skill) = Self::match_tool_category(&tool_name, skills) { - return Some(Self::context_from_skill(skill, "tool category")); - } - - for skill in skills { - let slug = normalize_skill_key(&skill.slug); - let name = skill.name.to_lowercase(); - - if Self::slug_in_text(&tool_name, &slug) { - return Some(Self::context_from_skill(skill, "tool invocation")); - } - if Self::slug_in_text(&args, &slug) || Self::keyword_match(&args, &name) { - return Some(Self::context_from_skill(skill, "tool arguments")); - } - } - - None - } - - pub fn detect_from_text(&self, text: &str, skills: &[SkillEntry]) -> Option { - let text_lower = text.to_lowercase(); - - skills.iter().find_map(|skill| { - let slug = normalize_skill_key(&skill.slug); - let name = skill.name.to_lowercase(); - (Self::slug_in_text(&text_lower, &slug) || Self::keyword_match(&text_lower, &name)) - .then(|| Self::context_from_skill(skill, "observation match")) - }) - } - - fn match_tool_category<'a>( - tool_name: &str, - skills: &'a [SkillEntry], - ) -> Option<&'a SkillEntry> { - const CATEGORY_MAP: &[(&str, &[&str])] = &[ - ("git_", &["git"]), - ("repo_", &["repo", "repository"]), - ("project_", &["repo", "repository", "project"]), - ("issue_", &["issue", "triage"]), - ("list_issues", &["issue", "triage"]), - ("create_issue", &["issue"]), - ("update_issue", &["issue"]), - ("pr_", &["pr", "pull", "pull-request"]), - ("pull_request_", &["pr", "pull", "pull-request"]), - ("code_review", &["code-review", "review"]), - ("security_", &["security", "review"]), - ("test_", &["test", "testing"]), - ("read_", &["file", "reader"]), - ("git_file", &["file", "reader"]), - ("curl", &["http", "api"]), - ("project_curl", &["http", "api"]), - ]; - - for (prefix, categories) in CATEGORY_MAP { - if tool_name.starts_with(prefix) { - if let Some(skill) = skills.iter().find(|skill| { - let slug = normalize_skill_key(&skill.slug); - let name = skill.name.to_lowercase(); - categories - .iter() - .any(|category| slug.contains(category) || name.contains(category)) - }) { - return Some(skill); - } - } - } - None - } - - fn slug_in_text(text: &str, slug: &str) -> bool { - text.contains(slug) - || slug - .split(['/', '-']) - .filter(|seg| seg.len() >= 3) - .any(|seg| text.contains(seg)) - } - - fn keyword_match(text: &str, name: &str) -> bool { - let significant = name - .split(|c: char| !c.is_alphanumeric()) - .filter(|w| w.len() >= 3) - .collect::>(); - - if significant.len() >= 2 { - significant.iter().all(|w| text.contains(*w)) - } else { - significant.first().is_some_and(|w| text.contains(w)) - } - } - - fn context_from_skill(skill: &SkillEntry, trigger: &str) -> SkillContext { - SkillContext::new( - skill, - SkillActivation::Passive, - Some(trigger), - format!( - "# {} (passive: {})\n\n{}", - skill.name, trigger, skill.content - ), - None, - ) - } -} diff --git a/libs/agent/perception/vector.rs b/libs/agent/perception/vector.rs deleted file mode 100644 index dad6a42..0000000 --- a/libs/agent/perception/vector.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Vector-based skill and memory awareness using Qdrant embeddings. -//! -//! Leverages semantic similarity search to find relevant skills and conversation -//! memories based on vector embeddings. This is more powerful than keyword matching -//! because it captures semantic meaning, not just surface-level word overlap. -//! -//! - **VectorActiveAwareness**: Searches skills by semantic similarity when the user -//! sends a message, finding skills relevant to the conversation topic. -//! -//! - **VectorPassiveAwareness**: Searches past conversation memories to provide relevant -//! historical context when similar topics arise, based on tool-call patterns. - -use crate::client::ChatRequestMessage; -use crate::embed::EmbedService; -use crate::perception::{SkillActivation, SkillContext, SkillEntry, normalize_skill_key}; - -/// Maximum relevant memories to inject. -const MAX_MEMORY_RESULTS: usize = 3; -/// Minimum similarity score (0.0–1.0) for memories. -const MIN_MEMORY_SCORE: f32 = 0.72; -/// Maximum skills to return from vector search. -const MAX_SKILL_RESULTS: usize = 3; -/// Minimum similarity score for skills. -const MIN_SKILL_SCORE: f32 = 0.70; - -/// Vector-based active skill awareness — semantic search for relevant skills. -/// -/// When the user sends a message, this awareness mode searches the Qdrant skill index -/// for skills whose content is semantically similar to the message, even if no keywords -/// match directly. This captures intent beyond explicit skill mentions. -#[derive(Debug, Clone)] -pub struct VectorActiveAwareness { - pub max_skills: usize, - pub min_score: f32, -} - -impl Default for VectorActiveAwareness { - fn default() -> Self { - Self { - max_skills: MAX_SKILL_RESULTS, - min_score: MIN_SKILL_SCORE, - } - } -} - -impl VectorActiveAwareness { - pub fn new(max_skills: usize, min_score: f32) -> Self { - Self { - max_skills, - min_score, - } - } - - /// Search for skills semantically relevant to the user's input. - /// - /// Uses Qdrant vector search within the given project to find skills whose - /// embedded content is similar to `query`. Only returns results above `min_score`. - pub async fn detect( - &self, - embed_service: &EmbedService, - query: &str, - project_uuid: &str, - ) -> Vec { - let results = match embed_service - .search_skills(query, project_uuid, self.max_skills) - .await - { - Ok(results) => results, - Err(_) => return Vec::new(), - }; - - results - .into_iter() - .filter(|r| r.score >= self.min_score) - .map(|r| { - let name = r - .payload - .extra - .as_ref() - .and_then(|v| v.get("name")) - .and_then(|v| v.as_str()) - .unwrap_or("skill") - .to_string(); - let slug = r - .payload - .extra - .as_ref() - .and_then(|v| v.get("slug")) - .and_then(|v| v.as_str()) - .map(normalize_skill_key) - .unwrap_or_else(|| normalize_skill_key(&name)); - let skill = SkillEntry { - slug, - name, - description: None, - content: r.payload.text.clone(), - }; - SkillContext::new( - &skill, - SkillActivation::Vector, - Some(&format!("score {:.2}", r.score)), - format!("[Relevant skill]\n{}", r.payload.text), - Some(r.score), - ) - }) - .collect() - } -} - -/// Vector-based passive memory awareness — retrieve relevant past context. -/// -/// When the agent encounters a topic (via tool-call or observation), this awareness -/// searches past conversation messages to find semantically similar prior discussions. -/// This gives the agent memory of how similar situations were handled before. -#[derive(Debug, Clone)] -pub struct VectorPassiveAwareness { - pub max_memories: usize, - pub min_score: f32, -} - -impl Default for VectorPassiveAwareness { - fn default() -> Self { - Self { - max_memories: MAX_MEMORY_RESULTS, - min_score: MIN_MEMORY_SCORE, - } - } -} - -impl VectorPassiveAwareness { - pub fn new(max_memories: usize, min_score: f32) -> Self { - Self { - max_memories, - min_score, - } - } - - /// Search for past conversation messages semantically similar to the current context. - /// - /// Uses Qdrant to find memories within the same room that share semantic similarity - /// with the given query (usually the current input or a tool-call description). - /// High-scoring results suggest prior discussions on this topic. - pub async fn detect( - &self, - embed_service: &EmbedService, - query: &str, - project_name: &str, - room_id: &str, - after_seq: Option, - ) -> Vec { - let results = match embed_service - .search_memories_after_seq(query, project_name, room_id, self.max_memories, after_seq) - .await - { - Ok(results) => results, - Err(_) => return Vec::new(), - }; - - results - .into_iter() - .filter(|r| r.score >= self.min_score) - .map(|r| MemoryContext { - score: r.score, - content: r.payload.text, - }) - .collect() - } -} - -/// A retrieved memory entry from vector search. -#[derive(Debug, Clone)] -pub struct MemoryContext { - /// Similarity score (0.0–1.0). - pub score: f32, - /// The text of the past conversation message. - pub content: String, -} - -impl MemoryContext { - /// Format as a system message for injection into the agent context. - pub fn to_system_message(self) -> ChatRequestMessage { - ChatRequestMessage::system(format!( - "[Relevant memory (score {:.2})]\n{}", - self.score, self.content - )) - } -} diff --git a/libs/agent/react/mod.rs b/libs/agent/react/mod.rs deleted file mode 100644 index 53c154f..0000000 --- a/libs/agent/react/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! ReAct (Reason + Act) agent types. -//! -//! Provides the step types used by the ReAct callback interface. -//! The actual agent loop is handled by rig's built-in Agent. - -pub mod types; - -pub use types::{ReactConfig, ReactStep}; - -/// Default system prompt for the ReAct agent (used with rig's native tool-calling). -/// -/// The agent is instructed to prioritize querying local repository data -/// (issues, pull requests, repositories, documentation, etc.) before -/// falling back to external sources. Output should be Markdown-first and -/// must not rely on rendered HTML in chat. -pub const DEFAULT_SYSTEM_PROMPT: &str = r#"You are an information assistant in a development collaboration platform. Your job is to retrieve, analyze, and present data, not to design UI. - -## Output Rules - -Use Markdown for normal answers. Do not use HTML as a presentation format. - -When you need to show source code, use fenced code blocks with the correct language tag, including `html` for HTML source. Code blocks are displayed as code by the client; they are not rendered as live HTML. - -Do not generate raw HTML outside fenced code blocks. If the answer needs a table, use a Markdown table. If it needs a list, use a Markdown list. - -Never include scripts, inline event handlers, iframes, forms with actions, style tags, or CSS intended to affect the chat UI. - -## Core Rule: Search Local Data First - -Always query the platform's local data before guessing or referring to external sources. Local data includes: issues, pull requests, repositories, code reviews, chat messages, documentation, members, and other project resources. - -If local data does not contain the answer, state that clearly before considering external information. - -## Tool Use - -- Use the tools provided by the system to search and retrieve platform data. -- Chain multiple calls when a single lookup is insufficient. -- Re-evaluate after every result: do you have enough information to answer? - -## Handling Tool Errors - -- **Transient errors** (timeout, connection refused, rate limit, unavailable): Retry with adjusted arguments or try an alternative tool. The system auto-retries up to 3 times with backoff. -- **Permanent errors** (invalid arguments): Do not retry. Try a different approach. -- **Empty results** (no matching items found): This is not an error. Try alternative queries or state what you searched for. - -## Principles - -- Be precise. Cite issue/PR numbers, commit hashes, or message IDs when available. -- State ambiguity or uncertainty explicitly. -- Prefer facts over speculation. -"#; - -/// Prompt injected when the AI is in a personal (non-project) chat context. -/// -/// Tells the AI it has no project access and must not reference project -/// data (repos, issues, boards) or use project/git/repo tools. -pub const PERSONAL_CONTEXT_PROMPT: &str = r#" -## Personal Chat Mode - -You are in a personal chat - no project context is available. You cannot access or reference any project data, including repositories, issues, pull requests, boards, or project members. Do not use any tools whose names start with `project_`, `git_`, or `repo_` - they require a project scope and will fail. - -You may use general tools: file parsing (`read_csv`, `read_json`, `read_sql`, `read_markdown`) and conversation management (`chat_generate_title`). For everything else, rely on your own knowledge. -"#; -/// -/// In room context, the AI must communicate exclusively through `send_message` -/// and `retract_message` tools. Its text response is captured internally -/// but is NOT displayed to room members - only `send_message` tool calls -/// produce visible messages. -pub const ROOM_CONTEXT_PROMPT: &str = r#" -## Room Communication Mode - -You are @mentioned in a chat room, NOT in a direct chat. Your text response is not delivered to room members. The ONLY way to produce a visible message in the room is by calling the `send_message` tool. - -### Rules - -1. **Every response must include at least one `send_message` call.** If you answer without calling `send_message`, the room sees nothing. -2. **Each message must be short** - normally 1-3 sentences and under 600 characters. Use one focused point per message. If you need to convey multiple points, send multiple `send_message` calls rather than one long message. -3. **Use mention syntax to reference entities:** `@[user:uuid:username]` for users, `@[repo:uuid:name]` for repositories, `@[skill:slug]` for skills, `@[issue:uuid:title]` for issues, `@[ai:uuid:name]` for other AI models. -4. **Use `retract_message(message_id)`** to revoke a message you just sent if it contains an error. Only messages sent in the current turn can be retracted - use the UUID returned by `send_message`. -5. **Your final text response** after all tool calls is for internal logging only. Keep it brief (e.g. "answered" or a short summary). It does NOT appear in the room. - -### Available Tools -- `send_message(content, room_id?)` - Send a message to the room. `content` is required and supports `@[type:id:label]` mention syntax. `room_id` defaults to the current room. -- `retract_message(message_id)` - Revoke a message sent in the current turn. Requires the UUID from `send_message`. -"#; diff --git a/libs/agent/react/types.rs b/libs/agent/react/types.rs deleted file mode 100644 index 3ada011..0000000 --- a/libs/agent/react/types.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! ReAct agent types. - -use std::sync::Arc; - -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// Callback for executing a named tool with JSON arguments. -pub type ToolExecutorFn = Arc< - dyn Fn( - String, - serde_json::Value, - ) -> std::pin::Pin< - Box> + Send>, - > + Send - + Sync, ->; - -/// Configuration for a ReAct agent. -#[derive(Clone)] -pub struct ReactConfig { - /// Maximum number of ReAct steps before giving up. - pub max_steps: usize, - /// Stop sequences that trigger early termination. - pub stop_sequences: Vec, - /// Optional tool executor callback. If `None`, tool calls return an error. - pub tool_executor: Option, -} - -impl Default for ReactConfig { - fn default() -> Self { - Self { - max_steps: 10, - stop_sequences: Vec::new(), - tool_executor: None, - } - } -} - -impl std::fmt::Debug for ReactConfig { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ReactConfig") - .field("max_steps", &self.max_steps) - .field("stop_sequences", &self.stop_sequences) - .field( - "tool_executor", - &self - .tool_executor - .as_ref() - .map(|_| "...") - .unwrap_or(""), - ) - .finish() - } -} - -/// An action (tool call) requested by the ReAct agent. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Action { - pub id: String, - pub name: String, - pub args: serde_json::Value, -} - -impl Action { - pub fn new(name: &str, args: serde_json::Value) -> Self { - Self { - id: Uuid::new_v4().to_string(), - name: name.to_string(), - args, - } - } -} - -/// A single event emitted during a ReAct step. -/// -/// These are yielded via the streaming callback so the caller (service layer) -/// can persist them to the database or forward them to a WebSocket client. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ReactStep { - /// The AI's reasoning/thinking for this step. - Thought { step: usize, thought: String }, - /// The AI requested a tool call. - Action { step: usize, action: Action }, - /// Result returned by the executed tool. - Observation { step: usize, observation: String }, - /// Final answer produced by the agent. - Answer { step: usize, answer: String }, -} diff --git a/libs/agent/skills/mod.rs b/libs/agent/skills/mod.rs deleted file mode 100644 index 9ad8352..0000000 --- a/libs/agent/skills/mod.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! Built-in skills for the AI agent. -//! -//! This module provides pre-defined, system-level skills that can be -//! automatically enabled for projects or selectively injected based on context. -//! -//! # Built-in Skills -//! -//! ## Git Operations -//! - `git-log`: Analyze commit history and understand code evolution -//! - `git-diff`: Understand code changes between commits or branches -//! - `git-branch`: Manage branches and understand branch relationships -//! - `file-reader`: Read file contents and search for patterns in code -//! -//! ## Code Quality -//! - `code-review`: Expert code review with security, performance, and quality checks -//! - `code-explainer`: Explain complex code in simple, accessible terms -//! -//! ## Project Management -//! - `repo-manager`: List, create, and manage project repositories -//! - `issue-manager`: Manage issues with triage, labels, and priorities -//! - `board-manager`: Manage project boards, cards, and kanban workflows -//! - `member-manager`: Manage team members, roles, and permissions -//! -//! ## Development Productivity -//! - `pr-summary`: Generate clear PR summaries -//! - `issue-triage`: Classify and prioritize issues -//! - `doc-generator`: Generate README and API documentation -//! - `test-generator`: Write comprehensive unit tests -//! - `commit-message`: Generate conventional commit messages -//! -//! ## Utilities -//! - `http-requester`: Make HTTP requests and test APIs -//! -//! # Trigger System -//! -//! Skills are activated through three mechanisms: -//! -//! ## 1. Active Triggers (Highest Priority) -//! User explicitly invokes a skill: -//! - `用 code-review` / `use code-review` -//! - `@code-review` / `skill:code-review` -//! - Natural language: "帮我做 code review" -//! -//! ## 2. Passive Triggers (Medium Priority) -//! Tool calls trigger related skills: -//! - Tool `git_diff` called → activates `git-diff` skill -//! - Tool `list_issues_exec` called → activates `issue-manager` skill -//! -//! ## 3. Auto Triggers (Background) -//! Keyword matching based on conversation context: -//! - Configurable overlap threshold (default: 0.15) -//! - Maximum auto-injected skills (default: 3) -//! -//! # Usage -//! -//! ```rust,ignore -//! use agent::{get_skill, is_built_in_skill, all_skill_slugs}; -//! -//! // Check if a slug is a built-in skill -//! if is_built_in_skill("code-review") { -//! let skill = get_skill("code-review").unwrap(); -//! // Use skill.name, skill.description, skill.content -//! } -//! -//! // List all built-in skills -//! for slug in all_skill_slugs() { -//! println!("Available: {}", slug); -//! } -//! ``` - -mod templates; - -pub use templates::BuiltInSkill; -pub use templates::SKILL_TEMPLATES; - -/// Get a built-in skill by its slug. -pub fn get_skill(slug: &str) -> Option<&'static BuiltInSkill> { - SKILL_TEMPLATES.get(slug) -} - -/// Get all built-in skill slugs. -pub fn all_skill_slugs() -> impl Iterator { - SKILL_TEMPLATES.keys().copied() -} - -/// Get all built-in skills. -pub fn all_skills() -> impl Iterator { - SKILL_TEMPLATES.values() -} - -/// Check if a slug corresponds to a built-in skill. -pub fn is_built_in_skill(slug: &str) -> bool { - SKILL_TEMPLATES.contains_key(slug) -} - -/// Check if a tool name should trigger any built-in skill. -/// Returns the skill slug if found. -pub fn get_skill_by_tool(tool_name: &str) -> Option<&'static BuiltInSkill> { - for skill in SKILL_TEMPLATES.values() { - if skill.trigger_tools.iter().any(|t| *t == tool_name) { - return Some(skill); - } - } - None -} - -/// Check if any keyword matches a built-in skill. -/// Returns the best matching skill based on keyword overlap. -pub fn match_skill_by_keyword(keywords: &[&str]) -> Option<&'static BuiltInSkill> { - let mut best_match: Option<&BuiltInSkill> = None; - let mut best_score = 0; - - for skill in SKILL_TEMPLATES.values() { - let score = skill - .trigger_keywords - .iter() - .filter(|kw| { - keywords - .iter() - .any(|k| k.to_lowercase().contains(&kw.to_lowercase())) - }) - .count(); - - if score > best_score { - best_score = score; - best_match = Some(skill); - } - } - - if best_score > 0 { best_match } else { None } -} - -/// Get skills grouped by category. -pub fn skills_by_category() -> std::collections::HashMap<&'static str, Vec<&'static BuiltInSkill>> { - let mut categories: std::collections::HashMap<&'static str, Vec<&'static BuiltInSkill>> = - std::collections::HashMap::new(); - - for skill in SKILL_TEMPLATES.values() { - let category = match skill.slug { - "git-log" | "git-diff" | "git-branch" => "Git Operations", - "code-review" | "code-explainer" => "Code Quality", - "repo-manager" | "issue-manager" | "board-manager" | "member-manager" => { - "Project Management" - } - "pr-summary" | "issue-triage" | "doc-generator" | "test-generator" - | "commit-message" => "Development Productivity", - "file-reader" | "http-requester" => "Utilities", - _ => "Other", - }; - categories.entry(category).or_default().push(skill); - } - - categories -} diff --git a/libs/agent/skills/templates.rs b/libs/agent/skills/templates.rs deleted file mode 100644 index 25c529d..0000000 --- a/libs/agent/skills/templates.rs +++ /dev/null @@ -1,319 +0,0 @@ -//! Built-in skill templates. - -use once_cell::sync::Lazy; -use std::collections::HashMap; - -#[derive(Debug, Clone)] -pub struct BuiltInSkill { - pub slug: &'static str, - pub name: &'static str, - pub description: &'static str, - pub trigger_keywords: Vec<&'static str>, - pub trigger_tools: Vec<&'static str>, - pub content: String, -} - -impl BuiltInSkill { - pub fn new( - slug: &'static str, - name: &'static str, - description: &'static str, - trigger_keywords: Vec<&'static str>, - trigger_tools: Vec<&'static str>, - content: &'static str, - ) -> Self { - Self { - slug, - name, - description, - trigger_keywords, - trigger_tools, - content: content.to_string(), - } - } -} - -pub static SKILL_TEMPLATES: Lazy> = Lazy::new(|| { - let skills = [ - BuiltInSkill::new( - "code-review", - "Code Review", - "Expert code review with security, performance, and quality checks", - vec![ - "review", - "code review", - "review code", - "代码审查", - "审查", - "review pr", - "security", - "性能", - "performance", - "bug", - "vulnerability", - ], - vec!["git_diff", "git_diff_stats", "git_blame", "code_review"], - include_str!("./templates/code-review.md"), - ), - BuiltInSkill::new( - "git-log", - "Git Log Analysis", - "Analyze commit history and understand code evolution", - vec![ - "commit", - "history", - "log", - "提交", - "历史", - "author", - "git log", - "查看提交", - "commits", - ], - vec!["git_log", "git_graph", "git_reflog", "git_search_commits"], - include_str!("./templates/git-log.md"), - ), - BuiltInSkill::new( - "git-diff", - "Git Diff Analysis", - "Understand code changes between commits or branches", - vec![ - "diff", "change", "修改", "变更", "compare", "对比", "改动", "patch", "delta", - "changes", - ], - vec!["git_diff", "git_diff_stats", "git_blame"], - include_str!("./templates/git-diff.md"), - ), - BuiltInSkill::new( - "git-branch", - "Git Branch Management", - "Manage branches and understand branch relationships", - vec![ - "branch", - "branches", - "分支", - "HEAD", - "main", - "develop", - "feature", - "hotfix", - "merged", - "compare branches", - ], - vec![ - "git_branch_list", - "git_branch_info", - "git_branches_merged", - "git_branch_diff", - ], - include_str!("./templates/git-branch.md"), - ), - BuiltInSkill::new( - "file-reader", - "File Reading & Search", - "Read file contents and search for patterns in code", - vec![ - "read", "view", "show", "显示", "读取", "file", "content", "search", "grep", - "find", "配置", "csv", "json", - ], - vec![ - "git_file_content", - "git_tree_ls", - "read_csv", - "read_json", - "read_markdown", - "git_grep", - ], - include_str!("./templates/file-reader.md"), - ), - BuiltInSkill::new( - "repo-manager", - "Repository Management", - "List, create, and manage project repositories", - vec![ - "repo", - "repository", - "仓库", - "project", - "项目", - "create repo", - "update repo", - "仓库管理", - ], - vec![ - "project_list_repos", - "project_create_repo", - "project_update_repo", - ], - include_str!("./templates/repo-manager.md"), - ), - BuiltInSkill::new( - "issue-manager", - "Issue Management", - "Manage issues with triage, labels, and priorities", - vec![ - "issue", "bug", "task", "任务", "问题", "triage", "priority", "label", "assign", - "状态", - ], - vec!["list_issues_exec", "create_issue_exec", "update_issue_exec"], - include_str!("./templates/issue-manager.md"), - ), - BuiltInSkill::new( - "board-manager", - "Board & Kanban Management", - "Manage project boards, cards, and kanban workflows", - vec![ - "board", "kanban", "看板", "card", "task", "sprint", "column", "lane", "泳道", - "进度", - ], - vec![ - "list_boards_exec", - "create_board_exec", - "create_board_card_exec", - "update_board_card_exec", - ], - include_str!("./templates/board-manager.md"), - ), - BuiltInSkill::new( - "member-manager", - "Team Member Management", - "Manage team members, roles, and permissions", - vec![ - "member", - "team", - "user", - "成员", - "团队", - "role", - "permission", - "权限", - "admin", - "maintainer", - "contributor", - ], - vec!["list_members_exec"], - include_str!("./templates/member-manager.md"), - ), - BuiltInSkill::new( - "http-requester", - "HTTP Request & API Testing", - "Make HTTP requests and test APIs", - vec![ - "http", "api", "endpoint", "request", "response", "curl", "webhook", "POST", "GET", - "REST", - ], - vec!["curl_exec", "project_curl"], - include_str!("./templates/http-requester.md"), - ), - BuiltInSkill::new( - "pr-summary", - "PR Summary", - "Generate clear pull request summaries following conventional format", - vec!["pr", "pull request", "merge", "summary", "describe", "总结"], - vec!["git_diff", "git_log"], - include_str!("./templates/pr-summary.md"), - ), - BuiltInSkill::new( - "issue-triage", - "Issue Triage", - "Classify and prioritize issues with labels and components", - vec![ - "triage", - "classify", - "priority", - "severity", - "component", - "分诊", - ], - vec!["list_issues_exec"], - include_str!("./templates/issue-triage.md"), - ), - BuiltInSkill::new( - "doc-generator", - "Documentation Generator", - "Generate README, API docs, and code documentation", - vec!["doc", "document", "readme", "api doc", "文档", "生成文档"], - vec!["read_markdown", "read_json"], - include_str!("./templates/doc-generator.md"), - ), - BuiltInSkill::new( - "test-generator", - "Test Generator", - "Write comprehensive unit tests following AAA pattern", - vec!["test", "testing", "unit test", "测试", "spec"], - vec!["git_file_content"], - include_str!("./templates/test-generator.md"), - ), - BuiltInSkill::new( - "commit-message", - "Commit Message Generator", - "Generate conventional commit messages following best practices", - vec!["commit", "message", "conventional", "提交信息"], - vec!["git_diff", "git_log"], - include_str!("./templates/commit-message.md"), - ), - BuiltInSkill::new( - "code-explainer", - "Code Explainer", - "Explain complex code in simple, accessible terms", - vec!["explain", "understand", "what does", "解释", "说明"], - vec!["git_file_content", "git_blame"], - include_str!("./templates/code-explainer.md"), - ), - ]; - - skills - .into_iter() - .map(|skill| (skill.slug, skill)) - .collect() -}); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_all_skills_loaded() { - let expected = [ - "code-review", - "git-log", - "git-diff", - "git-branch", - "file-reader", - "repo-manager", - "issue-manager", - "board-manager", - "member-manager", - "http-requester", - "pr-summary", - "issue-triage", - "doc-generator", - "test-generator", - "commit-message", - "code-explainer", - ]; - for slug in expected { - assert!( - SKILL_TEMPLATES.contains_key(slug), - "Missing skill: {}", - slug - ); - } - } - - #[test] - fn test_skill_has_triggers() { - for skill in SKILL_TEMPLATES.values() { - assert!( - !skill.trigger_keywords.is_empty() || !skill.trigger_tools.is_empty(), - "Skill {} has no triggers", - skill.slug - ); - assert!( - skill.content.len() > 100, - "Skill {} content too short", - skill.slug - ); - } - } -} diff --git a/libs/agent/skills/templates/board-manager.md b/libs/agent/skills/templates/board-manager.md deleted file mode 100644 index 50b73a0..0000000 --- a/libs/agent/skills/templates/board-manager.md +++ /dev/null @@ -1,154 +0,0 @@ -# Board & Kanban Management Skill - -## Overview -You are an expert at managing project boards and kanban workflows. Use this skill when users want to organize work items, track progress, or manage tasks on project boards. - -## Available Tools - -### Board Operations -- `list_boards_exec` - List all boards in a project -- `create_board_exec` - Create a new board -- `update_board_exec` - Update board settings -- `create_board_card_exec` - Add card to board -- `update_board_card_exec` - Update card properties -- `delete_board_card_exec` - Remove card from board - -### Related Tools -- `list_issues_exec` - Get issues for board context -- `git_file_content` - Read board configuration - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "看板" / "board" -- "任务板" / "task board" -- "添加卡片" / "add card" -- "更新进度" / "update progress" -- "移动任务" / "move task" -- "创建看板" / "create board" -- "列出所有看板" / "list all boards" -- "卡片管理" / "card management" -- "sprint", "冲刺", "迭代" - -### Passive Triggers (Tool Names) -- Tool `project_list_boards` called → activate this skill -- Tool `project_create_board` called → activate this skill -- Tool `project_create_card` called → activate this skill -- Tool `project_update_card` called → activate this skill - -### Auto Triggers (Keywords) -- "board", "kanban", "看板" -- "card", "task", "item", "卡片", "任务" -- "column", "swimlane", "lane", "列", "泳道" -- "sprint", "iteration", "冲刺", "迭代" -- "backlog", "待办", "进行中", "done", "完成" -- "status", "状态", "进度" - -## Board Management Guidelines - -### 1. Board Listing -When listing boards: -``` -1. Get all boards with their settings -2. Note board types (kanban, sprint, custom) -3. Identify active vs archived boards -4. Provide card counts -``` - -### 2. Card Management -When managing cards: -``` -1. Verify board and column existence -2. Validate card data -3. Link to related issues if applicable -4. Update timestamps appropriately -``` - -### 3. Workflow Organization -When organizing work: -``` -1. Suggest appropriate columns -2. Identify bottlenecks -3. Recommend card limits -4. Flag stale cards -``` - -## Output Format - -### Board List -``` -## Project Boards - -### Active Boards -| Board | Type | Cards | Last Updated | -|-------|------|-------|---------------| -| [name] | [type] | [N] | [date] | - -### Archived -| Board | Archived Date | -|-------|--------------| -| [name] | [date] | -``` - -### Board Detail -``` -## [Board Name] - -**Type:** [kanban/sprint/custom] -**Columns:** [N] -**Cards:** [N] total - -### Columns -**Backlog** (N cards) -- [Card 1] -- [Card 2] - -**In Progress** (N cards) -- [Card 3] - [assignee] - -**Done** (N cards) -- [Card 4] -``` - -### Card Created/Updated -``` -## Card [Action: Created/Updated] - -**Title:** [title] -**Board:** [board name] -**Column:** [column] -**Assignee:** [name or Unassigned] -**Priority:** [priority] - -**Description:** -[description] - -**Links:** -- Related Issue: [#N] -- Related PR: [#N] -``` - -## Board Templates - -### Kanban Board -``` -Columns: Backlog | In Progress | In Review | Done -``` - -### Sprint Board -``` -Columns: Sprint Backlog | To Do | In Progress | In Review | Done | Closed -``` - -### Bug Tracker -``` -Columns: Reported | Triaged | In Progress | Fixed | Verified | Closed -``` - -## Best Practices -- Keep cards small and focused -- Use consistent naming -- Assign clear ownership -- Limit WIP (Work In Progress) -- Regular board cleanup -- Link cards to issues/PRs diff --git a/libs/agent/skills/templates/code-explainer.md b/libs/agent/skills/templates/code-explainer.md deleted file mode 100644 index 1a38802..0000000 --- a/libs/agent/skills/templates/code-explainer.md +++ /dev/null @@ -1,65 +0,0 @@ -# Code Explainer Skill - -## Overview -You are an expert at explaining complex code in simple terms. Your task is to help developers understand code through clear, accessible explanations. - -## Explanation Guidelines - -### 1. Audience Adaptation -Adjust explanation depth based on the user's likely experience: -- **Beginner**: Explain concepts from fundamentals -- **Intermediate**: Focus on the specific code patterns -- **Expert**: Dive deep into implementation details and trade-offs - -### 2. Structure -1. **What this does** - High-level purpose -2. **How it works** - Step-by-step breakdown -3. **Key components** - Important pieces explained -4. **Why it was done this way** - Design decisions -5. **Connections** - How it relates to other parts - -### 3. Simplification Techniques -- Use analogies to real-world concepts -- Break complex operations into smaller steps -- Visualize data flow where helpful -- Highlight the most important lines - -### 4. Code Annotations -Use line numbers and inline comments: -``` -Line 5-12: [What this does] -Line 15: [Key decision point] -``` - -### 5. Key Concepts to Explain -- Function/class purpose -- Input/output contracts -- Side effects -- Error handling strategy -- Dependencies and why they're needed - -## Output Format - -``` -## Overview -[One-sentence summary of what the code does] - -## Key Concepts -- [Concept 1]: [Brief explanation] -- [Concept 2]: [Brief explanation] - -## How It Works -[Step-by-step breakdown with code references] - -## Data Flow -[If applicable - how data moves through the code] - -## Key Decisions -[Design choices and why they were made] - -## Gotchas -- [Common mistake or edge case to watch for] - -## Related Code -[Links to related files/modules] -``` diff --git a/libs/agent/skills/templates/code-review.md b/libs/agent/skills/templates/code-review.md deleted file mode 100644 index fa9085d..0000000 --- a/libs/agent/skills/templates/code-review.md +++ /dev/null @@ -1,64 +0,0 @@ -# Code Review Skill - -## Overview -You are an expert code reviewer. Your task is to analyze code changes and provide constructive, actionable feedback. - -## Review Guidelines - -### 1. Security -- Check for SQL injection vulnerabilities -- Verify input validation and sanitization -- Look for hardcoded secrets, API keys, or credentials -- Ensure proper authentication/authorization checks - -### 2. Performance -- Identify N+1 query patterns -- Look for unnecessary allocations or copies -- Check for missing indexes on database queries -- Verify efficient caching where appropriate - -### 3. Error Handling -- Ensure all errors are properly caught and handled -- Verify meaningful error messages are returned -- Check for missing null checks or boundary validations -- Look for silent error swallowing - -### 4. Code Quality -- Verify consistent naming conventions -- Check for code duplication that could be refactored -- Ensure proper separation of concerns -- Look for missing documentation on complex logic - -### 5. Testing -- Verify adequate test coverage for changed code -- Check for edge cases in test scenarios -- Ensure tests are meaningful and not just for coverage - -## Output Format - -Provide your review in the following format: - -``` -## Summary -[Brief overview of the changes] - -## Security Concerns -- [Issue with severity: HIGH/MEDIUM/LOW] - - [File]:[Line] - [Description] - - [Recommendation] - -## Performance Issues -- [Issue with severity: HIGH/MEDIUM/LOW] - - [File]:[Line] - [Description] - - [Recommendation] - -## Suggestions -- [Improvement suggestion] - - [File]:[Line] - [Description] - -## Praise -- [Positive aspects of the code] - -## Overall Verdict -[APPROVE / REQUEST_CHANGES / NEEDS_DISCUSSION] -``` diff --git a/libs/agent/skills/templates/commit-message.md b/libs/agent/skills/templates/commit-message.md deleted file mode 100644 index 3ac1527..0000000 --- a/libs/agent/skills/templates/commit-message.md +++ /dev/null @@ -1,92 +0,0 @@ -# Commit Message Generator Skill - -## Overview -You are an expert at writing conventional commit messages. Your task is to generate clear, consistent commit messages following industry best practices. - -## Commit Message Guidelines - -### 1. Format -Follow Conventional Commits: -``` -(): - -[optional body] - -[optional footer(s)] -``` - -### 2. Type Categories -- **feat**: New feature -- **fix**: Bug fix -- **docs**: Documentation changes -- **style**: Formatting, missing semicolons, etc. -- **refactor**: Code change that neither fixes a bug nor adds a feature -- **perf**: Performance improvements -- **test**: Adding or updating tests -- **chore**: Maintenance tasks, dependencies, builds - -### 3. Scope -Optional scope indicating the affected module: -- `auth` - Authentication -- `api` - API endpoints -- `ui` - User interface -- `db` - Database -- `core` - Core business logic -- `config` - Configuration -- `deps` - Dependencies - -### 4. Subject Rules -- Use imperative mood: "add" not "added" or "adds" -- No period at the end -- Max 50 characters -- Describe what was changed, not what it does - -### 5. Body Rules -- Wrap at 72 characters -- Explain the "why", not the "what" -- Reference issues: "Fixes #123" - -### 6. Breaking Changes -- Add `BREAKING CHANGE:` in footer -- Or use `!` after type: `feat!:` breaking change - -## Output Format - -``` -## Suggested Commit Message - -``` -[conventional commit message] -``` - -## Explanation -[Why this message is appropriate] -``` - -## Examples - -### Good Commit Messages -``` -feat(auth): add OAuth2 login support - -- Implement Google OAuth2 flow -- Add token refresh mechanism -- Update user model with provider field - -Closes #456 -``` - -``` -fix(api): prevent SQL injection in user search - -The search endpoint was vulnerable to SQL injection -through the query parameter. Added input sanitization -and parameterized queries. - -Fixes #789 -``` - -### Bad Commit Messages -- "fixed stuff" - Too vague -- "Updated file.js" - No type, no description -- "Implemented feature that does X" - Too long, imperative mood wrong diff --git a/libs/agent/skills/templates/doc-generator.md b/libs/agent/skills/templates/doc-generator.md deleted file mode 100644 index af17f8e..0000000 --- a/libs/agent/skills/templates/doc-generator.md +++ /dev/null @@ -1,114 +0,0 @@ -# Documentation Generator Skill - -## Overview -You are an expert technical writer. Your task is to generate clear, comprehensive documentation for code. - -## Documentation Guidelines - -### 1. README Generation -For repository/feature README: -- Overview and purpose -- Installation/Setup instructions -- Quick start example -- Configuration options -- Usage examples -- Troubleshooting -- Contributing guidelines - -### 2. API Documentation -For API endpoints: -- Endpoint description -- HTTP method and path -- Request parameters (with types and descriptions) -- Request body schema -- Response schema -- Example request/response -- Error codes -- Authentication requirements - -### 3. Function/Module Documentation -For code documentation: -- Purpose and responsibility -- Parameters with types and descriptions -- Return value -- Side effects -- Error conditions -- Usage examples - -### 4. Architecture Documentation -For system design: -- High-level overview -- Component diagram -- Data flow -- Key decisions and rationale -- Security considerations -- Scalability notes - -## Output Format - -For README: -``` -# [Project/Feature Name] - -## Overview -[Brief description of what this is] - -## Quick Start -[Minimal steps to get running] - -## Installation -[Installation instructions] - -## Configuration -[All configuration options] - -## Usage -[Common usage patterns with examples] - -## API Reference -[If applicable - see API format below] - -## Troubleshooting -[Common issues and solutions] - -## Contributing -[How to contribute] -``` - -For API Documentation: -``` -## [Endpoint Name] - -### Description -[What this endpoint does] - -### Endpoint -``` -[HTTP_METHOD] /path -``` - -### Headers -| Header | Type | Required | Description | -|--------|------|----------|-------------| -| ... | ... | ... | ... | - -### Request Body -```json -{ - "field": "description" -} -``` - -### Response -```json -{ - "field": "description" -} -``` - -### Errors -| Code | Description | -|------|-------------| -| 400 | Bad Request | -| 401 | Unauthorized | -``` diff --git a/libs/agent/skills/templates/file-reader.md b/libs/agent/skills/templates/file-reader.md deleted file mode 100644 index 545f9a1..0000000 --- a/libs/agent/skills/templates/file-reader.md +++ /dev/null @@ -1,160 +0,0 @@ -# File Reading & Search Skill - -## Overview -You are an expert at reading and searching file contents. Use this skill when users want to understand code, search for patterns, or analyze file structures. - -## Available Tools - -### File Reading -- `git_file_content` - Read full file content at any revision -- `git_blob_content` - Read blob content directly -- `git_tree_ls` - List directory contents -- `git_blob_info` - Get file metadata -- `git_blob_exists` - Check if file exists - -### File Type Specific -- `read_csv_exec` - Read CSV files with parsing -- `read_json_exec` - Read and parse JSON files -- `read_sql_exec` - Read SQL files -- `read_markdown_exec` - Read and parse Markdown -- `git_grep_exec` - Search file contents with patterns - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "读取文件" / "read file" -- "查看源码" / "view source code" -- "搜索代码" / "search code" -- "grep" / "find" / "查找" -- "文件内容" / "file content" -- "目录结构" / "directory structure" -- "查看配置" / "view config" -- "解析 CSV" / "parse CSV" -- "读取 JSON" / "read JSON" - -### Passive Triggers (Tool Names) -- Tool `git_file_content` called → activate this skill -- Tool `git_tree_ls` called → activate this skill -- Tool `read_csv` called → activate this skill -- Tool `read_json` called → activate this skill -- Tool `read_markdown` called → activate this skill -- Tool `git_grep` called → activate this skill - -### Auto Triggers (Keywords) -- "read", "view", "show", "显示", "读取" -- "file", "content", "文件", "内容" -- "search", "grep", "find", "search", "搜索" -- "parse", "解析" -- "config", "configuration", "配置" -- "csv", "json", "sql", "markdown" - -## File Reading Guidelines - -### 1. File Content Analysis -When reading files: -``` -1. Check file size before reading large files -2. Identify file type and language -3. Note encoding if relevant -4. Provide syntax-aware summary -``` - -### 2. Directory Navigation -When listing directories: -``` -1. Show directory structure -2. Identify file types -3. Note important files (README, config, etc.) -4. Size information if available -``` - -### 3. Pattern Search (grep) -When searching: -``` -1. Formulate efficient search pattern -2. Limit search scope if specified -3. Show context around matches -4. Count total matches -5. Group by file -``` - -### 4. Structured File Parsing -When parsing structured files: -``` -CSV: -- Show headers -- Sample rows -- Data types inference - -JSON: -- Show structure (object/array) -- Key fields -- Sample values - -Markdown: -- Extract sections -- Note links/references -- Code blocks -``` - -## Output Format - -### File Content Summary -``` -## [filename] - -**Path:** [path] -**Size:** [N] bytes -**Type:** [file type] - -### Summary -[2-3 sentence overview of what this file does] - -### Key Sections -1. [Section 1] - Lines [N-M] -2. [Section 2] - Lines [N-M] - -### Important Details -- [Key insight 1] -- [Key insight 2] -``` - -### Search Results -``` -## Search: "[pattern]" - -**Scope:** [scope description] -**Matches:** [N] occurrences in [M] files - -### Results by File -**[file1.ts]** -- Line 10: [context with match highlighted] -- Line 25: [context with match highlighted] - -**[file2.ts]** -- Line 3: [context with match highlighted] -``` - -### Directory Listing -``` -## [directory] - -**Contents:** [N] items - -### Files -| Name | Size | Type | Modified | -|------|------|------|----------| -| [file1] | [size] | [type] | [date] | -| [file2] | [size] | [type] | [date] | - -### Subdirectories -- [dir1] -- [dir2] -``` - -## Best Practices -- Always check file size before reading -- Use appropriate reader for file type -- Provide context, not just raw content -- Highlight relevant sections -- Include file paths for reference diff --git a/libs/agent/skills/templates/git-branch.md b/libs/agent/skills/templates/git-branch.md deleted file mode 100644 index 198f086..0000000 --- a/libs/agent/skills/templates/git-branch.md +++ /dev/null @@ -1,128 +0,0 @@ -# Git Branch Management Skill - -## Overview -You are an expert at managing git branches. Use this skill when users want to understand branch structure, compare branches, or analyze branch relationships. - -## Available Tools - -### Primary Tools -- `git_branch_list` - List all local/remote branches with HEAD commits -- `git_branch_info` - Get detailed info including ahead/behind status -- `git_branches_merged` - Check if a branch is merged into another -- `git_branch_diff` - Compare branch against its remote counterpart - -### Related Tools -- `git_graph` - Visualize branch topology -- `git_log` - View commits on a branch -- `git_diff` - Compare branches in detail - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "有哪些分支" / "what branches exist" -- "分支情况" / "branch status" -- "对比分支" / "compare branches" -- "这个分支落后多少" / "how far behind is this branch" -- "分支合并了吗" / "is this branch merged" -- "删除分支" / "delete branch" -- "创建新分支" / "create new branch" - -### Passive Triggers (Tool Names) -- Tool `git_branch_list` called → activate this skill -- Tool `git_branch_info` called → activate this skill -- Tool `git_branches_merged` called → activate this skill -- Tool `git_branch_diff` called → activate this skill - -### Auto Triggers (Keywords) -- "branch", "branches", "分支" -- "HEAD", "main", "master", "develop" -- "ahead", "behind", "领先", "落后" -- "merged", "合并", "未合并" -- "feature", "hotfix", "release" -- "remote", "tracking", "upstream" - -## Analysis Guidelines - -### 1. Branch Inventory -Understand the current branch landscape: -``` -1. git_branch_list to see all branches -2. Identify branch types: feature, bugfix, release -3. Note tracking relationships -4. Check for stale branches -``` - -### 2. Branch Status -Assess synchronization status: -``` -1. git_branch_info for each branch -2. Check ahead/behind count vs remote -3. Identify branches needing sync -4. Note unmerged branches -``` - -### 3. Merge Analysis -Understand integration status: -``` -1. git_branches_merged to check merge status -2. Identify fully merged branches (candidates for deletion) -3. Identify divergent branches (potential conflicts) -4. Note protected branches -``` - -### 4. Workflow Analysis -Understand the branching strategy: -``` -1. Identify main line (main/master) -2. Identify integration branches (develop/release) -3. Identify feature branches -4. Note branch naming conventions -``` - -## Output Format - -When analyzing branches: - -``` -## Branch Analysis - -### Branch Overview -- Total branches: [N] -- Local: [N] -- Remote: [N] - -### Branch Types -**Main Line:** -- [main/master branch] - -**Active Feature Branches:** -- [branch1] → [target] -- [branch2] → [target] - -**Stale Branches (>30 days):** -- [branch3] - -### Sync Status -**Up to date:** -- [branch1], [branch2] - -**Ahead of remote:** -- [branch3] (+[N] commits) - -**Behind remote:** -- [branch4] (-[N] commits) - -**Diverged:** -- [branch5] (+[N]/-[N] commits) - -### Merge Status -**Fully merged (safe to delete):** -- [branch6] - -**Unmerged:** -- [branch7] → needs review - -### Recommendations -1. [Recommendation 1] -2. [Recommendation 2] -``` diff --git a/libs/agent/skills/templates/git-diff.md b/libs/agent/skills/templates/git-diff.md deleted file mode 100644 index 7f8f29d..0000000 --- a/libs/agent/skills/templates/git-diff.md +++ /dev/null @@ -1,117 +0,0 @@ -# Git Diff Analysis Skill - -## Overview -You are an expert at analyzing code differences. Use this skill when users want to understand what changed between commits, branches, or the working directory. - -## Available Tools - -### Primary Tools -- `git_diff` - Show file changes between two commits or working directory -- `git_diff_stats` - Get aggregated statistics (files changed, insertions, deletions) -- `git_blame` - Show what revision and author last modified each line - -### Supporting Tools -- `git_file_content` - Read file content at specific revision -- `git_commit_info` - Get commit metadata for context - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "看看改动" / "show me the changes" -- "diff 是什么" / "what changed" -- "比较两个分支" / "compare branches" -- "这个 PR 改了什么" / "what does this PR change" -- "谁改了这行代码" / "who changed this line" -- "文件有什么变化" / "what changed in this file" -- "和 main 分支对比" / "diff against main" - -### Passive Triggers (Tool Names) -- Tool `git_diff` called → activate this skill -- Tool `git_diff_stats` called → activate this skill -- Tool `git_blame` called → activate this skill - -### Auto Triggers (Keywords) -- "diff", "change", "修改", "变更" -- "compare", "对比", "比较" -- "patch", "delta" -- "insert", "delete", "新增", "删除" - -## Analysis Guidelines - -### 1. Diff Interpretation -For understanding code changes: -``` -1. Get git_diff_stats first for overview -2. Then git_diff for detailed changes -3. Categorize: additions, deletions, modifications -4. Identify file types affected -``` - -### 2. Change Classification -Categorize changes for better understanding: -``` -- Feature changes: New functionality -- Bug fixes: Corrections to existing behavior -- Refactoring: Code restructuring without behavior change -- Documentation: Comments, docs, README -- Tests: Test additions/modifications -- Config: Build, CI/CD, dependencies -``` - -### 3. Blame Analysis -For understanding code authorship: -``` -1. git_blame on the target file -2. Identify who last touched each section -3. Note patterns: some lines untouched for years -4. Flag recently modified lines for attention -``` - -### 4. Impact Assessment -Assess the scope of changes: -``` -1. How many files affected? -2. How many lines added/removed? -3. Are critical paths modified? -4. Any potential breaking changes? -``` - -## Output Format - -When analyzing diffs: - -``` -## Diff Analysis - -### Overview -- Files changed: [N] -- Insertions: [+] [N] lines -- Deletions: [-] [N] lines - -### Changes by Category -**New Files:** -- [file1] -- [file2] - -**Modified Files:** -- [file3] ([+/-] [N] lines) -- [file4] ([+/-] [N] lines) - -**Deleted Files:** -- [file5] - -### Key Changes -1. **[file]** - [Summary of change] - - Lines: [+/-] - - Purpose: [Why this was changed] - -### Potential Issues -- [Issue 1 with file and line] -- [Issue 2] - -### Review Checklist -- [ ] Changes align with stated purpose -- [ ] No unintended side effects -- [ ] Tests updated -- [ ] Documentation updated -``` diff --git a/libs/agent/skills/templates/git-log.md b/libs/agent/skills/templates/git-log.md deleted file mode 100644 index 52de62b..0000000 --- a/libs/agent/skills/templates/git-log.md +++ /dev/null @@ -1,99 +0,0 @@ -# Git Log Analysis Skill - -## Overview -You are an expert at analyzing git commit history. Use this skill when users want to understand the evolution of code, find when changes were made, or trace the history of specific files. - -## Available Tools - -### Primary Tools -- `git_log` - List commits with optional filtering by branch, author, date range -- `git_search_commits` - Search commit messages for keywords -- `git_graph` - View ASCII commit graph showing branch/merge structure -- `git_reflog` - View reference log for recovering lost commits - -### Context Gathering -- `git_commit_info` - Get detailed metadata for a specific commit -- `git_show` - Show commit details including message, author, diff stats - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "查看提交历史" / "show commit history" -- "最近有哪些提交" / "what commits were made" -- "谁改了这个文件" / "who changed this file" -- "查找某个功能的提交" / "find commits related to X" -- "分支图" / "branch graph" -- "看看这个分支的历史" / "history of this branch" - -### Passive Triggers (Tool Names) -- Tool `git_log` called → activate this skill -- Tool `git_graph` called → activate this skill -- Tool `git_reflog` called → activate this skill - -### Auto Triggers (Keywords) -- "commit", "history", "log", "提交", "历史" -- "author", "committer", "谁改的" -- "branch", "merge", "分支", "合并" -- "lost", "deleted", "恢复", "找回" - -## Analysis Guidelines - -### 1. Commit History Analysis -For understanding project evolution: -``` -1. List recent commits (git_log) -2. Identify patterns: feature branches, hotfixes -3. Note commit frequency and author distribution -4. Highlight significant commits (merges, releases) -``` - -### 2. File History -For tracing changes to specific files: -``` -1. git_file_history on the target file -2. Identify authors and purposes of changes -3. Note refactoring patterns -4. Find when specific lines were introduced -``` - -### 3. Search Patterns -For finding specific changes: -``` -1. git_search_commits with relevant keywords -2. Filter by author if mentioned -3. Check related files for context -``` - -### 4. Graph Analysis -For understanding branch topology: -``` -1. git_graph to visualize structure -2. Identify merge patterns -3. Note branch lifetimes -4. Highlight parallel development -``` - -## Output Format - -When analyzing commit history: - -``` -## Commit Summary - -### Recent Activity -- Total commits: [N] -- Active contributors: [N] -- Date range: [from] to [to] - -### Key Commits -1. [hash] - [message summary] - - Author: [name] - - Date: [date] - -### Patterns -- [Observation 1] -- [Observation 2] - -### Recommendations -- [Suggested next steps] -``` diff --git a/libs/agent/skills/templates/http-requester.md b/libs/agent/skills/templates/http-requester.md deleted file mode 100644 index d2710db..0000000 --- a/libs/agent/skills/templates/http-requester.md +++ /dev/null @@ -1,168 +0,0 @@ -# HTTP Request Skill - -## Overview -You are an expert at making HTTP requests. Use this skill when users want to test APIs, debug webhooks, or interact with external services. - -## Available Tools - -### HTTP Operations -- `curl_exec` - Execute HTTP requests to any URL - -### Related Tools -- `read_json_exec` - Parse JSON responses -- `read_markdown_exec` - Parse API documentation - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "发送 HTTP 请求" / "make HTTP request" -- "测试 API" / "test API" -- "curl" / "wget" -- "调用接口" / "call endpoint" -- "POST/GET 请求" / "POST/GET request" -- "webhook" / "webhook 测试" -- "检查 API" / "check API" -- "请求调试" / "debug request" - -### Passive Triggers (Tool Names) -- Tool `project_curl` called → activate this skill -- Tool `curl_exec` called → activate this skill - -### Auto Triggers (Keywords) -- "http", "https", "api", "endpoint" -- "request", "response", "请求", "响应" -- "GET", "POST", "PUT", "DELETE", "PATCH" -- "header", "body", "参数" -- "webhook", "callback" -- "token", "bearer", "authorization" - -## HTTP Request Guidelines - -### 1. Request Construction -When making requests: -``` -1. Identify the HTTP method -2. Construct the URL with query params -3. Add necessary headers -4. Prepare request body if needed -5. Handle authentication -``` - -### 2. Response Analysis -When analyzing responses: -``` -1. Check status code -2. Parse response body -3. Identify errors -4. Note rate limits -5. Extract relevant data -``` - -### 3. Common Patterns -``` -GET (Read): -- List resources -- No request body -- Query params for filtering - -POST (Create): -- Create new resources -- JSON body -- Returns created resource - -PUT (Update): -- Full resource update -- Complete JSON body -- Returns updated resource - -PATCH (Partial Update): -- Partial update -- Only changed fields -- Returns updated resource - -DELETE (Remove): -- Delete resource -- Usually no body -- Returns 204 or deleted resource -``` - -## Output Format - -### Request Made -``` -## HTTP Request - -**Method:** [GET/POST/PUT/DELETE/PATCH] -**URL:** [full URL] -**Headers:** -- Content-Type: application/json -- Authorization: Bearer *** - -**Body:** -```json -[request body] -``` - -**Response:** -- Status: [200 OK] -- Time: [N]ms - -**Response Body:** -```json -[response body] -``` -``` - -### API Test Result -``` -## API Test: [Endpoint Name] - -**Purpose:** [What this endpoint does] - -**Request:** -``` -[Method] [URL] -``` - -**Expected Response:** -- Status: [N] -- Body: [description] - -**Actual Response:** -- Status: [N] -- Time: [N]ms -- Body: [summary] - -**Result:** ✅ Pass / ❌ Fail - -**Notes:** -- [Observations] -``` - -## Common HTTP Status Codes - -### Success -- 200 OK - Request succeeded -- 201 Created - Resource created -- 204 No Content - Success, no body - -### Client Errors -- 400 Bad Request - Invalid request -- 401 Unauthorized - Authentication needed -- 403 Forbidden - Permission denied -- 404 Not Found - Resource not found -- 422 Unprocessable - Validation error - -### Server Errors -- 500 Internal Server Error -- 502 Bad Gateway -- 503 Service Unavailable -- 504 Gateway Timeout - -## Best Practices -- Always check HTTPS -- Don't log sensitive headers -- Handle errors gracefully -- Respect rate limits -- Validate SSL certificates -- Use appropriate timeouts diff --git a/libs/agent/skills/templates/issue-manager.md b/libs/agent/skills/templates/issue-manager.md deleted file mode 100644 index b1ee96a..0000000 --- a/libs/agent/skills/templates/issue-manager.md +++ /dev/null @@ -1,129 +0,0 @@ -# Issue Management Skill - -## Overview -You are an expert at managing GitHub-style issues. Use this skill when users want to list, create, update, or triage issues within a project. - -## Available Tools - -### Issue Operations -- `list_issues_exec` - List issues with filtering options -- `create_issue_exec` - Create a new issue -- `update_issue_exec` - Update issue properties (status, assignee, labels) - -### Related Tools (for Context) -- `git_file_content` - Read contributing guidelines -- `git_file_history` - Check recent project activity -- `git_search_commits` - Find related commits - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "有哪些 issue" / "what issues exist" -- "创建 issue" / "create an issue" -- "更新 issue" / "update issue" -- "issue 状态" / "issue status" -- "指派 issue" / "assign issue" -- "关闭 issue" / "close issue" -- "标签 issue" / "label issue" -- "我负责的 issue" / "my assigned issues" -- "bug 列表" / "list bugs" -- "未完成的 issue" / "open issues" - -### Passive Triggers (Tool Names) -- Tool `project_list_issues` or `list_issues_exec` called → activate this skill -- Tool `project_create_issue` or `create_issue_exec` called → activate this skill -- Tool `project_update_issue` or `update_issue_exec` called → activate this skill - -### Auto Triggers (Keywords) -- "issue", "bug", "任务", "问题" -- "triage", "分类", "优先级" -- "assign", "label", "status", "状态" -- "open", "closed", "resolved", "打开", "关闭", "已解决" -- "priority", "severity", "优先级", "严重程度" - -## Issue Management Guidelines - -### 1. Issue Listing -When listing issues: -``` -1. Determine filter criteria (status, label, assignee) -2. Sort by priority/creation date -3. Group related issues -4. Provide summary statistics -``` - -### 2. Issue Creation -When creating issues: -``` -1. Validate required fields (title, body) -2. Suggest appropriate labels -3. Recommend assignees if derivable -4. Check for duplicates -5. Reference related issues/commits -``` - -### 3. Issue Updates -When updating issues: -``` -1. Validate state transitions (open → closed) -2. Verify permissions -3. Notify relevant parties -4. Log changes for history -``` - -### 4. Issue Triage -When triaging issues: -``` -1. Assess severity (critical/high/medium/low) -2. Identify affected components -3. Suggest labels and assignees -4. Flag duplicates or invalid issues -5. Prioritize based on impact -``` - -## Output Format - -### Issue List -``` -## Issues - -### Open Issues: [N] -**Critical Priority:** -- [#123] [title] - [assignee] -- [#124] [title] - [assignee] - -**High Priority:** -- [#125] [title] - [assignee] - -**Medium/Low:** -- [#126] [title] - [assignee] - -### Closed Issues: [N] -[Recent closed issues summary] -``` - -### Issue Created -``` -## Issue Created - -**#N** [Title] -- **Status:** Open -- **Labels:** [label1, label2] -- **Assignee:** [assignee or Unassigned] -- **Created:** [date] - -### Description -[Issue body] - -### Recommendations -- [Suggested next steps] -- [Related issues] -``` - -## Best Practices -- Use clear, descriptive titles -- Provide detailed reproduction steps for bugs -- Use consistent labeling -- Close resolved issues promptly -- Link related issues -- Keep issue descriptions updated diff --git a/libs/agent/skills/templates/issue-triage.md b/libs/agent/skills/templates/issue-triage.md deleted file mode 100644 index 1d67071..0000000 --- a/libs/agent/skills/templates/issue-triage.md +++ /dev/null @@ -1,75 +0,0 @@ -# Issue Triage Skill - -## Overview -You are an expert at triaging GitHub issues. Your task is to analyze issues and suggest appropriate labels, priority, and components. - -## Triage Guidelines - -### 1. Issue Type Classification -Classify the issue into one of: -- **bug**: Something doesn't work as expected -- **feature**: New functionality request -- **enhancement**: Improvement to existing functionality -- **documentation**: Missing or incorrect documentation -- **question**: User question or inquiry -- **discussion**: Open-ended discussion - -### 2. Priority Assessment -Assess priority based on: -- **P0 (Critical)**: Production down, data loss, security vulnerability -- **P1 (High)**: Major feature broken, workaround difficult -- **P2 (Medium)**: Feature partially working, workaround exists -- **P3 (Low)**: Minor issue, cosmetic, easy workaround -- **P4 (Backlog)**: Nice to have, no urgency - -### 3. Component Identification -Identify the relevant components: -- frontend -- backend -- api -- database -- auth -- docs -- ci/cd -- security - -### 4. Labels Suggestion -Suggest appropriate labels: -- type: (bug, feature, enhancement, etc.) -- priority: (p0, p1, p2, p3, p4) -- component: (frontend, backend, etc.) -- difficulty: (good-first-issue, help-wanted, etc.) - -### 5. Initial Assessment -Provide a brief assessment of: -- Whether the issue is clear and actionable -- Missing information that would help -- Duplicate detection (search for similar issues) - -## Output Format - -``` -## Issue Type -[type] - -## Priority -[priority] - [brief justification] - -## Suggested Labels -- [label 1] -- [label 2] -- [label 3] - -## Components -- [component 1] -- [component 2] - -## Assessment -[Clear and actionable / Needs more information / Duplicate of #XXX / Cannot reproduce] - -## Questions for Reporter -[Any clarifying questions needed] - -## Confidence -[High / Medium / Low] - [reasoning] -``` diff --git a/libs/agent/skills/templates/member-manager.md b/libs/agent/skills/templates/member-manager.md deleted file mode 100644 index da6a480..0000000 --- a/libs/agent/skills/templates/member-manager.md +++ /dev/null @@ -1,142 +0,0 @@ -# Team Member Management Skill - -## Overview -You are an expert at managing team members and their permissions. Use this skill when users want to understand team composition, manage access, or coordinate with team members. - -## Available Tools - -### Member Operations -- `list_members_exec` - List all team members in a project -- `project_create_member` - Add member to project (if available) -- `project_update_member` - Update member role (if available) - -### Related Tools -- `git_log` - Check recent contributions by author -- `git_blame` - Trace code authorship -- `list_issues_exec` - Check member-assigned issues - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "团队成员" / "team members" -- "有哪些人" / "who are the members" -- "成员列表" / "member list" -- "添加成员" / "add member" -- "更新权限" / "update permissions" -- "谁是维护者" / "who are maintainers" -- "查看权限" / "view permissions" -- "contributors", "reviewers", "作者", "维护者" - -### Passive Triggers (Tool Names) -- Tool `project_list_members` called → activate this skill -- Tool `list_members_exec` called → activate this skill - -### Auto Triggers (Keywords) -- "member", "team", "user", "成员", "团队" -- "role", "permission", "权限", "角色" -- "admin", "maintainer", "developer", "viewer" -- "contributor", "collaborator" -- "access", "add", "remove", "管理" - -## Member Management Guidelines - -### 1. Member Listing -When listing members: -``` -1. Get all members with roles -2. Group by role (admin, member, viewer) -3. Note external collaborators -4. Provide contact information if available -``` - -### 2. Role Understanding -Common roles and their capabilities: -``` -Admin: -- Full project access -- Can manage members -- Can delete project - -Maintainer: -- Can manage repository settings -- Can merge PRs -- Can manage branches - -Developer: -- Can push to branches -- Can create PRs -- Can manage issues - -Viewer: -- Read-only access -- Can comment on issues/PRs -``` - -### 3. Contribution Analysis -When analyzing contributions: -``` -1. Check git_log by author -2. Identify active contributors -3. Note code ownership patterns -4. Flag inactive contributors -``` - -## Output Format - -### Member List -``` -## Team Members - -### Administrators ([N]) -- [@username] - [name] - [email] - -### Maintainers ([N]) -- [@username] - [name] - [email] - -### Developers ([N]) -- [@username] - [name] - [email] - -### Viewers ([N]) -- [@username] - [name] - [email] - -### External Collaborators ([N]) -- [@username] - [name] - [email] -``` - -### Member Detail -``` -## [@username] - -**Name:** [name] -**Email:** [email] -**Role:** [role] -**Joined:** [date] - -**Activity:** -- Commits: [N] (last 30 days) -- Issues: [N] opened, [N] closed -- PRs: [N] submitted, [N] merged - -**Permissions:** -- [Permission 1] -- [Permission 2] -``` - -### Role Update -``` -## Permission Updated - -**Member:** [@username] -**Previous Role:** [old role] -**New Role:** [new role] -**Updated by:** [admin name] -**Date:** [date] -``` - -## Best Practices -- Follow principle of least privilege -- Regular access reviews -- Remove inactive members -- Use groups for bulk permissions -- Document role changes -- Rotate admin access diff --git a/libs/agent/skills/templates/pr-summary.md b/libs/agent/skills/templates/pr-summary.md deleted file mode 100644 index 5abe441..0000000 --- a/libs/agent/skills/templates/pr-summary.md +++ /dev/null @@ -1,56 +0,0 @@ -# PR Summary Skill - -## Overview -You are an expert at summarizing pull requests. Your task is to provide clear, concise summaries of code changes. - -## Summary Guidelines - -### 1. Title -Create a brief, descriptive title for the PR (max 72 characters). - -### 2. Description -Write a clear explanation of: -- **What changed**: The main purpose of the PR -- **Why it changed**: The motivation or problem being solved -- **How it changed**: The approach taken - -### 3. Key Changes -List the most important changes (max 5 items): -1. [Change 1] -2. [Change 2] -3. [Change 3] - -### 4. Breaking Changes -List any breaking changes, or state "None" if none exist. - -### 5. Testing -Describe how the changes were tested. - -### 6. Screenshots/Visual Changes -If there are UI changes, describe or reference screenshots. - -## Output Format - -``` -## Title -[Short, descriptive title] - -## Summary -[Brief overview of what this PR does and why] - -## What Changed -- [Key change 1] -- [Key change 2] -- [Key change 3] - -## Breaking Changes -[None / List of breaking changes] - -## Testing -[How was this tested?] - -## Checklist -- [ ] Tests added/updated -- [ ] Documentation updated -- [ ] Code follows project conventions -``` diff --git a/libs/agent/skills/templates/repo-manager.md b/libs/agent/skills/templates/repo-manager.md deleted file mode 100644 index e7fb9ca..0000000 --- a/libs/agent/skills/templates/repo-manager.md +++ /dev/null @@ -1,112 +0,0 @@ -# Repository Management Skill - -## Overview -You are an expert at managing code repositories. Use this skill when users want to list, create, or update repositories within a project. - -## Available Tools - -### Repository Operations -- `list_repos_exec` - List all repositories in a project -- `create_repo_exec` - Create a new repository -- `update_repo_exec` - Update repository settings -- `create_commit_exec` - Create a commit directly (rare use case) - -### File Operations (for repo content) -- `git_file_content` - Read file from repository -- `git_tree_ls` - List directory contents -- `git_blob_content` - Read blob content -- `git_blob_create` - Create blob in repository - -## When to Use - -### Active Triggers (User Explicitly Asks) -- "有哪些仓库" / "what repositories exist" -- "列出项目仓库" / "list project repos" -- "创建新仓库" / "create new repository" -- "更新仓库设置" / "update repository settings" -- "仓库信息" / "repository information" -- "初始化仓库" / "initialize repository" - -### Passive Triggers (Tool Names) -- Tool `project_list_repos` called → activate this skill -- Tool `project_create_repo` called → activate this skill -- Tool `project_update_repo` called → activate this skill - -### Auto Triggers (Keywords) -- "repo", "repository", "仓库" -- "project", "项目" -- "create", "new", "创建", "新建" -- "update", "settings", "更新", "设置" - -## Repository Management Guidelines - -### 1. Repository Listing -When listing repositories: -``` -1. Use list_repos_exec to get all repos -2. Organize by: public/private, active/stale -3. Note default branches -4. Identify repository purposes -``` - -### 2. Repository Creation -When creating repositories: -``` -1. Verify project access and permissions -2. Check name uniqueness within project -3. Determine privacy (public/private) -4. Set appropriate default branch -5. Add description for discoverability -``` - -### 3. Repository Configuration -When updating settings: -``` -1. Only owner or admin can update -2. Validate all field changes -3. Log changes for audit -4. Notify relevant team members -``` - -## Output Format - -### Repository List Response -``` -## Repositories - -### Active Repositories -| Name | Description | Default Branch | Privacy | -|------|-------------|----------------|---------| -| [repo1] | [desc] | [branch] | [public/private] | -| [repo2] | [desc] | [branch] | [public/private] | - -### Archived/Stale -| Name | Last Activity | Notes | -|------|--------------|-------| -| [repo3] | [date] | [reason] | -``` - -### Repository Create Confirmation -``` -## Repository Created - -- **Name:** [name] -- **Description:** [description] -- **Default Branch:** [branch] -- **Privacy:** [public/private] -- **Clone URLs:** - - SSH: [url] - - HTTPS: [url] - -**Next Steps:** -1. Clone the repository -2. Add collaborators -3. Set up CI/CD -``` - -## Best Practices -- Use descriptive repository names -- Set appropriate privacy levels -- Maintain clear descriptions -- Use consistent default branch naming -- Archive unused repositories instead of deleting diff --git a/libs/agent/skills/templates/test-generator.md b/libs/agent/skills/templates/test-generator.md deleted file mode 100644 index 2eebbeb..0000000 --- a/libs/agent/skills/templates/test-generator.md +++ /dev/null @@ -1,59 +0,0 @@ -# Test Generator Skill - -## Overview -You are an expert at writing unit tests. Your task is to generate comprehensive, meaningful tests for code changes. - -## Test Generation Guidelines - -### 1. Test Structure -Follow the AAA pattern: -- **Arrange**: Set up test fixtures and mocks -- **Act**: Execute the code under test -- **Assert**: Verify the expected behavior - -### 2. Test Coverage Priorities -Focus on: -1. Happy path - main functionality works -2. Edge cases - boundary conditions, empty inputs, null values -3. Error handling - proper error messages and types -4. Security - input validation, authorization checks -5. Performance - timeout handling, large inputs - -### 3. Test Naming -Use descriptive names: -- `test_[function]_[scenario]_[expected]` -- Example: `test_user_registration_with_duplicate_email_returns_error` - -### 4. Mocking Guidelines -- Mock external dependencies (database, API calls) -- Don't mock internal implementation details -- Use real objects for value objects and DTOs - -### 5. Test Data -Use realistic, meaningful test data: -- Avoid "test", "foo", "bar" for user-facing content -- Use edge case values (empty string, max length, special characters) - -## Output Format - -For each test, provide: -``` -### [Test Name] - -**Given:** [Preconditions] -**When:** [Action taken] -**Then:** [Expected outcome] - -```[language] -[Complete, runnable test code] -``` -``` - -## Test Checklist - -- [ ] Happy path test -- [ ] Empty/null input tests -- [ ] Boundary value tests -- [ ] Error case tests -- [ ] Integration with mocked dependencies -- [ ] Test isolation (no cross-test dependencies) diff --git a/libs/agent/sync.rs b/libs/agent/sync.rs deleted file mode 100644 index 08e20d0..0000000 --- a/libs/agent/sync.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Model sync from OpenRouter — syncs AI model metadata into the local database. - -use crate::error::AgentError; - -/// Response from `GET /v1/models`. -#[derive(Debug, serde::Deserialize)] -pub struct ModelsListResponse { - pub data: Vec, -} - -#[derive(Debug, serde::Deserialize)] -pub struct ModelEntry { - pub id: String, -} - -pub async fn list_accessible_models( - client: &reqwest::Client, - base_url: &str, - api_key: &str, -) -> Result, AgentError> { - let base = base_url.trim_end_matches('/'); - let url = if base.ends_with("/v1") { - format!("{}/models", base) - } else { - format!("{}/v1/models", base) - }; - let resp = client - .get(&url) - .header("Authorization", format!("Bearer {}", api_key)) - .send() - .await - .map_err(|e| AgentError::Internal(format!("failed to list models: {}", e)))?; - - let body: ModelsListResponse = resp - .json() - .await - .map_err(|e| AgentError::Internal(format!("failed to parse models response: {}", e)))?; - - Ok(body.data.into_iter().map(|m| m.id).collect()) -} diff --git a/libs/agent/task/events.rs b/libs/agent/task/events.rs deleted file mode 100644 index 1deedf2..0000000 --- a/libs/agent/task/events.rs +++ /dev/null @@ -1,171 +0,0 @@ -use models::agent_task::TaskStatus; -use serde::Serialize; -use std::sync::Arc; - -/// Event payload published to WebSocket clients via Redis Pub/Sub. -#[derive(Debug, Clone, Serialize)] -pub struct TaskEvent { - pub task_id: i64, - pub project_id: uuid::Uuid, - pub parent_id: Option, - pub event: String, - pub message: Option, - pub output: Option, - pub error: Option, - pub status: String, -} - -impl TaskEvent { - pub fn started(task_id: i64, project_id: uuid::Uuid, parent_id: Option) -> Self { - Self { - task_id, - project_id, - parent_id, - event: "started".to_string(), - message: None, - output: None, - error: None, - status: TaskStatus::Running.to_string(), - } - } - - pub fn progress( - task_id: i64, - project_id: uuid::Uuid, - parent_id: Option, - msg: String, - ) -> Self { - Self { - task_id, - project_id, - parent_id, - event: "progress".to_string(), - message: Some(msg), - output: None, - error: None, - status: TaskStatus::Running.to_string(), - } - } - - pub fn completed( - task_id: i64, - project_id: uuid::Uuid, - parent_id: Option, - output: String, - ) -> Self { - Self { - task_id, - project_id, - parent_id, - event: "done".to_string(), - message: None, - output: Some(output), - error: None, - status: TaskStatus::Done.to_string(), - } - } - - pub fn failed( - task_id: i64, - project_id: uuid::Uuid, - parent_id: Option, - error: String, - ) -> Self { - Self { - task_id, - project_id, - parent_id, - event: "failed".to_string(), - message: None, - output: None, - error: Some(error), - status: TaskStatus::Failed.to_string(), - } - } - - pub fn cancelled(task_id: i64, project_id: uuid::Uuid, parent_id: Option) -> Self { - Self { - task_id, - project_id, - parent_id, - event: "cancelled".to_string(), - message: None, - output: None, - error: None, - status: TaskStatus::Cancelled.to_string(), - } - } -} - -/// Helper trait for publishing task lifecycle events via Redis Pub/Sub. -/// -/// Callers inject a suitable `publish_fn` at construction time via -/// `TaskEvents::new(...)`. If no publisher is supplied events are silently -/// dropped (graceful degradation on startup). -pub trait TaskEventPublisher: Send + Sync { - fn publish(&self, project_id: uuid::Uuid, event: TaskEvent); -} - -/// No-op publisher used when no Redis Pub/Sub connection is available. -#[derive(Clone, Default)] -pub struct NoOpPublisher; - -impl TaskEventPublisher for NoOpPublisher { - fn publish(&self, _: uuid::Uuid, _: TaskEvent) {} -} - -#[derive(Clone)] -pub struct TaskEvents { - publisher: Arc, -} - -impl TaskEvents { - pub fn new(publisher: impl TaskEventPublisher + 'static) -> Self { - Self { - publisher: Arc::new(publisher), - } - } - - pub fn noop() -> Self { - Self::new(NoOpPublisher) - } - - fn emit(&self, task: &models::agent_task::Model, event: TaskEvent) { - self.publisher.publish(task.project_uuid, event); - } - - pub fn emit_started(&self, task: &models::agent_task::Model) { - self.emit( - task, - TaskEvent::started(task.id, task.project_uuid, task.parent_id), - ); - } - - pub fn emit_progress(&self, task: &models::agent_task::Model, msg: String) { - self.emit( - task, - TaskEvent::progress(task.id, task.project_uuid, task.parent_id, msg), - ); - } - - pub fn emit_completed(&self, task: &models::agent_task::Model, output: String) { - self.emit( - task, - TaskEvent::completed(task.id, task.project_uuid, task.parent_id, output), - ); - } - - pub fn emit_failed(&self, task: &models::agent_task::Model, error: String) { - self.emit( - task, - TaskEvent::failed(task.id, task.project_uuid, task.parent_id, error), - ); - } - - pub fn emit_cancelled(&self, task: &models::agent_task::Model) { - self.emit( - task, - TaskEvent::cancelled(task.id, task.project_uuid, task.parent_id), - ); - } -} diff --git a/libs/agent/task/lifecycle.rs b/libs/agent/task/lifecycle.rs deleted file mode 100644 index 71dfa3c..0000000 --- a/libs/agent/task/lifecycle.rs +++ /dev/null @@ -1,192 +0,0 @@ -use models::agent_task::{ActiveModel, Column as C, Entity, Model, TaskStatus}; -use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter}; - -pub struct TaskLifecycle; - -impl super::TaskService { - /// Mark a task as running and record the start time. - pub async fn start(&self, task_id: i64) -> Result { - let model = Entity::find_by_id(task_id).one(self.db()).await?; - let model = - model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - - let mut active: ActiveModel = model.into(); - active.status = sea_orm::Set(TaskStatus::Running); - active.started_at = sea_orm::Set(Some(chrono::Utc::now().into())); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - let updated = active.update(self.db()).await?; - self.events().emit_started(&updated); - Ok(updated) - } - - /// Update progress text (e.g., "step 2/5: analyzing PR"). - pub async fn update_progress( - &self, - task_id: i64, - progress: impl Into, - ) -> Result<(), DbErr> { - let model = Entity::find_by_id(task_id).one(self.db()).await?; - let model = - model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - - let progress_str = progress.into(); - let mut active: ActiveModel = model.into(); - active.progress = sea_orm::Set(Some(progress_str.clone())); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - let updated = active.update(self.db()).await?; - self.events().emit_progress(&updated, progress_str); - Ok(()) - } - - /// Mark a task as completed with the output text. - pub async fn complete(&self, task_id: i64, output: impl Into) -> Result { - let model = Entity::find_by_id(task_id).one(self.db()).await?; - let model = - model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - - let mut active: ActiveModel = model.into(); - active.status = sea_orm::Set(TaskStatus::Done); - let out = output.into(); - active.output = sea_orm::Set(Some(out.clone())); - active.done_at = sea_orm::Set(Some(chrono::Utc::now().into())); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - let updated = active.update(self.db()).await?; - self.events().emit_completed(&updated, out); - Ok(updated) - } - - /// Mark a task as failed with an error message. - pub async fn fail(&self, task_id: i64, error: impl Into) -> Result { - let model = Entity::find_by_id(task_id).one(self.db()).await?; - let model = - model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - - let mut active: ActiveModel = model.into(); - active.status = sea_orm::Set(TaskStatus::Failed); - let err = error.into(); - active.error = sea_orm::Set(Some(err.clone())); - active.done_at = sea_orm::Set(Some(chrono::Utc::now().into())); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - let updated = active.update(self.db()).await?; - self.events().emit_failed(&updated, err); - Ok(updated) - } - - /// Propagate child task status up the tree. - /// - /// Only allows cancelling tasks that are not yet in a terminal state - /// (Pending / Running / Paused). - /// - /// Cancelled children are marked done so that `are_children_done()` returns - /// true for the parent after cancellation. - pub async fn cancel(&self, task_id: i64) -> Result { - // Collect all task IDs (parent + descendants) using an explicit stack. - let mut stack = vec![task_id]; - let mut idx = 0; - while idx < stack.len() { - let current = stack[idx]; - let children = Entity::find() - .filter(C::ParentId.eq(current)) - .all(self.db()) - .await?; - for child in children { - stack.push(child.id); - } - idx += 1; - } - - // Mark every collected task as cancelled (terminal state). - for id in &stack { - let model = Entity::find_by_id(*id).one(self.db()).await?; - if let Some(m) = model { - if !m.is_done() { - let mut active: ActiveModel = m.into(); - active.status = sea_orm::Set(TaskStatus::Cancelled); - active.done_at = sea_orm::Set(Some(chrono::Utc::now().into())); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - active.update(self.db()).await?; - } - } - } - - let final_model = Entity::find_by_id(task_id) - .one(self.db()) - .await? - .ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - self.events().emit_cancelled(&final_model); - Ok(final_model) - } - - /// Pause a running or pending task. - /// - /// Pausing a task that is not Pending/Running is a no-op that returns - /// the current model (same behaviour as `start` on an already-running task). - pub async fn pause(&self, task_id: i64) -> Result { - let model = Entity::find_by_id(task_id).one(self.db()).await?; - let model = - model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - - if !model.is_running() { - // Already in a terminal or paused state — return unchanged. - return Ok(model); - } - - let mut active: ActiveModel = model.into(); - active.status = sea_orm::Set(TaskStatus::Paused); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - active.update(self.db()).await - } - - /// Resume a paused task back to Running. - /// - /// Returns an error if the task is not currently Paused. - pub async fn resume(&self, task_id: i64) -> Result { - let model = Entity::find_by_id(task_id).one(self.db()).await?; - let model = - model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - - if model.status != TaskStatus::Paused { - return Err(DbErr::Custom(format!( - "cannot resume task {}: expected status Paused, got {}", - task_id, model.status - ))); - } - - let mut active: ActiveModel = model.into(); - active.status = sea_orm::Set(TaskStatus::Running); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - active.update(self.db()).await - } - - /// Retry a failed or cancelled task by resetting it to Pending. - /// - /// Clears `output`, `error`, and `done_at`; increments `retry_count`. - /// Only tasks in Failed or Cancelled state can be retried. - pub async fn retry(&self, task_id: i64) -> Result { - let model = Entity::find_by_id(task_id).one(self.db()).await?; - let model = - model.ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - - match model.status { - TaskStatus::Failed | TaskStatus::Cancelled | TaskStatus::Done => {} - _ => { - return Err(DbErr::Custom(format!( - "cannot retry task {}: only Failed/Cancelled/Done tasks can be retried (got {})", - task_id, model.status - ))); - } - } - - let retry_count = model.retry_count.map(|c| c + 1).unwrap_or(1); - - let mut active: ActiveModel = model.into(); - active.status = sea_orm::Set(TaskStatus::Pending); - active.output = sea_orm::Set(None); - active.error = sea_orm::Set(None); - active.done_at = sea_orm::Set(None); - active.started_at = sea_orm::Set(None); - active.retry_count = sea_orm::Set(Some(retry_count)); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - active.update(self.db()).await - } -} diff --git a/libs/agent/task/mod.rs b/libs/agent/task/mod.rs deleted file mode 100644 index 12dd38b..0000000 --- a/libs/agent/task/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Agent task service — managing task/sub-agent execution lifecycle. -//! -//! A task (`agent_task` record) can be: -//! - A **root task**: initiated by a user or system event. -//! The parent/Supervisor agent spawns sub-tasks and coordinates their results. -//! - A **sub-task**: a unit of work executed by a sub-agent. -//! -//! Execution flow: -//! 1. Create task record (status = pending) -//! 2. Notify listeners (WebSocket: task_started) -//! 3. Spawn execution (tokio::spawn or via room queue) -//! 4. Update progress (status = running, progress = "step 2/5: ...") -//! 5. On completion: update output + status = done / error + status = failed -//! 6. Notify listeners (WebSocket: task_done) -//! 7. If root task: notify parent/Supervisor to aggregate results -//! -//! This module is intentionally kept simple and synchronous with the DB. -//! Long-running execution is delegated to the caller (tokio::spawn). - -pub mod events; -pub mod lifecycle; -pub mod store; -pub mod tree; - -use db::database::AppDatabase; - -pub use events::{NoOpPublisher, TaskEvent, TaskEventPublisher, TaskEvents}; -pub use lifecycle::TaskLifecycle; - -/// Service for managing agent tasks (root tasks and sub-tasks). -#[derive(Clone)] -pub struct TaskService { - db: AppDatabase, - events: TaskEvents, -} - -impl TaskService { - pub fn new(db: AppDatabase) -> Self { - Self { - db, - events: TaskEvents::noop(), - } - } - - pub fn with_events(db: AppDatabase, events: TaskEvents) -> Self { - Self { db, events } - } - - pub(crate) fn db(&self) -> &AppDatabase { - &self.db - } - - pub(crate) fn events(&self) -> &TaskEvents { - &self.events - } -} - -/// Builder for TaskService so that the events publisher can be set independently -/// of the database connection. -#[derive(Clone, Default)] -pub struct TaskServiceBuilder { - events: Option, -} - -impl TaskServiceBuilder { - pub fn with_events(mut self, events: TaskEvents) -> Self { - self.events = Some(events); - self - } - - pub async fn build(self, db: AppDatabase) -> TaskService { - TaskService { - db, - events: self.events.unwrap_or_else(TaskEvents::noop), - } - } -} diff --git a/libs/agent/task/store.rs b/libs/agent/task/store.rs deleted file mode 100644 index c14f174..0000000 --- a/libs/agent/task/store.rs +++ /dev/null @@ -1,111 +0,0 @@ -use models::IssueId; -use models::agent_task::{ActiveModel, AgentType, Entity, Model}; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, QueryOrder, QuerySelect, -}; - -impl super::TaskService { - /// Get a task by ID. - pub async fn get(&self, task_id: i64) -> Result, DbErr> { - Entity::find_by_id(task_id).one(self.db()).await - } - - /// List all tasks (root only) for a project. - pub async fn list( - &self, - project_uuid: impl Into, - limit: u64, - ) -> Result, DbErr> { - let uuid: uuid::Uuid = project_uuid.into(); - Entity::find() - .filter(models::agent_task::Column::ProjectUuid.eq(uuid)) - .filter(models::agent_task::Column::ParentId.is_null()) - .order_by_desc(models::agent_task::Column::CreatedAt) - .limit(limit) - .all(self.db()) - .await - } - - /// List all active (non-terminal) tasks for a project. - pub async fn active_tasks( - &self, - project_uuid: impl Into, - ) -> Result, DbErr> { - let uuid: uuid::Uuid = project_uuid.into(); - Entity::find() - .filter(models::agent_task::Column::ProjectUuid.eq(uuid)) - .filter(models::agent_task::Column::Status.is_in([ - models::agent_task::TaskStatus::Pending, - models::agent_task::TaskStatus::Running, - models::agent_task::TaskStatus::Paused, - ])) - .order_by_desc(models::agent_task::Column::CreatedAt) - .all(self.db()) - .await - } - - /// Create a new task (root or sub-task) with status = pending. - pub async fn create( - &self, - project_uuid: impl Into, - input: impl Into, - agent_type: AgentType, - ) -> Result { - self.create_with_parent(project_uuid, None, input, agent_type, None, None) - .await - } - - /// Create a new task bound to an issue. - pub async fn create_for_issue( - &self, - project_uuid: impl Into, - issue_id: IssueId, - input: impl Into, - agent_type: AgentType, - ) -> Result { - self.create_with_parent(project_uuid, None, input, agent_type, None, Some(issue_id)) - .await - } - - /// Create a new sub-task with a parent reference. - pub async fn create_subtask( - &self, - project_uuid: impl Into, - parent_id: i64, - input: impl Into, - agent_type: AgentType, - title: Option, - ) -> Result { - self.create_with_parent( - project_uuid, - Some(parent_id), - input, - agent_type, - title, - None, - ) - .await - } - - async fn create_with_parent( - &self, - project_uuid: impl Into, - parent_id: Option, - input: impl Into, - agent_type: AgentType, - title: Option, - issue_id: Option, - ) -> Result { - let model = ActiveModel { - project_uuid: sea_orm::Set(project_uuid.into()), - parent_id: sea_orm::Set(parent_id), - issue_id: sea_orm::Set(issue_id), - agent_type: sea_orm::Set(agent_type), - status: sea_orm::Set(models::agent_task::TaskStatus::Pending), - title: sea_orm::Set(title), - input: sea_orm::Set(input.into()), - ..Default::default() - }; - model.insert(self.db()).await - } -} diff --git a/libs/agent/task/tree.rs b/libs/agent/task/tree.rs deleted file mode 100644 index 86e202e..0000000 --- a/libs/agent/task/tree.rs +++ /dev/null @@ -1,89 +0,0 @@ -use models::agent_task::{ActiveModel, Column as C, Entity, Model, TaskStatus}; -use sea_orm::{ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, QueryOrder}; - -impl super::TaskService { - /// Propagate child task status up the tree. - /// - /// When a child task reaches a terminal state, checks whether all its - /// siblings are also terminal. If so, marks the parent appropriately: - /// - Done if any child succeeded - /// - Failed if all children failed or were cancelled - pub async fn propagate_to_parent(&self, task_id: i64) -> Result, DbErr> { - let model = self - .get(task_id) - .await? - .ok_or_else(|| DbErr::RecordNotFound("agent_task not found".to_string()))?; - - let Some(parent_id) = model.parent_id else { - return Ok(None); - }; - - let siblings = self.children(parent_id).await?; - if siblings.iter().all(|s| s.is_done()) { - let parent = self.get(parent_id).await?.ok_or_else(|| { - DbErr::RecordNotFound(format!("parent task {} not found", parent_id)) - })?; - if parent.is_running() { - let mut active: ActiveModel = parent.into(); - let has_success = siblings.iter().any(|s| s.status == TaskStatus::Done); - if has_success { - active.status = sea_orm::Set(TaskStatus::Done); - active.error = sea_orm::Set(None); - } else { - active.status = sea_orm::Set(TaskStatus::Failed); - active.error = - sea_orm::Set(Some("All sub-tasks failed or were cancelled".to_string())); - } - active.done_at = sea_orm::Set(Some(chrono::Utc::now().into())); - active.updated_at = sea_orm::Set(chrono::Utc::now().into()); - let updated = active.update(self.db()).await?; - return Ok(Some(updated)); - } - } - Ok(None) - } - - /// List all sub-tasks for a parent task. - pub async fn children(&self, parent_id: i64) -> Result, DbErr> { - Entity::find() - .filter(C::ParentId.eq(parent_id)) - .order_by_asc(C::CreatedAt) - .all(self.db()) - .await - } - - /// Check if all sub-tasks of a given parent are in a terminal state. - /// Returns true if there are no children (empty tree counts as done). - pub async fn are_children_done(&self, parent_id: i64) -> Result { - let children = self.children(parent_id).await?; - Ok(children.is_empty() || children.iter().all(|c| c.is_done())) - } - - /// Delete a task and all its sub-tasks recursively. - /// Only allows deletion of root tasks. - pub async fn delete(&self, task_id: i64) -> Result<(), DbErr> { - // Collect all task IDs to delete using an explicit stack (avoiding async recursion). - let mut stack = vec![task_id]; - let mut idx = 0; - while idx < stack.len() { - let current = stack[idx]; - let children = Entity::find() - .filter(C::ParentId.eq(current)) - .all(self.db()) - .await?; - for child in children { - stack.push(child.id); - } - idx += 1; - } - - for task_id in stack { - let model = Entity::find_by_id(task_id).one(self.db()).await?; - if let Some(m) = model { - let active: ActiveModel = m.into(); - active.delete(self.db()).await?; - } - } - Ok(()) - } -} diff --git a/libs/agent/tokent.rs b/libs/agent/tokent.rs deleted file mode 100644 index 4460003..0000000 --- a/libs/agent/tokent.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! Token counting utilities using tiktoken. -//! -//! Provides accurate token counting for OpenAI-compatible models. -//! Uses the `tiktoken-rs` crate (already in workspace dependencies). -//! -//! # Strategy -//! -//! Remote usage from API response is always preferred. When the API does not -//! return usage metadata (e.g., local models, streaming), tiktoken is used as -//! a fallback for accurate counting. - -use std::collections::HashMap; -use std::sync::OnceLock; -use std::sync::RwLock; - -use crate::error::Result; - -static TOKENIZER_CACHE: OnceLock>> = OnceLock::new(); - -fn get_cached_tokenizers() -> &'static RwLock> { - TOKENIZER_CACHE.get_or_init(|| RwLock::new(HashMap::new())) -} - -/// Token usage data. Use `from_remote()` when the API returns usage info, -/// or `from_estimate()` when falling back to tiktoken. -#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)] -pub struct TokenUsage { - pub input_tokens: i64, - pub output_tokens: i64, -} - -impl TokenUsage { - /// Create from remote API usage data. Returns `None` if all values are zero - /// (some providers return zeroed usage on error). - pub fn from_remote(prompt_tokens: u32, completion_tokens: u32) -> Option { - if prompt_tokens == 0 && completion_tokens == 0 { - None - } else { - Some(Self { - input_tokens: prompt_tokens as i64, - output_tokens: completion_tokens as i64, - }) - } - } - - /// Create from tiktoken estimate. - pub fn from_estimate(input_tokens: usize, output_tokens: usize) -> Self { - Self { - input_tokens: input_tokens as i64, - output_tokens: output_tokens as i64, - } - } - - pub fn total(&self) -> i64 { - self.input_tokens + self.output_tokens - } -} - -/// Resolve token usage: remote data is preferred, tiktoken is the fallback. -/// -/// `remote` — `Some` when API returned usage; `None` when not available. -/// `model` — model name, required for tiktoken fallback. -/// `input_text` — input text length hint for fallback estimate (uses ~4 chars/token). -pub fn resolve_usage( - remote: Option, - model: &str, - input_text: &str, - output_text: &str, -) -> TokenUsage { - if let Some(usage) = remote { - return usage; - } - - // Fallback: tiktoken estimate - let input = count_message_text(input_text, model).unwrap_or_else(|_| { - // Rough estimate: ~4 chars per token - (input_text.len() / 4).max(1) - }); - let output = output_text.len() / 4; - TokenUsage::from_estimate(input, output) -} - -/// Estimate the number of tokens in a text string using the appropriate tokenizer. -pub fn count_text(text: &str, model: &str) -> Result { - let bpe = get_tokenizer(model)?; - // Use encode_ordinary since we're counting raw text, not chat messages - let tokens = bpe.encode_ordinary(text); - Ok(tokens.len()) -} - -/// Count tokens in a single chat message (text content only). -pub fn count_message_text(text: &str, model: &str) -> Result { - let bpe = get_tokenizer(model)?; - // For messages, use encode_with_special_tokens to count role/separator tokens - let tokens = bpe.encode_with_special_tokens(text); - Ok(tokens.len()) -} - -/// Estimate the maximum number of characters that fit within a token budget -/// given a model's context limit and a reserve for the output. -/// -/// Uses a rough estimate of ~4 characters per token (typical for English text). -/// For non-Latin scripts, this is less accurate. -pub fn estimate_max_chars( - _model: &str, - context_limit: usize, - reserve_output_tokens: usize, -) -> Result { - let chars_per_token = 4; - // Subtract reserve for output, system overhead, and a safety margin (10%) - let safe_limit = context_limit - .saturating_sub(reserve_output_tokens) - .saturating_sub(512); // 512 token safety margin - Ok(safe_limit.saturating_mul(chars_per_token)) -} - -/// Truncate text to fit within a token budget for a given model. -pub fn truncate_to_token_budget( - text: &str, - model: &str, - context_limit: usize, - reserve_output_tokens: usize, -) -> Result { - let max_chars = estimate_max_chars(model, context_limit, reserve_output_tokens)?; - - if text.len() <= max_chars { - return Ok(text.to_string()); - } - - // Binary search for the exact character boundary that fits the token budget - let bpe = get_tokenizer(model)?; - let mut low = 0usize; - let mut high = text.len(); - let mut result = text.to_string(); - - while low + 100 < high { - let mid = (low + high) / 2; - // Find the nearest valid char boundary to avoid panicking on multi-byte UTF-8 - let mid = text.floor_char_boundary(mid); - let candidate = &text[..mid]; - let tokens = bpe.encode_ordinary(candidate); - - if tokens.len() <= safe_token_budget(context_limit, reserve_output_tokens) { - result = candidate.to_string(); - low = mid; - } else { - high = mid; - } - } - - Ok(result) -} - -/// Returns the safe token budget (context limit minus reserve and margin). -fn safe_token_budget(context_limit: usize, reserve: usize) -> usize { - context_limit.saturating_sub(reserve).saturating_sub(512) -} - -/// Get the appropriate tiktoken tokenizer for a model. -/// -/// Model name mapping: -/// - "gpt-4o", "o1", "o3", "o4" → o200k_base -/// - "claude-*", "gpt-3.5-turbo", "gpt-4" → cl100k_base -/// - Unknown → cl100k_base (safe fallback) -fn get_tokenizer(model: &str) -> Result { - use tiktoken_rs; - - { - let cache = get_cached_tokenizers().read().unwrap(); - if let Some(bpe) = cache.get(model) { - return Ok(bpe.clone()); - } - } - - // Try model-specific tokenizer first - let bpe: &'static _ = if let Ok(bpe) = tiktoken_rs::bpe_for_model(model) { - bpe - } else { - // Fallback: use cl100k_base for unknown models - tiktoken_rs::cl100k_base_singleton() - }; - - { - let mut cache = get_cached_tokenizers().write().unwrap(); - cache.insert(model.to_string(), bpe.clone()); - } - - Ok(bpe.clone()) -} - -/// Estimate tokens for a simple prefix/suffix pattern (e.g., "assistant\n" + text). -/// Returns the token count including the prefix. -pub fn count_with_prefix(text: &str, prefix: &str, model: &str) -> Result { - let bpe = get_tokenizer(model)?; - let prefixed = format!("{}{}", prefix, text); - let tokens = bpe.encode_with_special_tokens(&prefixed); - Ok(tokens.len()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_count_text() { - let count = count_text("Hello, world!", "gpt-4").unwrap(); - assert!(count > 0); - } - - #[test] - fn test_estimate_max_chars() { - // gpt-4o context ~128k tokens - let chars = estimate_max_chars("gpt-4o", 128_000, 2048).unwrap(); - assert!(chars > 0); - } - - #[test] - fn test_truncate() { - // 50k chars exceeds budget: 8192 - 512 - 512 = 7168 tokens → ~28k chars - let long_text = "a".repeat(50000); - let truncated = truncate_to_token_budget(&long_text, "gpt-4o", 8192, 512).unwrap(); - assert!(truncated.len() < long_text.len()); - } -} diff --git a/libs/agent/tool/call.rs b/libs/agent/tool/call.rs deleted file mode 100644 index 4588c4a..0000000 --- a/libs/agent/tool/call.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Tool call and result types. - -use serde::{Deserialize, Serialize}; - -/// A single tool invocation requested by the AI model. -#[derive(Debug, Clone)] -pub struct ToolCall { - pub id: String, - pub name: String, - pub arguments: String, -} - -impl ToolCall { - pub fn arguments_json(&self) -> serde_json::Result { - serde_json::from_str(&self.arguments) - } - - pub fn parse_args(&self) -> serde_json::Result { - serde_json::from_str(&self.arguments) - } -} - -/// The result of executing a tool call. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "status", content = "value")] -pub enum ToolResult { - /// Successful result with a JSON value. - #[serde(rename = "ok")] - Ok(serde_json::Value), - /// Error result with an error message. - #[serde(rename = "error")] - Error(String), -} - -impl ToolResult { - pub fn ok(value: T) -> Self { - Self::Ok(serde_json::to_value(value).unwrap_or(serde_json::Value::Null)) - } - - pub fn error(message: impl Into) -> Self { - Self::Error(message.into()) - } - - pub fn is_error(&self) -> bool { - matches!(self, Self::Error(_)) - } -} - -/// Errors that can occur during tool execution. -#[derive(Debug, thiserror::Error)] -pub enum ToolError { - #[error("tool not found: {0}")] - NotFound(String), - - #[error("argument parse error: {0}")] - ParseError(String), - - #[error("execution error: {0}")] - ExecutionError(String), - - #[error("recursion limit exceeded (max depth: {max_depth})")] - RecursionLimitExceeded { max_depth: u32 }, - - #[error("max tool calls exceeded: {0}")] - MaxToolCallsExceeded(usize), - - #[error("internal error: {0}")] - Internal(String), -} - -impl ToolError { - pub fn into_result(self) -> ToolResult { - ToolResult::Error(self.to_string()) - } -} - -impl From for ToolError { - fn from(e: serde_json::Error) -> Self { - Self::ParseError(e.to_string()) - } -} - -/// A completed tool call with its result, ready to be sent back to the AI. -#[derive(Debug, Clone)] -pub struct ToolCallResult { - /// The original tool call. - pub call: ToolCall, - /// The execution result. - pub result: ToolResult, -} - -impl ToolCallResult { - pub fn ok(call: ToolCall, value: serde_json::Value) -> Self { - Self { - call, - result: ToolResult::Ok(value), - } - } - - pub fn error(call: ToolCall, message: impl Into) -> Self { - Self { - call, - result: ToolResult::Error(message.into()), - } - } - - pub fn from_result(call: ToolCall, result: ToolResult) -> Self { - Self { call, result } - } -} diff --git a/libs/agent/tool/context.rs b/libs/agent/tool/context.rs deleted file mode 100644 index 4966362..0000000 --- a/libs/agent/tool/context.rs +++ /dev/null @@ -1,245 +0,0 @@ -//! Execution context passed to each tool handler. -//! -//! Carries runtime information a tool handler needs: database, cache, -//! request metadata, and the tool registry. Cheap to clone via `Arc`. - -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use config::AppConfig; -use db::cache::AppCache; -use db::database::AppDatabase; -use queue::MessageProducer; -use uuid::Uuid; - -use super::registry::ToolRegistry; - -/// Context available during tool execution. Cheap to clone via `Arc`. -#[derive(Clone)] -pub struct ToolContext { - inner: Arc, -} - -#[derive(Clone)] -struct Inner { - pub db: AppDatabase, - pub cache: AppCache, - pub config: AppConfig, - pub room_id: Uuid, - pub sender_id: Option, - pub project_id: Uuid, - pub registry: ToolRegistry, - pub embed_service: Option, - pub message_producer: Option, - /// When in room context, identifies the AI model that is responding. - /// Used by send_message/retract_message to set the correct sender. - pub ai_model_id: Option, - pub ai_model_name: Option, - /// Message IDs sent by the AI in the current ReAct turn. - /// Shared across tool calls so send_message can register IDs - /// and retract_message can validate turn-scoped retraction. - pub sent_in_turn: std::sync::Arc>>, - depth: u32, - max_depth: u32, - tool_call_count: Arc, - max_tool_calls: usize, -} - -impl ToolContext { - pub fn new( - db: AppDatabase, - cache: AppCache, - config: AppConfig, - room_id: Uuid, - sender_id: Option, - ) -> Self { - Self { - inner: Arc::new(Inner { - db, - cache, - config, - room_id, - sender_id, - project_id: Uuid::nil(), - registry: ToolRegistry::new(), - embed_service: None, - message_producer: None, - ai_model_id: None, - ai_model_name: None, - sent_in_turn: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), - depth: 0, - max_depth: 5, - tool_call_count: Arc::new(AtomicUsize::new(0)), - max_tool_calls: 128, - }), - } - } - - pub fn with_project(mut self, project_id: Uuid) -> Self { - Arc::make_mut(&mut self.inner).project_id = project_id; - self - } - - pub fn with_registry(mut self, registry: ToolRegistry) -> Self { - Arc::make_mut(&mut self.inner).registry = registry; - self - } - - pub fn with_max_depth(mut self, max_depth: u32) -> Self { - Arc::make_mut(&mut self.inner).max_depth = max_depth; - self - } - - pub fn with_max_tool_calls(mut self, max: usize) -> Self { - Arc::make_mut(&mut self.inner).max_tool_calls = max; - self - } - - pub fn with_embed_service(mut self, embed_service: crate::embed::EmbedService) -> Self { - Arc::make_mut(&mut self.inner).embed_service = Some(embed_service); - self - } - - pub fn with_message_producer(mut self, producer: MessageProducer) -> Self { - Arc::make_mut(&mut self.inner).message_producer = Some(producer); - self - } - - pub fn with_ai_model(mut self, model_id: Uuid, model_name: String) -> Self { - Arc::make_mut(&mut self.inner).ai_model_id = Some(model_id); - Arc::make_mut(&mut self.inner).ai_model_name = Some(model_name); - self - } - - pub fn with_sent_in_turn(mut self, sent: std::sync::Arc>>) -> Self { - Arc::make_mut(&mut self.inner).sent_in_turn = sent; - self - } - - /// Register a message ID as sent in the current turn (called by send_message). - pub fn register_sent_message(&self, id: Uuid) { - if let Ok(mut list) = self.inner.sent_in_turn.lock() { - list.push(id); - } - } - - /// Check if a message ID was sent in the current turn (called by retract_message). - pub fn is_sent_in_turn(&self, id: Uuid) -> bool { - self.inner - .sent_in_turn - .lock() - .map(|list| list.contains(&id)) - .unwrap_or(false) - } - - pub fn embed_service(&self) -> Option<&crate::embed::EmbedService> { - self.inner.embed_service.as_ref() - } - - /// Message queue producer for publishing room events (messages, retractions, etc.). - pub fn message_producer(&self) -> Option<&MessageProducer> { - self.inner.message_producer.as_ref() - } - - pub fn recursion_exceeded(&self) -> bool { - self.inner.depth >= self.inner.max_depth - } - - pub fn tool_calls_exceeded(&self) -> bool { - self.inner.tool_call_count.load(Ordering::Relaxed) >= self.inner.max_tool_calls - } - - /// Current recursion depth. - pub fn depth(&self) -> u32 { - self.inner.depth - } - - /// Current tool call count. - pub fn tool_call_count(&self) -> usize { - self.inner.tool_call_count.load(Ordering::Relaxed) - } - - /// Reserves a number of tool calls for this shared execution context. - pub(crate) fn reserve_tool_calls(&self, additional: usize) -> Result<(), usize> { - if additional == 0 { - return Ok(()); - } - - let current = &self.inner.tool_call_count; - let max_tool_calls = self.inner.max_tool_calls; - - loop { - let existing = current.load(Ordering::Relaxed); - let Some(next) = existing.checked_add(additional) else { - return Err(existing); - }; - if next > max_tool_calls { - return Err(existing); - } - - match current.compare_exchange(existing, next, Ordering::AcqRel, Ordering::Relaxed) { - Ok(_) => return Ok(()), - Err(_) => continue, - } - } - } - - /// Returns a child context for a recursive tool call (depth + 1). - pub(crate) fn child_context(&self) -> Self { - let mut inner = (*self.inner).clone(); - inner.depth += 1; - Self { - inner: Arc::new(inner), - } - } - - /// Database connection. - pub fn db(&self) -> &AppDatabase { - &self.inner.db - } - - /// Redis cache. - pub fn cache(&self) -> &AppCache { - &self.inner.cache - } - - /// Application config. - pub fn config(&self) -> &AppConfig { - &self.inner.config - } - - /// Room where the original message was sent. - pub fn room_id(&self) -> Uuid { - self.inner.room_id - } - - /// User who sent the original message. - pub fn sender_id(&self) -> Option { - self.inner.sender_id - } - - /// AI model ID when in room context (the AI that is responding). - pub fn ai_model_id(&self) -> Option { - self.inner.ai_model_id - } - - /// AI model display name when in room context. - pub fn ai_model_name(&self) -> Option { - self.inner.ai_model_name.clone() - } - - /// Project context for the room. - pub fn project_id(&self) -> Uuid { - self.inner.project_id - } - - /// Tool registry for this request. - pub fn registry(&self) -> &ToolRegistry { - &self.inner.registry - } - - /// Mutable access to registry for adding tools. - pub fn registry_mut(&mut self) -> &mut ToolRegistry { - &mut Arc::make_mut(&mut self.inner).registry - } -} diff --git a/libs/agent/tool/definition.rs b/libs/agent/tool/definition.rs deleted file mode 100644 index 12a89f2..0000000 --- a/libs/agent/tool/definition.rs +++ /dev/null @@ -1,89 +0,0 @@ -//! Tool definition: schema, parameters, and OpenAI-compatible tool objects. - -use serde::{Deserialize, Serialize}; - -/// A JSON Schema parameter definition for a tool argument. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ToolParam { - pub name: String, - #[serde(rename = "type")] - pub param_type: String, - pub description: Option, - pub required: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub items: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ToolSchema { - #[serde(rename = "type", default)] - pub schema_type: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub properties: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub required: Option>, -} - -impl Default for ToolSchema { - fn default() -> Self { - Self { - schema_type: "object".to_string(), - properties: None, - required: None, - } - } -} - -/// A tool definition with schema and metadata. -#[derive(Debug, Clone)] -pub struct ToolDefinition { - pub name: String, - pub description: Option, - pub parameters: Option, - pub strict: bool, -} - -impl ToolDefinition { - pub fn new(name: impl Into) -> Self { - Self { - name: name.into(), - description: None, - parameters: None, - strict: false, - } - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - pub fn parameters(mut self, schema: ToolSchema) -> Self { - self.parameters = Some(schema); - self - } - - pub fn strict(mut self) -> Self { - self.strict = true; - self - } - - pub fn to_openai_tool(&self) -> serde_json::Value { - let parameters = self - .parameters - .as_ref() - .map(|s| serde_json::to_value(s).unwrap_or(serde_json::Value::Null)); - - serde_json::json!({ - "type": "function", - "function": { - "name": self.name, - "description": self.description, - "parameters": parameters.unwrap_or(serde_json::json!({})), - "strict": self.strict, - } - }) - } -} diff --git a/libs/agent/tool/executor.rs b/libs/agent/tool/executor.rs deleted file mode 100644 index 581ef50..0000000 --- a/libs/agent/tool/executor.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Executes tool calls and converts results to OpenAI `tool` messages. - -use futures::StreamExt; -use futures::stream; - -use crate::client::ChatRequestMessage; - -use super::call::{ToolCall, ToolCallResult, ToolError, ToolResult}; -use super::context::ToolContext; - -pub struct ToolExecutor { - max_tool_calls: usize, - max_depth: u32, - max_concurrency: usize, -} - -impl Default for ToolExecutor { - fn default() -> Self { - Self { - max_tool_calls: 128, - max_depth: 5, - max_concurrency: 8, - } - } -} - -impl ToolExecutor { - pub fn new() -> Self { - Self::default() - } - - pub fn with_max_tool_calls(mut self, max: usize) -> Self { - self.max_tool_calls = max; - self - } - - pub fn with_max_depth(mut self, depth: u32) -> Self { - self.max_depth = depth; - self - } - - /// Set the maximum number of tool calls executed concurrently. - /// Defaults to 8. Set to 1 for strictly sequential execution. - pub fn with_max_concurrency(mut self, n: usize) -> Self { - self.max_concurrency = n; - self - } - - /// # Errors - /// - /// Returns `ToolError::MaxToolCallsExceeded` if the total number of tool calls - /// exceeds `max_tool_calls`. - pub async fn execute_batch( - &self, - calls: Vec, - ctx: &mut ToolContext, - ) -> Result, ToolError> { - let ctx = ctx - .clone() - .with_max_tool_calls(self.max_tool_calls) - .with_max_depth(self.max_depth); - - if ctx.recursion_exceeded() { - return Err(ToolError::RecursionLimitExceeded { - max_depth: self.max_depth, - }); - } - - if let Err(current) = ctx.reserve_tool_calls(calls.len()) { - return Err(ToolError::MaxToolCallsExceeded(current)); - } - - let concurrency = self.max_concurrency; - let calls_clone: Vec = calls.clone(); - - // Execute tool calls concurrently but preserve input order for ID matching. - // buffer_unordered returns results in *completion* order, which mispairs IDs - // on concurrent errors. Instead, track each result with its original index. - let indexed_results: Vec<(usize, Result)> = - stream::iter(calls.into_iter().enumerate().map(|(i, call)| { - let child_ctx = ctx.child_context(); - async move { (i, self.execute_one(call, child_ctx).await) } - })) - .buffer_unordered(concurrency) - .collect() - .await; - - // Re-sort by original index to restore input order, then pair with original calls. - let mut result_map: std::collections::HashMap> = - indexed_results.into_iter().collect(); - - let results: Vec = calls_clone - .into_iter() - .enumerate() - .map(|(i, call)| { - let r = result_map.remove(&i).unwrap_or_else(|| { - Err(ToolError::ExecutionError( - "missing result for tool call".into(), - )) - }); - r.unwrap_or_else(|e: ToolError| ToolCallResult::error(call, e.to_string())) - }) - .collect(); - - Ok(results) - } - - async fn execute_one( - &self, - call: ToolCall, - ctx: ToolContext, - ) -> Result { - let handler = ctx - .registry() - .get(&call.name) - .ok_or_else(|| ToolError::NotFound(call.name.clone()))? - .clone(); - - let args = call.arguments_json()?; - - match handler.execute(ctx, args).await { - Ok(value) => Ok(ToolCallResult::ok(call, value)), - Err(e) => Ok(ToolCallResult::error(call, e.to_string())), - } - } - - pub fn to_tool_messages(results: &[ToolCallResult]) -> Vec { - results - .iter() - .map(|r| { - let content = match &r.result { - ToolResult::Ok(v) => { - serde_json::to_string(v).unwrap_or_else(|_| "null".to_string()) - } - ToolResult::Error(msg) => serde_json::to_string(&serde_json::json!({ - "error": msg - })) - .unwrap_or_else(|_| r#"{"error":"unknown error"}"#.to_string()), - }; - - ChatRequestMessage::tool(&r.call.id, &content) - }) - .collect() - } -} diff --git a/libs/agent/tool/mod.rs b/libs/agent/tool/mod.rs deleted file mode 100644 index 3ca8506..0000000 --- a/libs/agent/tool/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Unified function call routing for AI agents. -//! -//! Provides a type-safe, request-scoped tool registry and executor. -//! -//! # Architecture -//! -//! - [`definition`](definition) — Tool schemas: name, description, parameter JSON schema -//! - [`registry`](registry) — Request-scoped `ToolRegistry` mapping names → handlers -//! - [`call`](call) — Execution types: `ToolCall`, `ToolResult`, `ToolError` -//! - [`context`](context) — Execution context passed to each tool handler -//! - [`executor`](executor) — `ToolExecutor` coordinating lookup → execute → result -//! - [`rig_adapter`](rig_adapter) — Adapter to bridge with rig's Tool trait -//! - [`examples`](examples) — `#[tool]` macro usage guide - -pub mod call; -pub mod context; -pub mod definition; -pub mod executor; -pub mod recorder; -pub mod registry; - -#[cfg(feature = "rig")] -pub mod rig_adapter; - -pub use call::{ToolCall, ToolCallResult, ToolError, ToolResult}; -pub use context::ToolContext; -pub use definition::{ToolDefinition, ToolParam, ToolSchema}; -pub use executor::ToolExecutor; -pub use recorder::{ToolCallRecord, ToolCallRecorder}; -pub use registry::{ToolHandler, ToolRegistry}; - -#[cfg(feature = "rig")] -pub use rig_adapter::{RecordingTool, RigToolAdapter, RigToolSet, is_retryable_tool_error}; diff --git a/libs/agent/tool/recorder.rs b/libs/agent/tool/recorder.rs deleted file mode 100644 index 9eef43a..0000000 --- a/libs/agent/tool/recorder.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Batch tool call recorder — persists tool call records to `ai_tool_call` table. -//! -//! Uses an mpsc channel + background flush loop to batch-insert records, -//! reducing DB pressure from individual inserts. -//! -//! Flush triggers: -//! - Buffer reaches `BATCH_SIZE` (default 50) -//! - `FLUSH_INTERVAL` (default 5s) elapses with non-empty buffer -//! - Sender is dropped (remaining records flushed on channel close) - -use std::time::Duration; - -use db::database::AppDatabase; -use models::ai::ToolCallStatus; -use models::ai::ai_tool_call; -use sea_orm::*; -use tokio::sync::mpsc; -use uuid::Uuid; - -const FLUSH_INTERVAL: Duration = Duration::from_secs(5); -const BATCH_SIZE: usize = 50; - -/// A single tool call record to be persisted. -#[derive(Debug, Clone)] -pub struct ToolCallRecord { - pub tool_call_id: String, - pub session_id: Uuid, - pub tool_name: String, - pub caller: Uuid, - pub arguments: serde_json::Value, - pub status: ToolCallStatus, - pub execution_time_ms: Option, - pub error_message: Option, - pub error_stack: Option, - pub retry_count: i32, -} - -/// Channel-based batched recorder. Cheap to clone — all clones share the same sender. -#[derive(Clone)] -pub struct ToolCallRecorder { - tx: mpsc::UnboundedSender, - session_id: Uuid, -} - -impl ToolCallRecorder { - /// Create a new recorder with an auto-generated session ID - /// and spawn a background flush loop. - pub fn new(db: AppDatabase) -> Self { - Self::with_session(db, Uuid::new_v4()) - } - - /// Create a new recorder with a specific session ID - /// (so tool call records can be linked to an `AiSession`). - pub fn with_session(db: AppDatabase, session_id: Uuid) -> Self { - let (tx, rx) = mpsc::unbounded_channel(); - tokio::spawn(flush_loop(db, rx)); - Self { tx, session_id } - } - - /// The session ID shared by all tool calls recorded through this instance. - pub fn session_id(&self) -> Uuid { - self.session_id - } - - /// Enqueue a tool call record for batch persistence. - pub fn record(&self, record: ToolCallRecord) { - let _ = self.tx.send(record); - } -} - -async fn flush_loop(db: AppDatabase, mut rx: mpsc::UnboundedReceiver) { - let mut buffer = Vec::with_capacity(BATCH_SIZE); - let mut ticker = tokio::time::interval(FLUSH_INTERVAL); - ticker.tick().await; // skip first immediate tick - - loop { - tokio::select! { - Some(record) = rx.recv() => { - buffer.push(record); - if buffer.len() >= BATCH_SIZE { - flush(&db, &mut buffer).await; - } - } - _ = ticker.tick() => { - if !buffer.is_empty() { - flush(&db, &mut buffer).await; - } - } - else => { - // Channel closed — flush remaining and exit - if !buffer.is_empty() { - flush(&db, &mut buffer).await; - } - break; - } - } - } -} - -async fn flush(db: &AppDatabase, buffer: &mut Vec) { - let now = chrono::Utc::now(); - let models: Vec = buffer - .iter() - .map(|r| { - let status = r.status.to_string(); - ai_tool_call::ActiveModel { - tool_call_id: Set(r.tool_call_id.clone()), - session: Set(r.session_id), - tool_name: Set(r.tool_name.clone()), - caller: Set(r.caller), - arguments: Set(r.arguments.clone()), - result: Set(serde_json::Value::Null), - status: Set(status), - execution_time_ms: Set(r.execution_time_ms), - error_message: Set(r.error_message.clone()), - error_stack: Set(r.error_stack.clone()), - retry_count: Set(r.retry_count), - created_at: Set(now), - completed_at: Set(Some(now)), - updated_at: Set(now), - } - }) - .collect(); - - let count = models.len(); - if let Err(e) = ai_tool_call::Entity::insert_many(models).exec(db).await { - tracing::warn!(error = %e, count, "failed_to_flush_tool_call_records"); - } - - buffer.clear(); -} diff --git a/libs/agent/tool/registry.rs b/libs/agent/tool/registry.rs deleted file mode 100644 index 619af1e..0000000 --- a/libs/agent/tool/registry.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! Request-scoped tool registry. -//! -//! Tools are registered per-request (not globally) to keep the system testable -//! and allow different request contexts to have different tool sets. - -use std::collections::HashMap; - -use futures::FutureExt; - -use super::call::ToolError; -use super::context::ToolContext; -use super::definition::ToolDefinition; - -/// Error type for tool registry operations. -#[derive(Debug, Clone, thiserror::Error)] -pub enum ToolRegistryError { - #[error("tool already registered: {0}")] - AlreadyRegistered(String), -} - -/// Inner function pointer type for tool handlers. -type InnerHandlerFn = dyn Fn( - ToolContext, - serde_json::Value, - ) -> std::pin::Pin< - Box> + Send>, - > + Send - + Sync; - -/// Wrapper around `Arc` for `Clone` implementability. -#[derive(Clone)] -pub struct ToolHandler(std::sync::Arc); - -impl ToolHandler { - /// Creates a new handler from an async closure. - /// The closure should return `Result` (as used by git_tools), - /// which is converted to `Result`. - pub fn new(f: F) -> Self - where - F: Fn( - ToolContext, - serde_json::Value, - ) -> std::pin::Pin< - Box> + Send>, - > + Send - + Sync - + 'static, - { - Self(std::sync::Arc::new(f)) - } - - pub async fn execute( - &self, - ctx: ToolContext, - args: serde_json::Value, - ) -> Result { - (self.0)(ctx, args).await - } -} - -/// A request-scoped registry mapping tool names to their handlers. -#[derive(Clone, Default)] -pub struct ToolRegistry { - handlers: HashMap, - definitions: HashMap, -} - -impl ToolRegistry { - pub fn new() -> Self { - Self::default() - } - - pub fn register_fn(&mut self, name: impl Into, handler: F) -> &mut Self - where - F: Fn(ToolContext, serde_json::Value) -> Fut + Send + Sync + 'static, - Fut: std::future::Future> + Send + 'static, - { - let name_str = name.into(); - let def = ToolDefinition::new(&name_str); - let handler_fn: std::sync::Arc = - std::sync::Arc::new(move |ctx, args| handler(ctx, args).boxed()); - self.register(def, ToolHandler(handler_fn)); - self - } - - pub fn register(&mut self, def: ToolDefinition, handler: ToolHandler) -> &mut Self { - let name = def.name.clone(); - if self.handlers.contains_key(&name) { - tracing::warn!("tool already registered (skipping duplicate): {}", name); - return self; - } - self.handlers.insert(name.clone(), handler); - self.definitions.insert(name, def); - self - } - - /// Looks up a handler by tool name. - pub fn get(&self, name: &str) -> Option<&ToolHandler> { - self.handlers.get(name) - } - - pub fn definitions(&self) -> std::collections::hash_map::Values<'_, String, ToolDefinition> { - self.definitions.values() - } - - pub fn to_openai_tools(&self) -> Vec { - self.definitions - .values() - .map(|d| d.to_openai_tool()) - .collect() - } - - pub fn len(&self) -> usize { - self.handlers.len() - } - - pub fn is_empty(&self) -> bool { - self.handlers.is_empty() - } - - /// Merges another registry's tools into this one. - /// Skips tools with duplicate names and logs a warning. - pub fn merge(&mut self, other: ToolRegistry) { - for (name, handler) in other.handlers { - if self.handlers.contains_key(&name) { - tracing::warn!("merge skipped duplicate tool: {}", name); - continue; - } - self.handlers.insert(name, handler); - } - for (name, def) in other.definitions { - self.definitions.insert(name, def); - } - } -} diff --git a/libs/agent/tool/rig_adapter.rs b/libs/agent/tool/rig_adapter.rs deleted file mode 100644 index a27a4f9..0000000 --- a/libs/agent/tool/rig_adapter.rs +++ /dev/null @@ -1,371 +0,0 @@ -//! Adapter to bridge our ToolRegistry with rig's Tool system. -//! -//! This module provides adapters that wrap our custom ToolHandler/Registry -//! to implement rig's ToolDyn trait, enabling integration with rig's Agent. - -use std::collections::HashMap; -use std::time::{Duration, Instant}; - -use futures::FutureExt; -use rig::completion::ToolDefinition; -use rig::tool::{ToolDyn, ToolError, ToolSet}; - -use super::context::ToolContext; -use super::definition::ToolDefinition as AgentToolDefinition; -use super::recorder::{ToolCallRecord, ToolCallRecorder}; -use super::registry::{ToolHandler, ToolRegistry}; -use queue::MessageProducer; - -/// Returns true if the tool error message indicates a transient failure that can be retried. -pub fn is_retryable_tool_error(msg: &str) -> bool { - let lower = msg.to_lowercase(); - lower.contains("retry") - || lower.contains("timeout") - || lower.contains("rate limit") - || lower.contains("too many requests") - || lower.contains("unavailable") - || lower.contains("connection refused") - || lower.contains("5") - || lower.contains("try again") -} - -/// Wraps a ToolDyn with automatic retry and tool call recording. -/// -/// Used by the rig Agent path to replace the custom ReAct executor closure. -pub struct RecordingTool { - inner: Box, - db: db::database::AppDatabase, - session_id: uuid::Uuid, - caller: uuid::Uuid, -} - -impl RecordingTool { - pub fn new( - inner: Box, - db: db::database::AppDatabase, - session_id: uuid::Uuid, - caller: uuid::Uuid, - ) -> Self { - Self { - inner, - db, - session_id, - caller, - } - } -} - -impl ToolDyn for RecordingTool { - fn name(&self) -> String { - self.inner.name() - } - - fn definition<'a>( - &'a self, - prompt: String, - ) -> std::pin::Pin + Send + 'a>> { - self.inner.definition(prompt) - } - - fn call<'a>( - &'a self, - args: String, - ) -> std::pin::Pin> + Send + 'a>> - { - let inner: &'a Box = &self.inner; - let db = self.db.clone(); - let session_id = self.session_id; - let caller = self.caller; - let tool_name = inner.name(); - - Box::pin(async move { - let recorder = ToolCallRecorder::with_session(db.clone(), session_id); - let max_retries = 3u32; - let mut last_err = String::new(); - let start = Instant::now(); - - for attempt in 0..=max_retries { - let attempt_start = Instant::now(); - let attempt_args = args.clone(); - let attempt_result = inner.call(attempt_args).await; - - let elapsed_ms = attempt_start.elapsed().as_millis() as i64; - let args_json: serde_json::Value = serde_json::from_str(&args).unwrap_or_default(); - - match attempt_result { - Ok(value) => { - recorder.record(ToolCallRecord { - tool_call_id: tool_name.clone(), - session_id, - tool_name: tool_name.clone(), - caller, - arguments: args_json, - status: models::ai::ToolCallStatus::Success, - execution_time_ms: Some(elapsed_ms), - error_message: None, - error_stack: None, - retry_count: attempt as i32, - }); - return Ok(value); - } - Err(e) => { - let err_msg = e.to_string(); - if attempt < max_retries && is_retryable_tool_error(&err_msg) { - last_err = err_msg; - let backoff_ms = 100u64.saturating_mul(2u64.pow(attempt as u32)); - tokio::time::sleep(Duration::from_millis(backoff_ms)).await; - continue; - } - recorder.record(ToolCallRecord { - tool_call_id: tool_name.clone(), - session_id, - tool_name: tool_name.clone(), - caller, - arguments: args_json, - status: models::ai::ToolCallStatus::Failed, - execution_time_ms: Some(elapsed_ms), - error_message: Some(err_msg.clone()), - error_stack: None, - retry_count: attempt as i32, - }); - return Err(e); - } - } - } - - // Fallback: record failure after all retries exhausted - let elapsed_ms = start.elapsed().as_millis() as i64; - let args_json: serde_json::Value = serde_json::from_str(&args).unwrap_or_default(); - recorder.record(ToolCallRecord { - tool_call_id: tool_name.clone(), - session_id, - tool_name: tool_name.clone(), - caller, - arguments: args_json, - status: models::ai::ToolCallStatus::Failed, - execution_time_ms: Some(elapsed_ms), - error_message: Some(last_err), - error_stack: None, - retry_count: max_retries as i32, - }); - Err(ToolError::ToolCallError(Box::new(std::io::Error::new( - std::io::ErrorKind::Other, - "max retries exceeded", - )))) - }) - } -} - -/// A wrapper that converts our ToolRegistry to rig's ToolSet. -pub struct RigToolSet { - /// The rig ToolSet - inner: ToolSet, - /// Tool definitions for converting back - definitions: HashMap, -} - -impl RigToolSet { - /// Create a new RigToolSet from our ToolRegistry. - pub fn from_registry( - registry: &ToolRegistry, - db: db::database::AppDatabase, - cache: db::cache::AppCache, - config: config::AppConfig, - room_id: uuid::Uuid, - sender_id: Option, - project_id: uuid::Uuid, - message_producer: Option, - ai_model_id: Option, - ai_model_name: Option, - sent_in_turn: std::sync::Arc>>, - ) -> Self { - let mut toolset = ToolSet::default(); - let mut definitions = HashMap::new(); - - for name in registry - .definitions() - .map(|d| d.name.clone()) - .collect::>() - { - let def = registry - .definitions() - .find(|d| d.name == name) - .cloned() - .unwrap_or_else(|| AgentToolDefinition::new(&name)); - definitions.insert(name.clone(), def.clone()); - - let handler = registry.get(&name).cloned(); - if let Some(handler) = handler { - let adapter = RigToolAdapter { - handler, - definition: def, - db: db.clone(), - cache: cache.clone(), - config: config.clone(), - room_id, - sender_id, - project_id, - message_producer: message_producer.clone(), - ai_model_id, - ai_model_name: ai_model_name.clone(), - sent_in_turn: sent_in_turn.clone(), - }; - toolset.add_tool(adapter); - } - } - - Self { - inner: toolset, - definitions, - } - } - - /// Get the inner rig ToolSet - pub fn inner(&self) -> &ToolSet { - &self.inner - } - - /// Get the tool definitions - pub fn definitions(&self) -> &HashMap { - &self.definitions - } - - /// Convert to JSON tool definitions for non-rig paths - pub fn to_openai_tools(&self) -> Vec { - self.definitions - .values() - .map(|d| d.to_openai_tool()) - .collect() - } -} - -/// Adapter that wraps our ToolHandler to implement rig's ToolDyn. -pub struct RigToolAdapter { - handler: ToolHandler, - definition: AgentToolDefinition, - db: db::database::AppDatabase, - cache: db::cache::AppCache, - config: config::AppConfig, - room_id: uuid::Uuid, - sender_id: Option, - project_id: uuid::Uuid, - message_producer: Option, - ai_model_id: Option, - ai_model_name: Option, - sent_in_turn: std::sync::Arc>>, -} - -impl RigToolAdapter { - /// Create a new RigToolAdapter with all required context. - pub fn new( - handler: ToolHandler, - definition: AgentToolDefinition, - db: db::database::AppDatabase, - cache: db::cache::AppCache, - config: config::AppConfig, - room_id: uuid::Uuid, - sender_id: Option, - project_id: uuid::Uuid, - message_producer: Option, - ai_model_id: Option, - ai_model_name: Option, - sent_in_turn: std::sync::Arc>>, - ) -> Self { - Self { - handler, - definition, - db, - cache, - config, - room_id, - sender_id, - project_id, - message_producer, - ai_model_id, - ai_model_name, - sent_in_turn, - } - } -} - -impl ToolDyn for RigToolAdapter { - fn name(&self) -> String { - self.definition.name.clone() - } - - fn definition<'a>( - &'a self, - _prompt: String, - ) -> std::pin::Pin + Send + 'a>> { - let def = self.definition.clone(); - Box::pin(async move { - ToolDefinition { - name: def.name.clone(), - description: def.description.unwrap_or_default(), - parameters: def - .parameters - .as_ref() - .map(|p| serde_json::to_value(p).unwrap_or(serde_json::json!({}))) - .unwrap_or(serde_json::json!({})), - } - }) - } - - fn call<'a>( - &'a self, - args: String, - ) -> std::pin::Pin> + Send + 'a>> - { - let handler = self.handler.clone(); - let db = self.db.clone(); - let cache = self.cache.clone(); - let config = self.config.clone(); - let room_id = self.room_id; - let sender_id = self.sender_id; - let project_id = self.project_id; - let message_producer = self.message_producer.clone(); - let ai_model_id = self.ai_model_id; - let ai_model_name = self.ai_model_name.clone(); - let sent_in_turn = self.sent_in_turn.clone(); - - async move { - let mut ctx = ToolContext::new(db, cache, config, room_id, sender_id) - .with_project(project_id) - .with_sent_in_turn(sent_in_turn); - if let Some(mp) = message_producer { - ctx = ctx.with_message_producer(mp); - } - if let Some(mid) = ai_model_id { - ctx = ctx.with_ai_model(mid, ai_model_name.unwrap_or_default()); - } - - let args_json: serde_json::Value = - serde_json::from_str(&args).map_err(|e| ToolError::JsonError(e))?; - - let result = handler.execute(ctx, args_json).await; - - match result { - Ok(value) => serde_json::to_string(&value).map_err(|e| ToolError::JsonError(e)), - Err(e) => { - let error_msg = match e { - super::call::ToolError::NotFound(n) => n, - super::call::ToolError::ParseError(p) => p, - super::call::ToolError::ExecutionError(e) => e, - super::call::ToolError::RecursionLimitExceeded { max_depth } => { - format!("recursion limit exceeded (max depth: {})", max_depth) - } - super::call::ToolError::MaxToolCallsExceeded(n) => { - format!("max tool calls exceeded: {}", n) - } - super::call::ToolError::Internal(i) => i, - }; - Err(ToolError::ToolCallError(Box::new(std::io::Error::new( - std::io::ErrorKind::Other, - error_msg, - )))) - } - } - } - .boxed() - } -} diff --git a/libs/api/Cargo.toml b/libs/api/Cargo.toml deleted file mode 100644 index 5dbe663..0000000 --- a/libs/api/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "api" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true -[lib] -path = "lib.rs" -name = "api" - -[[bin]] -path = "gen_api.rs" -name = "gen_api" -[dependencies] -db = { workspace = true } -config = { workspace = true } -queue = { workspace = true } -email = { workspace = true } -tracing = { workspace = true } -service = { workspace = true } -session = { workspace = true } -agent = { workspace = true } -git = { workspace = true } -#frontend = { workspace = true } -models = { workspace = true } -room = { workspace = true } -transport = { workspace = true } -serde = { workspace = true, features = ["derive"] } -utoipa = { workspace = true, features = ["actix_extras", "chrono", "uuid", "preserve_order", "macros", "time"] } -serde_json = { workspace = true } -actix-web = { workspace = true } -uuid = { workspace = true } -anyhow = { workspace = true } -actix-cors = { workspace = true } -base64 = { workspace = true } -actix-ws = { workspace = true, features = [] } -actix = { workspace = true, features = ["macros"] } -tokio-stream = { workspace = true, features = ["sync"] } -futures = { workspace = true } -futures-util = { workspace = true } -tokio = { workspace = true, features = ["sync", "rt"] } -chrono = { workspace = true } -mime_guess2 = { workspace = true, features = ["phf-map"] } -sea-orm = "2.0.0-rc.37" -rust_decimal = "1.40.0" -actix-multipart = { workspace = true, features = ["tempfile"] } -redis = { workspace = true } -reqwest = { workspace = true, features = ["json", "native-tls", "stream"] } - -[build-dependencies] -brotli = "7" -flate2 = "1" -sha2 = "0.11" - -[lints] -workspace = true diff --git a/libs/api/agent/code_review.rs b/libs/api/agent/code_review.rs deleted file mode 100644 index 6437df7..0000000 --- a/libs/api/agent/code_review.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::agent::code_review::{TriggerCodeReviewRequest, TriggerCodeReviewResponse}; -use session::Session; - -#[utoipa::path( - post, - path = "/api/agents/code-review/{namespace}/{repo}", - request_body = TriggerCodeReviewRequest, - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, body = TriggerCodeReviewResponse, description = "AI code review triggered"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Repository or PR not found"), - ), - tag = "Agent" -)] -pub async fn trigger_code_review( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .trigger_ai_code_review( - namespace, - repo_name, - body.pr_number, - body.model_id, - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/agent/issue_triage.rs b/libs/api/agent/issue_triage.rs deleted file mode 100644 index 2cf71f2..0000000 --- a/libs/api/agent/issue_triage.rs +++ /dev/null @@ -1,48 +0,0 @@ -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -use crate::ApiResponse; - -#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)] -pub struct TriageIssueQuery { - pub issue_number: i64, -} - -#[utoipa::path( - get, - path = "/api/agents/{project}/triage", - params( - ("project" = String, Path, description = "Project name"), - ("issue_number" = i64, Query, description = "Issue number to triage"), - ), - responses( - (status = 200, description = "Issue triage result", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Issue not found"), - ), - tag = "Agent" -)] -pub async fn triage_issue( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let user_id = session.user().ok_or(crate::error::ApiError( - service::error::AppError::Unauthorized, - ))?; - let project = service - .utils_find_project_by_name(project_name.clone()) - .await?; - // Verify user has access to the project before triggering AI triage - service - .check_project_access(project.id, user_id) - .await - .map_err(|e| crate::error::ApiError(e))?; - let resp = service - .triage_issue(project_name, query.issue_number) - .await?; - Ok(crate::ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/agent/mod.rs b/libs/api/agent/mod.rs deleted file mode 100644 index 9945797..0000000 --- a/libs/api/agent/mod.rs +++ /dev/null @@ -1,126 +0,0 @@ -pub mod code_review; -pub mod issue_triage; -pub mod model; -pub mod model_capability; -pub mod model_parameter_profile; -pub mod model_pricing; -pub mod model_version; -pub mod pr_summary; -pub mod provider; - -use actix_web::web; - -pub fn init_agent_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/agents") - .route( - "/code-review/{namespace}/{repo}", - web::post().to(code_review::trigger_code_review), - ) - .route( - "/{project}/triage", - web::get().to(issue_triage::triage_issue), - ) - .route( - "/pr-description/{namespace}/{repo}", - web::post().to(pr_summary::generate_pr_description), - ) - .route("/providers", web::get().to(provider::provider_list)) - .route("/providers/{id}", web::get().to(provider::provider_get)) - .route("/providers", web::post().to(provider::provider_create)) - .route( - "/providers/{id}", - web::patch().to(provider::provider_update), - ) - .route( - "/providers/{id}", - web::delete().to(provider::provider_delete), - ) - .route("/models", web::get().to(model::model_list)) - .route("/models/catalog", web::get().to(model::model_catalog)) - .route("/models/{id}", web::get().to(model::model_get)) - .route("/models", web::post().to(model::model_create)) - .route("/models/{id}", web::patch().to(model::model_update)) - .route("/models/{id}", web::delete().to(model::model_delete)) - .route( - "/versions", - web::get().to(model_version::model_version_list), - ) - .route( - "/versions/{id}", - web::get().to(model_version::model_version_get), - ) - .route( - "/versions", - web::post().to(model_version::model_version_create), - ) - .route( - "/versions/{id}", - web::patch().to(model_version::model_version_update), - ) - .route( - "/versions/{id}", - web::delete().to(model_version::model_version_delete), - ) - .route( - "/versions/{model_version_id}/pricing", - web::get().to(model_pricing::model_pricing_list), - ) - .route( - "/pricing/{id}", - web::get().to(model_pricing::model_pricing_get), - ) - .route( - "/pricing", - web::post().to(model_pricing::model_pricing_create), - ) - .route( - "/pricing/{id}", - web::patch().to(model_pricing::model_pricing_update), - ) - .route( - "/pricing/{id}", - web::delete().to(model_pricing::model_pricing_delete), - ) - .route( - "/versions/{model_version_id}/capabilities", - web::get().to(model_capability::model_capability_list), - ) - .route( - "/capabilities/{id}", - web::get().to(model_capability::model_capability_get), - ) - .route( - "/capabilities", - web::post().to(model_capability::model_capability_create), - ) - .route( - "/capabilities/{id}", - web::patch().to(model_capability::model_capability_update), - ) - .route( - "/capabilities/{id}", - web::delete().to(model_capability::model_capability_delete), - ) - .route( - "/versions/{model_version_id}/parameters", - web::get().to(model_parameter_profile::model_parameter_profile_list), - ) - .route( - "/parameters/{id}", - web::get().to(model_parameter_profile::model_parameter_profile_get), - ) - .route( - "/parameters", - web::post().to(model_parameter_profile::model_parameter_profile_create), - ) - .route( - "/parameters/{id}", - web::patch().to(model_parameter_profile::model_parameter_profile_update), - ) - .route( - "/parameters/{id}", - web::delete().to(model_parameter_profile::model_parameter_profile_delete), - ), - ); -} diff --git a/libs/api/agent/model.rs b/libs/api/agent/model.rs deleted file mode 100644 index f099ad9..0000000 --- a/libs/api/agent/model.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::agent::model::{CreateModelRequest, UpdateModelRequest}; -use session::Session; -use uuid::Uuid; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct ListQuery { - pub provider_id: Option, -} - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct CatalogQuery { - pub provider_id: Option, - pub search: Option, - pub page: Option, - pub per_page: Option, -} - -#[utoipa::path( - get, - path = "/api/agents/models", - params(ListQuery), - responses( - (status = 200, body = Vec), - (status = 401, description = "Unauthorized"), - ), - tag = "Agent" -)] -pub async fn model_list( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let provider_id = if let Some(ref s) = query.provider_id { - Some(Uuid::parse_str(s).map_err(|_| { - service::error::AppError::BadRequest("Invalid provider UUID".to_string()) - })?) - } else { - None - }; - let resp = service.agent_model_list(provider_id, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/agents/models/catalog", - params(CatalogQuery), - responses( - (status = 200, body = service::agent::model::ModelListResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "Agent" -)] -pub async fn model_catalog( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let provider_id = if let Some(ref s) = query.provider_id { - Some(Uuid::parse_str(s).map_err(|_| { - service::error::AppError::BadRequest("Invalid provider UUID".to_string()) - })?) - } else { - None - }; - let page = query.page.unwrap_or(1).max(1); - let per_page = query.per_page.unwrap_or(20).clamp(1, 100); - let resp = service - .agent_model_list_with_pricing(provider_id, query.search.clone(), page, per_page, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/agents/models/{id}", - params(("id" = String, Path)), - responses( - (status = 200, body = service::agent::model::ModelResponse), - (status = 401, description = "Unauthorized"), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - let resp = service.agent_model_get(id, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/agents/models", - request_body = CreateModelRequest, - responses( - (status = 200, body = service::agent::model::ModelResponse), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_create( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .agent_model_create(body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/agents/models/{id}", - params(("id" = String, Path)), - request_body = UpdateModelRequest, - responses( - (status = 200, body = service::agent::model::ModelResponse), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - let resp = service - .agent_model_update(id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/agents/models/{id}", - params(("id" = String, Path)), - responses( - (status = 200), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - service.agent_model_delete(id, &session).await?; - Ok(crate::api_success()) -} diff --git a/libs/api/agent/model_capability.rs b/libs/api/agent/model_capability.rs deleted file mode 100644 index e3b3d33..0000000 --- a/libs/api/agent/model_capability.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::agent::model_capability::{ - CreateModelCapabilityRequest, UpdateModelCapabilityRequest, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/agents/versions/{model_version_id}/capabilities", - params(("model_version_id" = i64, Path)), - responses( - (status = 200, body = Vec), - (status = 401, description = "Unauthorized"), - ), - tag = "Agent" -)] -pub async fn model_capability_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let model_version_id = path.into_inner(); - let resp = service - .agent_model_capability_list(model_version_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/agents/capabilities/{id}", - params(("id" = i64, Path)), - responses( - (status = 200, body = service::agent::model_capability::ModelCapabilityResponse), - (status = 401, description = "Unauthorized"), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_capability_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = path.into_inner(); - let resp = service.agent_model_capability_get(id, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/agents/capabilities", - request_body = CreateModelCapabilityRequest, - responses( - (status = 200, body = service::agent::model_capability::ModelCapabilityResponse), - (status = 401), - (status = 403), - ), - tag = "Agent" -)] -pub async fn model_capability_create( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .agent_model_capability_create(body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/agents/capabilities/{id}", - params(("id" = i64, Path)), - request_body = UpdateModelCapabilityRequest, - responses( - (status = 200, body = service::agent::model_capability::ModelCapabilityResponse), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_capability_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let id = path.into_inner(); - let resp = service - .agent_model_capability_update(id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/agents/capabilities/{id}", - params(("id" = i64, Path)), - responses( - (status = 200), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_capability_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = path.into_inner(); - service.agent_model_capability_delete(id, &session).await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true }))) -} diff --git a/libs/api/agent/model_parameter_profile.rs b/libs/api/agent/model_parameter_profile.rs deleted file mode 100644 index 6e0271b..0000000 --- a/libs/api/agent/model_parameter_profile.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::agent::model_parameter_profile::{ - CreateModelParameterProfileRequest, UpdateModelParameterProfileRequest, -}; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/agents/versions/{model_version_id}/parameters", - params(("model_version_id" = String, Path)), - responses( - (status = 200, body = Vec), - (status = 401, description = "Unauthorized"), - ), - tag = "Agent" -)] -pub async fn model_parameter_profile_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let model_version_id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - let resp = service - .agent_model_parameter_profile_list(model_version_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/agents/parameters/{id}", - params(("id" = i64, Path)), - responses( - (status = 200, body = service::agent::model_parameter_profile::ModelParameterProfileResponse), - (status = 401, description = "Unauthorized"), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_parameter_profile_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = path.into_inner(); - let resp = service - .agent_model_parameter_profile_get(id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/agents/parameters", - request_body = CreateModelParameterProfileRequest, - responses( - (status = 200, body = service::agent::model_parameter_profile::ModelParameterProfileResponse), - (status = 401), - (status = 403), - ), - tag = "Agent" -)] -pub async fn model_parameter_profile_create( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .agent_model_parameter_profile_create(body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/agents/parameters/{id}", - params(("id" = i64, Path)), - request_body = UpdateModelParameterProfileRequest, - responses( - (status = 200, body = service::agent::model_parameter_profile::ModelParameterProfileResponse), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_parameter_profile_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let id = path.into_inner(); - let resp = service - .agent_model_parameter_profile_update(id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/agents/parameters/{id}", - params(("id" = i64, Path)), - responses( - (status = 200), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_parameter_profile_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = path.into_inner(); - service - .agent_model_parameter_profile_delete(id, &session) - .await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true }))) -} diff --git a/libs/api/agent/model_pricing.rs b/libs/api/agent/model_pricing.rs deleted file mode 100644 index a58260f..0000000 --- a/libs/api/agent/model_pricing.rs +++ /dev/null @@ -1,120 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::agent::model_pricing::{CreateModelPricingRequest, UpdateModelPricingRequest}; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/agents/versions/{model_version_id}/pricing", - params(("model_version_id" = String, Path)), - responses( - (status = 200, body = Vec), - (status = 401, description = "Unauthorized"), - ), - tag = "Agent" -)] -pub async fn model_pricing_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let model_version_id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - let resp = service - .agent_model_pricing_list(model_version_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/agents/pricing/{id}", - params(("id" = i64, Path)), - responses( - (status = 200, body = service::agent::model_pricing::ModelPricingResponse), - (status = 401, description = "Unauthorized"), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_pricing_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = path.into_inner(); - let resp = service.agent_model_pricing_get(id, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/agents/pricing", - request_body = CreateModelPricingRequest, - responses( - (status = 200, body = service::agent::model_pricing::ModelPricingResponse), - (status = 401), - (status = 403), - ), - tag = "Agent" -)] -pub async fn model_pricing_create( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .agent_model_pricing_create(body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/agents/pricing/{id}", - params(("id" = i64, Path)), - request_body = UpdateModelPricingRequest, - responses( - (status = 200, body = service::agent::model_pricing::ModelPricingResponse), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_pricing_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let id = path.into_inner(); - let resp = service - .agent_model_pricing_update(id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/agents/pricing/{id}", - params(("id" = i64, Path)), - responses( - (status = 200), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_pricing_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = path.into_inner(); - service.agent_model_pricing_delete(id, &session).await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true }))) -} diff --git a/libs/api/agent/model_version.rs b/libs/api/agent/model_version.rs deleted file mode 100644 index de78f26..0000000 --- a/libs/api/agent/model_version.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::agent::model_version::{CreateModelVersionRequest, UpdateModelVersionRequest}; -use session::Session; -use uuid::Uuid; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct ListQuery { - pub model_id: Option, -} - -#[utoipa::path( - get, - path = "/api/agents/versions", - params(ListQuery), - responses( - (status = 200, body = Vec), - (status = 401, description = "Unauthorized"), - ), - tag = "Agent" -)] -pub async fn model_version_list( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let model_id = - if let Some(ref s) = query.model_id { - Some(Uuid::parse_str(s).map_err(|_| { - service::error::AppError::BadRequest("Invalid model UUID".to_string()) - })?) - } else { - None - }; - let resp = service.agent_model_version_list(model_id, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/agents/versions/{id}", - params(("id" = String, Path)), - responses( - (status = 200, body = service::agent::model_version::ModelVersionResponse), - (status = 401, description = "Unauthorized"), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_version_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - let resp = service.agent_model_version_get(id, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/agents/versions", - request_body = CreateModelVersionRequest, - responses( - (status = 200, body = service::agent::model_version::ModelVersionResponse), - (status = 401), - (status = 403), - ), - tag = "Agent" -)] -pub async fn model_version_create( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .agent_model_version_create(body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/agents/versions/{id}", - params(("id" = String, Path)), - request_body = UpdateModelVersionRequest, - responses( - (status = 200, body = service::agent::model_version::ModelVersionResponse), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_version_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - let resp = service - .agent_model_version_update(id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/agents/versions/{id}", - params(("id" = String, Path)), - responses( - (status = 200), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn model_version_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - service.agent_model_version_delete(id, &session).await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true }))) -} diff --git a/libs/api/agent/pr_summary.rs b/libs/api/agent/pr_summary.rs deleted file mode 100644 index ef19890..0000000 --- a/libs/api/agent/pr_summary.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::agent::pr_summary::{GeneratePrDescriptionRequest, GeneratePrDescriptionResponse}; -use session::Session; - -#[utoipa::path( - post, - path = "/api/agents/pr-description/{namespace}/{repo}", - request_body = GeneratePrDescriptionRequest, - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, body = GeneratePrDescriptionResponse, description = "AI-generated PR description"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Repository or PR not found"), - ), - tag = "Agent" -)] -pub async fn generate_pr_description( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .generate_pr_description(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/agent/provider.rs b/libs/api/agent/provider.rs deleted file mode 100644 index 0dc1b56..0000000 --- a/libs/api/agent/provider.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::agent::provider::{CreateProviderRequest, UpdateProviderRequest}; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/agents/providers", - responses( - (status = 200, body = Vec), - (status = 401, description = "Unauthorized"), - ), - tag = "Agent" -)] -pub async fn provider_list( - service: web::Data, - session: Session, -) -> Result { - let resp = service.agent_provider_list(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/agents/providers/{id}", - params(("id" = String, Path, description = "Provider UUID")), - responses( - (status = 200, body = service::agent::provider::ProviderResponse), - (status = 401, description = "Unauthorized"), - (status = 404), - ), - tag = "Agent" -)] -pub async fn provider_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - let resp = service.agent_provider_get(id, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/agents/providers", - request_body = CreateProviderRequest, - responses( - (status = 200, body = service::agent::provider::ProviderResponse), - (status = 401), - (status = 403), - ), - tag = "Agent" -)] -pub async fn provider_create( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .agent_provider_create(body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/agents/providers/{id}", - params(("id" = String, Path)), - request_body = UpdateProviderRequest, - responses( - (status = 200, body = service::agent::provider::ProviderResponse), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn provider_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - let resp = service - .agent_provider_update(id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/agents/providers/{id}", - params(("id" = String, Path)), - responses( - (status = 200), - (status = 401), - (status = 403), - (status = 404), - ), - tag = "Agent" -)] -pub async fn provider_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = Uuid::parse_str(&path.into_inner()) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - service.agent_provider_delete(id, &session).await?; - Ok(crate::api_success()) -} diff --git a/libs/api/auth/captcha.rs b/libs/api/auth/captcha.rs deleted file mode 100644 index 494568c..0000000 --- a/libs/api/auth/captcha.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::ApiResponse; -use crate::error::ApiError; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::auth::captcha::{CaptchaQuery, CaptchaResponse}; -use session::Session; - -#[utoipa::path( - post, - path = "/api/auth/captcha", - request_body = CaptchaQuery, - responses( - (status = 200, description = "Captcha generated", body = ApiResponse), - (status = 500, description = "Internal server error", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_auth_captcha( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service.auth_captcha(&session, body.into_inner()).await?; - Ok(HttpResponse::Ok().json(ApiResponse::ok(resp))) -} diff --git a/libs/api/auth/email.rs b/libs/api/auth/email.rs deleted file mode 100644 index 907191d..0000000 --- a/libs/api/auth/email.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::auth::email::{EmailChangeRequest, EmailResponse, EmailVerifyRequest}; -use session::Session; - -#[utoipa::path( - post, - path = "/api/auth/email", - responses( - (status = 200, description = "Current email address", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "Auth" -)] -pub async fn api_email_get( - service: web::Data, - session: Session, -) -> Result { - let email = service.auth_get_email(&session).await?; - Ok(ApiResponse::ok(email).to_response()) -} - -#[utoipa::path( - post, - path = "/api/auth/email/change", - request_body = EmailChangeRequest, - responses( - (status = 200, description = "Verification email sent", body = ApiResponse), - (status = 401, description = "Unauthorized or invalid password"), - (status = 409, description = "Email already in use"), - ), - tag = "Auth" -)] -pub async fn api_email_change( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - service - .auth_email_change_request(&session, body.into_inner()) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/auth/email/verify", - request_body = EmailVerifyRequest, - responses( - (status = 200, description = "Email updated successfully", body = ApiResponse), - (status = 400, description = "Invalid or expired token"), - ), - tag = "Auth" -)] -pub async fn api_email_verify( - service: web::Data, - body: web::Json, -) -> Result { - service.auth_email_verify(body.into_inner()).await?; - Ok(crate::api_success()) -} diff --git a/libs/api/auth/login.rs b/libs/api/auth/login.rs deleted file mode 100644 index 076639d..0000000 --- a/libs/api/auth/login.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::ApiResponse; -use crate::error::ApiError; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::auth::login::LoginParams; -use session::Session; - -#[utoipa::path( - post, - path = "/api/auth/login", - request_body = LoginParams, - responses( - (status = 200, description = "Login successful", body = ApiResponse), - (status = 401, description = "Invalid credentials", body = ApiResponse), - (status = 428, description = "Two-factor authentication required", body = ApiResponse), - (status = 400, description = "Bad request", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_auth_login( - service: web::Data, - session: Session, - params: web::Json, -) -> Result { - service.auth_login(params.into_inner(), session).await?; - Ok(crate::api_success()) -} diff --git a/libs/api/auth/logout.rs b/libs/api/auth/logout.rs deleted file mode 100644 index a57698b..0000000 --- a/libs/api/auth/logout.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::ApiResponse; -use crate::error::ApiError; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - post, - path = "/api/auth/logout", - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Logout successful", body = ApiResponse), - (status = 500, description = "Internal server error", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_auth_logout( - service: web::Data, - session: Session, -) -> Result { - service.auth_logout(&session).await?; - Ok(crate::api_success()) -} diff --git a/libs/api/auth/me.rs b/libs/api/auth/me.rs deleted file mode 100644 index de504a6..0000000 --- a/libs/api/auth/me.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::auth::me::ContextMe; -use session::Session; - -#[utoipa::path( - post, - path = "/api/auth/me", - responses( - (status = 200, description = "Current user info", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 500, description = "Internal server error", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_auth_me( - service: web::Data, - session: Session, -) -> Result { - let me = service.auth_me(session).await?; - Ok(ApiResponse::ok(me).to_response()) -} diff --git a/libs/api/auth/mod.rs b/libs/api/auth/mod.rs deleted file mode 100644 index a4fc8d0..0000000 --- a/libs/api/auth/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -pub mod captcha; -pub mod email; -pub mod login; -pub mod logout; -pub mod me; -pub mod password; -pub mod register; -pub mod totp; -pub mod ws_token; - -pub fn init_auth_routes(cfg: &mut actix_web::web::ServiceConfig) { - cfg.service( - actix_web::web::scope("/auth") - .route("/login", actix_web::web::post().to(login::api_auth_login)) - .route( - "/register", - actix_web::web::post().to(register::api_auth_register), - ) - .route( - "/logout", - actix_web::web::post().to(logout::api_auth_logout), - ) - .route( - "/captcha", - actix_web::web::post().to(captcha::api_auth_captcha), - ) - .route("/me", actix_web::web::post().to(me::api_auth_me)) - .route( - "/password/change", - actix_web::web::post().to(password::api_user_change_password), - ) - .route( - "/password/reset", - actix_web::web::post().to(password::api_user_request_password_reset), - ) - .route( - "/password/confirm", - actix_web::web::post().to(password::api_user_confirm_password_reset), - ) - .route( - "/2fa/enable", - actix_web::web::post().to(totp::api_2fa_enable), - ) - .route( - "/2fa/verify", - actix_web::web::post().to(totp::api_2fa_verify), - ) - .route( - "/2fa/disable", - actix_web::web::post().to(totp::api_2fa_disable), - ) - .route( - "/2fa/status", - actix_web::web::post().to(totp::api_2fa_status), - ) - .route("/email", actix_web::web::post().to(email::api_email_get)) - .route( - "/email/change", - actix_web::web::post().to(email::api_email_change), - ) - .route( - "/email/verify", - actix_web::web::post().to(email::api_email_verify), - ), - ); - - // WebSocket token endpoint - cfg.route( - "/ws/token", - actix_web::web::post().to(ws_token::ws_token_generate), - ); -} diff --git a/libs/api/auth/password.rs b/libs/api/auth/password.rs deleted file mode 100644 index c179097..0000000 --- a/libs/api/auth/password.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::ApiResponse; -use crate::error::ApiError; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::auth::password::{ - ChangePasswordParams, ConfirmResetPasswordParams, ResetPasswordParams, -}; -use session::Session; - -#[utoipa::path( - post, - path = "/api/auth/password/change", - request_body = ChangePasswordParams, - responses( - (status = 200, description = "Password changed successfully", body = ApiResponse), - (status = 401, description = "Unauthorized or invalid password", body = ApiResponse), - (status = 400, description = "Bad request", body = ApiResponse), - (status = 500, description = "Internal server error", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_user_change_password( - service: web::Data, - session: Session, - params: web::Json, -) -> Result { - service - .auth_change_password(&session, params.into_inner()) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/auth/password/reset", - request_body = ResetPasswordParams, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Password reset email sent if account exists", body = ApiResponse), - (status = 500, description = "Internal server error", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_user_request_password_reset( - service: web::Data, - _session: Session, - params: web::Json, -) -> Result { - service - .auth_request_password_reset(params.into_inner()) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/auth/password/confirm", - request_body = ConfirmResetPasswordParams, - responses( - (status = 200, description = "Password reset confirmed", body = ApiResponse), - (status = 400, description = "Invalid or expired token", body = ApiResponse), - (status = 404, description = "User not found", body = ApiResponse), - (status = 500, description = "Internal server error", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_user_confirm_password_reset( - service: web::Data, - _session: Session, - params: web::Json, -) -> Result { - service - .auth_confirm_password_reset(params.into_inner()) - .await?; - Ok(crate::api_success()) -} diff --git a/libs/api/auth/register.rs b/libs/api/auth/register.rs deleted file mode 100644 index f835d82..0000000 --- a/libs/api/auth/register.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use models::DateTime; -use serde::Serialize; -use service::AppService; -use service::auth::register::RegisterParams; -use session::Session; -use utoipa::ToSchema; - -#[derive(Serialize, ToSchema)] -pub struct RegisterResponse { - uid: String, - username: String, - display_name: Option, - avatar_url: Option, - #[schema(ignore)] - created_at: DateTime, -} - -#[utoipa::path( - post, - path = "/api/auth/register", - request_body = RegisterParams, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Registration successful", body = ApiResponse), - (status = 400, description = "Bad request", body = ApiResponse), - (status = 409, description = "Username or email already exists", body = ApiResponse), - (status = 500, description = "Internal server error", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_auth_register( - service: web::Data, - session: Session, - params: web::Json, -) -> Result { - let user = service.auth_register(params.into_inner(), &session).await?; - let payload = RegisterResponse { - uid: user.uid.to_string(), - username: user.username, - display_name: user.display_name, - avatar_url: user.avatar_url, - created_at: user.created_at.naive_utc(), - }; - Ok(ApiResponse::ok(payload).to_response()) -} diff --git a/libs/api/auth/totp.rs b/libs/api/auth/totp.rs deleted file mode 100644 index ed66d2b..0000000 --- a/libs/api/auth/totp.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::auth::totp::{ - Disable2FAParams, Enable2FAResponse, Get2FAStatusResponse, Verify2FAParams, -}; -use session::Session; - -#[utoipa::path( - post, - path = "/api/auth/2fa/enable", - responses( - (status = 200, description = "2FA setup initiated", body = Enable2FAResponse), - (status = 401, description = "Unauthorized"), - (status = 409, description = "2FA already enabled"), - (status = 500, description = "Internal server error"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_2fa_enable( - service: web::Data, - session: Session, -) -> Result { - let resp = service.auth_2fa_enable(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/auth/2fa/verify", - request_body = Verify2FAParams, - responses( - (status = 200, description = "2FA verified and enabled"), - (status = 401, description = "Unauthorized or invalid code"), - (status = 400, description = "2FA not set up"), - (status = 500, description = "Internal server error"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_2fa_verify( - service: web::Data, - session: Session, - params: web::Json, -) -> Result { - service - .auth_2fa_verify_and_enable(&session, params.into_inner()) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/auth/2fa/disable", - request_body = Disable2FAParams, - responses( - (status = 200, description = "2FA disabled"), - (status = 401, description = "Unauthorized"), - (status = 400, description = "2FA not enabled or invalid code/password"), - (status = 500, description = "Internal server error"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_2fa_disable( - service: web::Data, - session: Session, - params: web::Json, -) -> Result { - service - .auth_2fa_disable(&session, params.into_inner()) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/auth/2fa/status", - responses( - (status = 200, description = "2FA status", body = Get2FAStatusResponse), - (status = 401, description = "Unauthorized"), - (status = 500, description = "Internal server error"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Auth" -)] -pub async fn api_2fa_status( - service: web::Data, - session: Session, -) -> Result { - let resp = service.auth_2fa_status(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/auth/ws_token.rs b/libs/api/auth/ws_token.rs deleted file mode 100644 index c98b91e..0000000 --- a/libs/api/auth/ws_token.rs +++ /dev/null @@ -1,52 +0,0 @@ -use actix_web::{HttpResponse, Result, web}; -use serde::Serialize; -use session::Session; -use utoipa::ToSchema; - -use crate::ApiResponse; -use crate::error::ApiError; -use service::AppService; -use service::error::AppError; -use service::ws_token::WS_TOKEN_TTL_SECONDS; - -#[derive(Debug, Serialize, ToSchema)] -pub struct WsTokenResponse { - pub token: String, - pub expires_in_seconds: i64, -} - -/// Returns a short-lived token that can be used to authenticate WebSocket connections -/// by passing it as a query parameter: `ws://host/ws?token=xxx` -#[utoipa::path( - post, - path = "/api/ws/token", - responses( - (status = 200, description = "Token generated successfully", body = ApiResponse), - (status = 401, description = "Unauthorized - not logged in", body = ApiResponse), - ), - tag = "WebSocket" -)] -pub async fn ws_token_generate( - service: web::Data, - session: Session, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(AppError::Unauthorized))?; - - let device_id = session.get::("device_id").unwrap_or_default(); - let client_id = session.get::("client_id").unwrap_or_default(); - - let token = service - .ws_token - .generate_token(user_id, device_id, client_id) - .await - .map_err(ApiError::from)?; - - let response = WsTokenResponse { - token, - expires_in_seconds: WS_TOKEN_TTL_SECONDS, - }; - - Ok(ApiResponse::ok(response).to_response()) -} diff --git a/libs/api/build.rs b/libs/api/build.rs deleted file mode 100644 index e2331c5..0000000 --- a/libs/api/build.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! Build script: reads all files from `dist/`, compresses them (brotli + gzip), -//! computes etags via SHA-256, and generates a `frontend` Rust module for `dist.rs`. - -use std::collections::BTreeMap; -use std::env; -use std::fs; -use std::io::Write; -use std::path::{Path, PathBuf}; - -use flate2::Compression; -use flate2::write::GzEncoder; -use sha2::{Digest, Sha256}; - -// ── Compression helpers ────────────────────────────────────────────────── - -fn gzip_compress(data: &[u8]) -> Vec { - let mut encoder = GzEncoder::new(Vec::new(), Compression::new(6)); - encoder.write_all(data).unwrap(); - encoder.finish().unwrap() -} - -fn brotli_compress(data: &[u8]) -> Option> { - use brotli::CompressorWriter; - let buf = Vec::new(); - let mut writer = CompressorWriter::new(buf, 4096, 6, 16); - if writer.write_all(data).is_ok() && writer.flush().is_ok() { - Some(writer.into_inner()) - } else { - None - } -} - -// ── ETag computation ───────────────────────────────────────────────────── - -fn compute_etag(data: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(data); - let hash = hasher.finalize(); - // First 32 hex chars for a compact etag - hash.iter() - .map(|b| format!("{:02x}", b)) - .take(16) - .collect::() -} - -// ── Asset collection ───────────────────────────────────────────────────── - -struct Asset { - data: Vec, - etag: String, - brotli: Option>, - gzip: Vec, -} - -fn collect_assets(dist_dir: &Path) -> BTreeMap { - let mut assets = BTreeMap::new(); - - for entry in walkdir(dist_dir) { - let rel = entry.strip_prefix(dist_dir).unwrap(); - let path_str = rel.to_string_lossy().replace('\\', "/"); - if path_str.is_empty() { - continue; - } - - let data = fs::read(&entry) - .unwrap_or_else(|e| panic!("Failed to read dist file {}: {}", path_str, e)); - - let etag = compute_etag(&data); - let brotli_data = brotli_compress(&data); - let gzip_data = gzip_compress(&data); - - assets.insert( - path_str.clone(), - Asset { - data, - etag, - brotli: brotli_data, - gzip: gzip_data, - }, - ); - } - - assets -} - -fn walkdir(dir: &Path) -> Vec { - let mut files = Vec::new(); - if let Ok(entries) = fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - files.extend(walkdir(&path)); - } else { - files.push(path); - } - } - } - files -} - -// ── Code generation ────────────────────────────────────────────────────── - -fn rust_byte_literal(data: &[u8]) -> String { - if data.len() < 200 { - let bytes: Vec = data.iter().map(|b| b.to_string()).collect(); - format!("[{}]", bytes.join(", ")) - } else { - let lines: Vec = data - .chunks(80) - .map(|chunk| { - chunk - .iter() - .map(|b| b.to_string()) - .collect::>() - .join(", ") - }) - .collect(); - format!("[\n{}\n]", lines.join(",\n")) - } -} - -fn path_to_ident(path: &str) -> String { - let s = path - .replace('-', "_") - .replace('.', "_") - .replace('/', "_") - .to_uppercase(); - format!("ASSET_{s}") -} - -fn etag_ident(path: &str) -> String { - format!("ETAG_{}", path_to_ident(path)) -} - -fn br_ident(path: &str) -> String { - format!("{}_BR", path_to_ident(path)) -} - -fn gz_ident(path: &str) -> String { - format!("{}_GZ", path_to_ident(path)) -} - -fn generate_frontend_module(assets: &BTreeMap, out_dir: &Path) { - let mut code = String::new(); - - code += "// AUTO-GENERATED by build.rs — DO NOT EDIT.\n"; - code += "// Frontend assets from dist/ embedded as static byte arrays.\n\n"; - - // Generate static byte arrays for each asset - for (path, asset) in assets { - let ident = path_to_ident(path); - let etag_id = etag_ident(path); - let br_id = br_ident(path); - let gz_id = gz_ident(path); - - code += &format!( - "static {}: &[u8] = &{};\n", - ident, - rust_byte_literal(&asset.data) - ); - code += &format!("static {}: &str = \"{}\";\n", etag_id, asset.etag); - if let Some(ref br) = asset.brotli { - code += &format!("static {}: &[u8] = &{};\n", br_id, rust_byte_literal(br)); - } - code += &format!( - "static {}: &[u8] = &{};\n", - gz_id, - rust_byte_literal(&asset.gzip) - ); - code += "\n"; - } - - // Uncompressed lookup - code += "/// Get an uncompressed frontend asset by path, returning (data, etag).\n"; - code += "pub fn get_frontend_asset_with_etag(path: &str) -> Option<(&'static [u8], &'static str)> {\n"; - code += " match path {\n"; - for (path, _asset) in assets { - let ident = path_to_ident(path); - let etag_id = etag_ident(path); - code += &format!(" \"{path}\" => Some((&{ident}, &{etag_id})),\n"); - } - code += " _ => None,\n"; - code += " }\n"; - code += "}\n\n"; - - // Compressed lookup (prefers brotli, falls back to gzip) - code += "/// Get a pre-compressed frontend asset by path.\n"; - code += "/// Returns (data, encoding, etag) — prefers brotli over gzip.\n"; - code += "pub fn get_frontend_asset_compressed(path: &str) -> Option<(&'static [u8], &'static str, &'static str)> {\n"; - code += " match path {\n"; - for (path, asset) in assets { - let etag_id = etag_ident(path); - if asset.brotli.is_some() { - let br_id = br_ident(path); - code += &format!(" \"{path}\" => Some((&{br_id}, \"br\", &{etag_id})),\n"); - } else { - let gz_id = gz_ident(path); - code += &format!(" \"{path}\" => Some((&{gz_id}, \"gzip\", &{etag_id})),\n"); - } - } - code += " _ => None,\n"; - code += " }\n"; - code += "}\n"; - - let out_path = out_dir.join("frontend.rs"); - fs::write(&out_path, code) - .unwrap_or_else(|e| panic!("Failed to write generated frontend.rs: {}", e)); -} - -fn main() { - let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let workspace_root = Path::new(&manifest_dir).parent().unwrap().parent().unwrap(); - let dist_dir = workspace_root.join("dist"); - - if !dist_dir.exists() { - println!("cargo:warning=dist/ directory not found — frontend assets will not be embedded"); - let out_dir = env::var("OUT_DIR").unwrap(); - let out_path = Path::new(&out_dir).join("frontend.rs"); - fs::write( - &out_path, - "//! No dist/ directory found — frontend assets not embedded.\n\ - pub fn get_frontend_asset_with_etag(_path: &str) -> Option<(&'static [u8], &'static str)> { None }\n\ - pub fn get_frontend_asset_compressed(_path: &str) -> Option<(&'static [u8], &'static str, &'static str)> { None }\n", - ).unwrap(); - return; - } - - println!("cargo:rerun-if-changed=dist/"); - - let assets = collect_assets(&dist_dir); - println!( - "cargo:warning=Collected {} frontend assets from dist/", - assets.len() - ); - - let out_dir = env::var("OUT_DIR").unwrap(); - generate_frontend_module(&assets, Path::new(&out_dir)); -} diff --git a/libs/api/chat/handlers/conversation.rs b/libs/api/chat/handlers/conversation.rs deleted file mode 100644 index c5990d4..0000000 --- a/libs/api/chat/handlers/conversation.rs +++ /dev/null @@ -1,181 +0,0 @@ -use actix_web::{HttpResponse, Result, web}; -use service::error::AppError; -use session::Session; -use uuid::Uuid; - -use crate::ApiResponse; -use crate::error::ApiError; - -use super::types::{ConversationListQuery, ConversationResponse, CreateConversationParams}; - -fn get_user_id(session: &Session) -> Result { - session - .user() - .ok_or_else(|| ApiError::from(AppError::Unauthorized)) -} - -#[utoipa::path( - post, - path = "/api/ai/conversations", - operation_id = "ai_conversation_create", - request_body = CreateConversationParams, - responses( - (status = 200, description = "Conversation created", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "AI Chat" -)] -pub async fn conversation_create( - service: web::Data, - session: Session, - params: web::Json, -) -> Result { - let user_id = get_user_id(&session)?; - let model = params.model.clone().unwrap_or_else(|| "gpt-4".to_string()); - - let conversation = service - .create_conversation( - user_id, - params.project_id, - params.title.clone(), - model, - params.model_config.clone(), - params.access_visibility.clone(), - params.can_ask.clone(), - params.model_uid, - params.model_name.clone(), - ) - .await?; - - let resp = ConversationResponse::from(conversation); - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations", - operation_id = "ai_conversation_list", - params( - ("project_id" = Option, Query, description = "Filter by project"), - ("q" = Option, Query, description = "Search query (title)"), - ), - responses( - (status = 200, description = "List of conversations", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - ), - tag = "AI Chat" -)] -pub async fn conversation_list( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let user_id = get_user_id(&session)?; - - let convs = service - .list_conversations(user_id, query.project_id, 50, query.q.clone()) - .await?; - - let resp: Vec = - convs.into_iter().map(ConversationResponse::from).collect(); - - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations/{conversation_id}", - operation_id = "ai_conversation_get", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ), - responses( - (status = 200, description = "Get conversation", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn conversation_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let user_id = get_user_id(&session)?; - let conversation_id = path.into_inner(); - - let c = service - .find_conversation_owned(conversation_id, user_id) - .await?; - - let resp = ConversationResponse::from(c); - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/ai/conversations/{conversation_id}", - operation_id = "ai_conversation_update", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ), - request_body = super::types::UpdateConversationParams, - responses( - (status = 200, description = "Conversation updated"), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn conversation_update( - service: web::Data, - session: Session, - path: web::Path, - params: web::Json, -) -> Result { - let user_id = get_user_id(&session)?; - let conversation_id = path.into_inner(); - - service - .update_conversation( - conversation_id, - user_id, - params.title.clone(), - params.model.clone(), - params.model_config.clone(), - params.status.clone(), - params.access_visibility.clone(), - params.can_ask.clone(), - params.model_uid, - params.model_name.clone(), - ) - .await?; - - Ok(crate::api_success()) -} - -#[utoipa::path( - delete, - path = "/api/ai/conversations/{conversation_id}", - operation_id = "ai_conversation_delete", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ), - responses( - (status = 200, description = "Conversation deleted"), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn conversation_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let user_id = get_user_id(&session)?; - let conversation_id = path.into_inner(); - - service - .delete_conversation(conversation_id, user_id) - .await?; - - Ok(crate::api_success()) -} diff --git a/libs/api/chat/handlers/fork.rs b/libs/api/chat/handlers/fork.rs deleted file mode 100644 index d05a863..0000000 --- a/libs/api/chat/handlers/fork.rs +++ /dev/null @@ -1,90 +0,0 @@ -use actix_web::{HttpResponse, Result, web}; -use service::error::AppError; -use session::Session; -use uuid::Uuid; - -use crate::ApiResponse; -use crate::error::ApiError; - -#[derive(Debug, serde::Serialize, utoipa::ToSchema)] -pub struct ForkConversationResponse { - pub id: Uuid, - pub title: Option, - pub model: String, - pub created_at: chrono::DateTime, -} - -/// Fork a conversation from a specific message, creating a new conversation -/// with all messages up to and including the source message. -#[utoipa::path( - post, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}/fork", - operation_id = "ai_conversation_fork", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Source message ID to fork from"), - ), - responses( - (status = 200, description = "Conversation forked", body = ApiResponse), - ), - tag = "AI Chat" -)] -pub async fn message_fork( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(AppError::Unauthorized))?; - let (conversation_id, source_message_id) = path.into_inner(); - - let new_conv = service - .fork_conversation_from_message(user_id, conversation_id, source_message_id) - .await?; - - let resp = ForkConversationResponse { - id: new_conv.id, - title: new_conv.title, - model: new_conv.model, - created_at: new_conv.created_at, - }; - - Ok(ApiResponse::ok(resp).to_response()) -} - -#[derive(Debug, serde::Serialize, utoipa::ToSchema)] -pub struct ForkListResponse { - pub forks: Vec, -} - -/// List all forks created from a specific message. -pub async fn message_forks( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(AppError::Unauthorized))?; - let (conversation_id, source_message_id) = path.into_inner(); - - let forks = service - .list_forks(conversation_id, user_id, source_message_id) - .await?; - - let fork_responses: Vec = forks - .into_iter() - .map(|f| ForkConversationResponse { - id: f.fork_message_id, - title: None, - model: String::new(), - created_at: f.created_at, - }) - .collect(); - - Ok(ApiResponse::ok(ForkListResponse { - forks: fork_responses, - }) - .to_response()) -} diff --git a/libs/api/chat/handlers/message.rs b/libs/api/chat/handlers/message.rs deleted file mode 100644 index f8ccad2..0000000 --- a/libs/api/chat/handlers/message.rs +++ /dev/null @@ -1,350 +0,0 @@ -use crate::ApiResponse; -use crate::error::ApiError; -use actix_web::{HttpResponse, Result, web}; -use models::ai::AiMessage; -use sea_orm::EntityTrait; -use service::error::AppError; -use session::Session; -use uuid::Uuid; - -use super::types::{CreateMessageParams, EditMessageParams, MessageListQuery, MessageResponse}; - -fn get_user_id(session: &Session) -> Result { - session - .user() - .ok_or_else(|| ApiError::from(AppError::Unauthorized)) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations/{conversation_id}/messages", - operation_id = "ai_message_list", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("limit" = Option, Query, description = "Max messages"), - ), - responses( - (status = 200, description = "List messages", body = ApiResponse>), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn message_list( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let user_id = get_user_id(&session)?; - let conversation_id = path.into_inner(); - - let limit = query.limit.unwrap_or(50) as u64; - let msgs = service - .list_messages(conversation_id, user_id, limit) - .await?; - - let resp: Vec = msgs.into_iter().map(MessageResponse::from).collect(); - - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/ai/conversations/{conversation_id}/messages", - operation_id = "ai_message_create", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ), - request_body = CreateMessageParams, - responses( - (status = 200, description = "Message created", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn message_create( - service: web::Data, - session: Session, - path: web::Path, - params: web::Json, -) -> Result { - let user_id = get_user_id(&session)?; - let conversation_id = path.into_inner(); - - let msg = service - .create_message( - conversation_id, - user_id, - params.parent_message_id, - "user".to_string(), - params.content.content.clone(), - params.model.clone(), - params.is_fork_origin.unwrap_or(false), - params.metadata.clone(), - params.room_id, - ) - .await?; - - let resp = MessageResponse::from(msg); - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}", - operation_id = "ai_message_get", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Message ID"), - ), - responses( - (status = 200, description = "Get message", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn message_get( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = get_user_id(&session)?; - let (conversation_id, message_id) = path.into_inner(); - - let msg = service - .get_message(conversation_id, user_id, message_id) - .await?; - - let resp = MessageResponse::from(msg); - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}/stop", - operation_id = "ai_message_stop", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Message ID"), - ), - responses( - (status = 200, description = "Message stopped"), - ), - tag = "AI Chat" -)] -pub async fn message_stop( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = get_user_id(&session)?; - let (conversation_id, message_id) = path.into_inner(); - - service - .stop_message(conversation_id, user_id, message_id) - .await?; - - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}/resend", - operation_id = "ai_message_resend", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Message ID"), - ), - responses( - (status = 200, description = "Resend message", body = ApiResponse), - ), - tag = "AI Chat" -)] -pub async fn message_resend( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = get_user_id(&session)?; - let (conversation_id, message_id) = path.into_inner(); - - let new_msg = service - .resend_message(conversation_id, user_id, message_id) - .await?; - - let resp = MessageResponse::from(new_msg); - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}/children", - operation_id = "ai_message_children", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Parent message ID"), - ), - responses( - (status = 200, description = "List child messages", body = ApiResponse>), - ), - tag = "AI Chat" -)] -pub async fn message_children( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = get_user_id(&session)?; - let (conversation_id, parent_message_id) = path.into_inner(); - - let msgs = service - .list_child_messages(conversation_id, user_id, parent_message_id) - .await?; - - let resp: Vec = msgs.into_iter().map(MessageResponse::from).collect(); - - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}/stream", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Message ID"), - ), - responses( - (status = 200, description = "SSE stream"), - ), - tag = "AI Chat" -)] -pub async fn message_stream( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = get_user_id(&session)?; - let (conversation_id, message_id) = path.into_inner(); - - // Streaming triggers AI execution and billing, so view-only access is not enough. - let conv = service - .find_conversation_full_access(conversation_id, user_id) - .await?; - - let model = conv.model; - - let msg = AiMessage::find_by_id(message_id) - .one(service.db.reader()) - .await - .map_err(AppError::from)? - .ok_or_else(|| ApiError::from(AppError::NotFound("message".into())))?; - if msg.conversation_id != conversation_id || msg.role != "user" || !msg.is_latest { - return Err(ApiError::from(AppError::NotFound("message".into()))); - } - - let response = actix_web::HttpResponse::Ok() - .content_type("text/event-stream") - .insert_header(("Cache-Control", "no-cache")) - .insert_header(("X-Accel-Buffering", "no")) - .streaming(super::super::stream::create_chat_sse_stream( - service.get_ref().clone(), - conversation_id, - message_id, - model, - user_id, - )); - - Ok(response.into()) -} - -#[utoipa::path( - post, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}/edit", - operation_id = "ai_message_edit", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Message ID to edit"), - ), - request_body = EditMessageParams, - responses( - (status = 200, description = "Message edited, new version created", body = ApiResponse), - ), - tag = "AI Chat" -)] -pub async fn message_edit( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, - params: web::Json, -) -> Result { - let user_id = get_user_id(&session)?; - let (conversation_id, message_id) = path.into_inner(); - - let new_msg = service - .edit_message(conversation_id, user_id, message_id, params.content.clone()) - .await?; - - let resp = MessageResponse::from(new_msg); - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}/versions", - operation_id = "ai_message_versions", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Message ID"), - ), - responses( - (status = 200, description = "List message versions", body = ApiResponse>), - ), - tag = "AI Chat" -)] -pub async fn message_versions( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = get_user_id(&session)?; - let (conversation_id, message_id) = path.into_inner(); - - let versions = service - .list_message_versions(conversation_id, user_id, message_id) - .await?; - - let resp: Vec = versions.into_iter().map(MessageResponse::from).collect(); - - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/ai/conversations/{conversation_id}/messages/{message_id}/switch-version", - operation_id = "ai_message_switch_version", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("message_id" = Uuid, Path, description = "Message ID"), - ), - request_body = super::types::SwitchVersionParams, - responses( - (status = 200, description = "Version switched", body = ApiResponse), - ), - tag = "AI Chat" -)] -pub async fn message_switch_version( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, - params: web::Json, -) -> Result { - let user_id = get_user_id(&session)?; - let (conversation_id, message_id) = path.into_inner(); - - let msg = service - .switch_message_version(conversation_id, user_id, message_id, params.version_number) - .await?; - - let resp = MessageResponse::from(msg); - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/chat/handlers/mod.rs b/libs/api/chat/handlers/mod.rs deleted file mode 100644 index 84854fd..0000000 --- a/libs/api/chat/handlers/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod conversation; -pub mod fork; -pub mod message; -pub mod share; -pub mod types; diff --git a/libs/api/chat/handlers/share.rs b/libs/api/chat/handlers/share.rs deleted file mode 100644 index 19a40df..0000000 --- a/libs/api/chat/handlers/share.rs +++ /dev/null @@ -1,76 +0,0 @@ -use actix_web::{HttpResponse, Result, web}; -use service::error::AppError; -use session::Session; -use uuid::Uuid; - -use crate::ApiResponse; -use crate::error::ApiError; - -use super::types::{ConversationResponse, ShareResponse}; - -fn get_user_id(session: &Session) -> Result { - session - .user() - .ok_or_else(|| ApiError::from(AppError::Unauthorized)) -} - -#[utoipa::path( - post, - path = "/api/ai/conversations/{conversation_id}/share", - operation_id = "ai_conversation_share", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ), - responses( - (status = 200, description = "Share token created", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn conversation_share( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let user_id = get_user_id(&session)?; - let conversation_id = path.into_inner(); - - let (share, share_token) = service.share_conversation(conversation_id, user_id).await?; - - let resp = ShareResponse { - id: share.id, - share_token, - view_count: share.view_count, - expires_at: share.expires_at, - }; - - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations/{conversation_id}/share/{share_token}", - operation_id = "ai_shared_conversation_get", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("share_token" = String, Path, description = "Share token"), - ), - responses( - (status = 200, description = "Get shared conversation", body = ApiResponse), - (status = 404, description = "Not found or expired"), - ), - tag = "AI Chat" -)] -pub async fn shared_conversation_get( - service: web::Data, - path: web::Path<(Uuid, String)>, -) -> Result { - let (conversation_id, share_token) = path.into_inner(); - - let c = service - .get_shared_conversation(conversation_id, share_token) - .await?; - - let resp = ConversationResponse::from(c); - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/chat/handlers/types.rs b/libs/api/chat/handlers/types.rs deleted file mode 100644 index 1c38478..0000000 --- a/libs/api/chat/handlers/types.rs +++ /dev/null @@ -1,176 +0,0 @@ -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Deserialize, utoipa::ToSchema)] -pub struct CreateConversationParams { - pub project_id: Option, - pub title: Option, - pub model: Option, - pub model_config: Option, - pub access_visibility: Option, - pub can_ask: Option, - /// AI model UUID for model selection - pub model_uid: Option, - /// AI model display name - pub model_name: Option, -} - -#[derive(Debug, Serialize, utoipa::ToSchema)] -pub struct ConversationResponse { - pub id: Uuid, - pub user_id: Uuid, - pub project_id: Option, - pub scope: String, - pub title: Option, - pub model: String, - pub model_config: Option, - pub status: String, - pub root_message_id: Option, - pub fork_count: i32, - pub is_shared: bool, - pub message_count: i32, - pub token_usage_total: Option, - pub access_visibility: String, - pub can_ask: String, - pub project_uid: Option, - pub model_uid: Option, - pub model_name: Option, - #[schema(value_type = chrono::DateTime)] - pub created_at: chrono::DateTime, - #[schema(value_type = chrono::DateTime)] - pub updated_at: chrono::DateTime, -} - -#[derive(Debug, Deserialize, utoipa::ToSchema)] -pub struct UpdateConversationParams { - pub title: Option, - pub model: Option, - pub model_config: Option, - pub status: Option, - pub access_visibility: Option, - pub can_ask: Option, - pub model_uid: Option, - pub model_name: Option, -} - -#[derive(Debug, Deserialize)] -pub struct ConversationListQuery { - pub project_id: Option, - pub q: Option, -} - -#[derive(Debug, Deserialize, utoipa::ToSchema)] -pub struct MessageContent { - pub role: String, - pub content: serde_json::Value, -} - -#[derive(Debug, Deserialize, utoipa::ToSchema)] -pub struct CreateMessageParams { - pub parent_message_id: Option, - pub content: MessageContent, - pub model: Option, - pub is_fork_origin: Option, - pub metadata: Option, - pub room_id: Option, -} - -#[derive(Debug, Serialize, utoipa::ToSchema)] -pub struct MessageResponse { - pub id: Uuid, - pub conversation_id: Uuid, - pub parent_message_id: Option, - pub role: String, - pub content: serde_json::Value, - pub model: Option, - pub is_fork_origin: bool, - pub stop_reason: Option, - pub input_tokens: Option, - pub output_tokens: Option, - pub latency_ms: Option, - pub metadata: Option, - pub room_id: Option, - pub version_group_id: Option, - pub version_number: i32, - pub is_latest: bool, - #[schema(value_type = chrono::DateTime)] - pub created_at: chrono::DateTime, -} - -#[derive(Debug, Deserialize)] -pub struct MessageListQuery { - pub limit: Option, -} - -#[derive(Debug, Deserialize, utoipa::ToSchema)] -pub struct EditMessageParams { - pub content: String, -} - -#[derive(Debug, Deserialize, utoipa::ToSchema)] -pub struct SwitchVersionParams { - pub version_number: i32, -} - -#[derive(Debug, Deserialize)] -pub struct ForkParams {} - -#[derive(Debug, Serialize, utoipa::ToSchema)] -pub struct ShareResponse { - pub id: Uuid, - pub share_token: String, - pub view_count: i32, - #[schema(value_type = Option>)] - pub expires_at: Option>, -} - -impl From for ConversationResponse { - fn from(c: models::ai::ai_conversation::Model) -> Self { - Self { - id: c.id, - user_id: c.user_id, - project_id: c.project_id, - scope: c.scope, - title: c.title, - model: c.model, - model_config: c.model_config, - status: c.status, - root_message_id: c.root_message_id, - fork_count: c.fork_count, - is_shared: c.is_shared, - message_count: c.message_count, - token_usage_total: c.token_usage_total, - access_visibility: c.access_visibility, - can_ask: c.can_ask, - project_uid: c.project_uid, - model_uid: c.model_uid, - model_name: c.model_name, - created_at: c.created_at, - updated_at: c.updated_at, - } - } -} - -impl From for MessageResponse { - fn from(m: models::ai::ai_message::Model) -> Self { - Self { - id: m.id, - conversation_id: m.conversation_id, - parent_message_id: m.parent_message_id, - role: m.role, - content: m.content, - model: m.model, - is_fork_origin: m.is_fork_origin, - stop_reason: m.stop_reason, - input_tokens: m.input_tokens, - output_tokens: m.output_tokens, - latency_ms: m.latency_ms, - metadata: m.metadata, - room_id: m.room_id, - version_group_id: m.version_group_id, - version_number: m.version_number, - is_latest: m.is_latest, - created_at: m.created_at, - } - } -} diff --git a/libs/api/chat/mod.rs b/libs/api/chat/mod.rs deleted file mode 100644 index 1279bbe..0000000 --- a/libs/api/chat/mod.rs +++ /dev/null @@ -1,106 +0,0 @@ -use actix_web::web; - -pub mod handlers; -pub mod stream; -pub mod subagent; -pub mod watch; - -pub fn init_chat_routes(cfg: &mut web::ServiceConfig) { - cfg.route( - "/ai/subagent/{conversation_id}/{children_id}/stream", - web::get().to(subagent::subagent_stream_watch), - ) - .route( - "/ai/subagent/{conversation_id}/{children_id}/stop", - web::post().to(subagent::subagent_stop), - ); - - cfg.service( - web::scope("/ai/conversations") - .route( - "", - web::post().to(handlers::conversation::conversation_create), - ) - .route("", web::get().to(handlers::conversation::conversation_list)) - .route( - "/{conversation_id}", - web::get().to(handlers::conversation::conversation_get), - ) - .route( - "/{conversation_id}", - web::patch().to(handlers::conversation::conversation_update), - ) - .route( - "/{conversation_id}", - web::delete().to(handlers::conversation::conversation_delete), - ) - .route( - "/{conversation_id}/watch", - web::get().to(watch::conversation_watch), - ) - .route( - "/subagent/{conversation_id}/{children_id}/stream", - web::get().to(subagent::subagent_stream_watch), - ) - .route( - "/subagent/{conversation_id}/{children_id}/stop", - web::post().to(subagent::subagent_stop), - ) - .route( - "/{conversation_id}/share", - web::post().to(handlers::share::conversation_share), - ) - .route( - "/{conversation_id}/share/{share_token}", - web::get().to(handlers::share::shared_conversation_get), - ) - .route( - "/{conversation_id}/messages", - web::get().to(handlers::message::message_list), - ) - .route( - "/{conversation_id}/messages", - web::post().to(handlers::message::message_create), - ) - .route( - "/{conversation_id}/messages/{message_id}", - web::get().to(handlers::message::message_get), - ) - .route( - "/{conversation_id}/messages/{message_id}/stop", - web::post().to(handlers::message::message_stop), - ) - .route( - "/{conversation_id}/messages/{message_id}/resend", - web::post().to(handlers::message::message_resend), - ) - .route( - "/{conversation_id}/messages/{message_id}/fork", - web::post().to(handlers::fork::message_fork), - ) - .route( - "/{conversation_id}/messages/{message_id}/forks", - web::get().to(handlers::fork::message_forks), - ) - .route( - "/{conversation_id}/messages/{message_id}/stream", - web::get().to(handlers::message::message_stream), - ) - .route( - "/{conversation_id}/messages/{message_id}/children", - web::get().to(handlers::message::message_children), - ) - .route( - "/{conversation_id}/messages/{message_id}/edit", - web::post().to(handlers::message::message_edit), - ) - .route( - "/{conversation_id}/messages/{message_id}/versions", - web::get().to(handlers::message::message_versions), - ) - .route( - "/{conversation_id}/messages/{message_id}/switch-version", - web::post().to(handlers::message::message_switch_version), - ), - ); -} diff --git a/libs/api/chat/stream.rs b/libs/api/chat/stream.rs deleted file mode 100644 index 3bb22eb..0000000 --- a/libs/api/chat/stream.rs +++ /dev/null @@ -1,1019 +0,0 @@ -use agent::chat::chat_execution; -use agent::chat::{AiChunkType, AiStreamChunk, normalize_thinking_content}; -use agent::client::AiClientConfig; -use agent::client::types::ChatRequestMessage; -use agent::client::{StreamChunk, StreamChunkType}; -use agent::react::PERSONAL_CONTEXT_PROMPT; -use futures::StreamExt; -use models::agents::{model, model_version}; -use models::ai::{AiMessage, ai_conversation, ai_message}; -use queue::{ChatMessageEvent, ChatStreamChunkEvent}; -use sea_orm::{ - ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set, -}; -use service::AppService; -use std::pin::Pin; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; -use tokio_stream::wrappers::ReceiverStream; -use uuid::Uuid; - -/// Create an SSE stream that executes AI chat with ReAct tool-calling. -/// -/// Also publishes chat messages and stream chunks via NATS JetStream for -/// multi-viewer support. The requesting client receives SSE events, while -/// other viewers receive chunks via NATS -> WebSocket broadcast. -pub fn create_chat_sse_stream( - service: AppService, - conversation_id: Uuid, - user_message_id: Uuid, - model_name: String, - user_id: Uuid, -) -> Pin> + Send>> { - let (tx, rx) = tokio::sync::mpsc::channel::(100); - - let cache = service.cache.clone(); - - tokio::spawn(async move { - // Check for active stream (SSE reconnect recovery) BEFORE starting a new one - // so the frontend can recover from a page refresh. - if let Some((msg_id, started_at)) = cache.get_chat_stream_active(conversation_id).await { - let _ = tx.send(format!( - "data: {{\"event\":\"recovery\",\"data\":{{\"message_id\":\"{}\",\"started_at\":{}}}}}\n\n", - msg_id, - started_at - )).await; - let _ = tx - .send("data: {\"event\":\"done\",\"data\":\"recovery\"}\n\n".to_string()) - .await; - return; - } - - let queue = service.queue_producer.clone(); - let chunk_seq = Arc::new(AtomicU64::new(0)); - - // Build messages from conversation history - let messages = match build_messages_from_history(&service, conversation_id).await { - Ok(msgs) => msgs, - Err(e) => { - let payload = serde_json::json!({"event":"error","data": e.to_string()}); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - return; - } - }; - - // Get AI config - let api_key = match service.config.ai_api_key() { - Ok(k) => k, - Err(_) => { - let _ = tx - .send( - "data: {\"event\":\"error\",\"data\":\"AI not configured\"}\n\n" - .to_string(), - ) - .await; - return; - } - }; - let base_url = match service.config.ai_basic_url() { - Ok(u) => u, - Err(_) => { - let _ = tx - .send( - "data: {\"event\":\"error\",\"data\":\"AI not configured\"}\n\n" - .to_string(), - ) - .await; - return; - } - }; - - let config = AiClientConfig::new(api_key).with_base_url(&base_url); - - // Get tools from ChatService if available - let (tools, tool_registry, embed_service) = match &service.chat_service { - Some(cs) => ( - cs.tools(), - cs.tool_registry().cloned(), - service.embed_service.as_ref().map(|es| (**es).clone()), - ), - None => (Vec::new(), None, None), - }; - - // Get project_id and scope from conversation - let (project_id, conv_project_id, is_personal) = - match service.find_conversation(conversation_id).await { - Ok(c) => { - let conv_project_id = c.project_id; - ( - conv_project_id.unwrap_or(Uuid::nil()), - conv_project_id, - conv_project_id.is_none(), - ) - } - Err(_) => { - let _ = tx - .send( - "data: {\"event\":\"error\",\"data\":\"conversation not found\"}\n\n" - .to_string(), - ) - .await; - return; - } - }; - - // In personal scope: filter out project/git/repo tools and inject personal context prompt - let tools = if is_personal { - tools - .into_iter() - .filter(|t| { - let name = t - .get("function") - .and_then(|f| f.get("name")) - .and_then(|n| n.as_str()) - .unwrap_or(""); - !name.starts_with("project_") - && !name.starts_with("git_") - && !name.starts_with("repo_") - && name != "send_message" - && name != "retract_message" - }) - .collect() - } else { - tools - }; - - // Inject personal context system prompt for non-project chats - let messages = if is_personal { - let mut msgs = messages; - msgs.insert( - 0, - ChatRequestMessage::system(PERSONAL_CONTEXT_PROMPT.to_string()), - ); - msgs - } else { - messages - }; - - let (model_record, billing_version_id) = match model::Entity::find() - .filter(model::Column::Name.eq(&model_name)) - .one(service.db.reader()) - .await - { - Ok(Some(m)) => { - let version_id = model_version::Entity::find() - .filter(model_version::Column::ModelId.eq(m.id)) - .filter(model_version::Column::Status.eq("active")) - .order_by_desc(model_version::Column::IsDefault) - .order_by_desc(model_version::Column::ReleaseDate) - .one(service.db.reader()) - .await - .ok() - .flatten() - .map(|v| v.id); - - match version_id { - Some(version_id) => (m, version_id), - None => { - let error_msg = "AI model version is not configured. Please configure an active model version before using AI."; - let payload = serde_json::json!({"event":"billing_error","data":error_msg}); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - let _ = tx - .send( - "data: {\"event\":\"done\",\"data\":\"billing_error\"}\n\n" - .to_string(), - ) - .await; - return; - } - } - } - _ => { - let error_msg = "AI model is not configured. Please sync or configure the model before using AI."; - let payload = serde_json::json!({"event":"billing_error","data":error_msg}); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - let _ = tx - .send("data: {\"event\":\"done\",\"data\":\"billing_error\"}\n\n".to_string()) - .await; - return; - } - }; - - // Pre-flight balance check: verify the selected account can afford a minimal AI call. - let balance_ok = if is_personal { - agent::billing::check_user_balance(&service.db, user_id, billing_version_id, 500, 250) - .await - } else { - agent::billing::check_balance( - &service.db, - project_id, - user_id, - billing_version_id, - 500, - 250, - ) - .await - }; - - match balance_ok { - Ok(true) => {} - Ok(false) => { - tracing::warn!(project_id = %project_id, user_id = %user_id, personal = is_personal, "Insufficient balance for chat AI call"); - - let (scope, scope_id) = if is_personal { - ("user", user_id) - } else { - ("project", project_id) - }; - let _ = agent::billing::persist_billing_error( - &service.db, scope, scope_id, "insufficient_balance", - "Insufficient balance. Your account does not have enough funds for this AI request.", - Some(serde_json::json!({ - "user_id": user_id.to_string(), - "project_id": if is_personal { None } else { Some(project_id.to_string()) }, - "model_version_id": billing_version_id.to_string(), - })), - ).await; - - let error_msg = "Insufficient balance. Your account does not have enough funds to process this AI request. Please add credits to continue."; - let payload = serde_json::json!({"event":"billing_error","data":error_msg}); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - let _ = tx - .send("data: {\"event\":\"done\",\"data\":\"billing_error\"}\n\n".to_string()) - .await; - return; - } - Err(e) => { - tracing::warn!(error = %e, "Balance check failed"); - let error_msg = format!("Billing check failed: {}", e); - let payload = serde_json::json!({"event":"billing_error","data":error_msg}); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - let _ = tx - .send("data: {\"event\":\"done\",\"data\":\"billing_error\"}\n\n".to_string()) - .await; - return; - } - } - - let max_tool_depth = 99; - let assistant_msg_id = Uuid::now_v7(); - - // Determine conversation project_id for chat message event - // Broadcast chat message start event via NATS - let chat_msg = ChatMessageEvent { - message_id: assistant_msg_id, - conversation_id, - project_id: conv_project_id, - sender_id: Uuid::nil(), - role: "assistant".to_string(), - content: String::new(), - model: Some(model_name.clone()), - input_tokens: None, - output_tokens: None, - timestamp: chrono::Utc::now(), - }; - let _ = queue.publish_chat_message(&chat_msg).await; - - // Mark stream as active in Redis so page refresh can recover - let _ = cache - .set_chat_stream_active(conversation_id, user_message_id) - .await; - - // Clear any stale cancel flag before starting - let _ = cache.clear_chat_stream_cancelled(conversation_id).await; - - // Cancellation token checked in on_chunk and by a periodic poller. - let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let cancelled_for_on_chunk = cancelled.clone(); - let recorded_chunks = Arc::new(tokio::sync::Mutex::new(Vec::::new())); - - let on_chunk_tx = tx.clone(); - let on_chunk_queue = queue.clone(); - let on_chunk_seq = chunk_seq.clone(); - let on_chunk_conv_id = conversation_id; - let on_chunk_msg_id = user_message_id; - let on_chunk_model = model_name.clone(); - let on_chunk_recorded = recorded_chunks.clone(); - - let on_chunk: agent::chat::StreamCallback = Box::new(move |chunk: AiStreamChunk| { - let tx = on_chunk_tx.clone(); - let queue = on_chunk_queue.clone(); - let seq = on_chunk_seq.fetch_add(1, Ordering::Relaxed); - let conv_id = on_chunk_conv_id; - let msg_id = on_chunk_msg_id; - let model = on_chunk_model.clone(); - let cancelled = cancelled_for_on_chunk.clone(); - let recorded = on_chunk_recorded.clone(); - Box::pin(async move { - // Check if stream has been cancelled - if cancelled.load(Ordering::Acquire) { - return; - } - - let chunk_type = chunk.chunk_type.clone(); - let event = match &chunk_type { - AiChunkType::Thinking => "thinking", - AiChunkType::Answer => "token", - AiChunkType::ToolCall => "tool_call", - AiChunkType::ToolResult => "tool_result", - }; - let content = match &chunk_type { - AiChunkType::Thinking => normalize_thinking_content(&chunk.content), - _ => chunk.content.clone(), - }; - - // Build structured data payload based on chunk type - let data_json = match &chunk_type { - AiChunkType::ToolCall | AiChunkType::ToolResult => { - // Use structured metadata if available - if let Some(meta) = chunk.metadata.clone() { - meta - } else { - // Fallback: wrap raw content as display text - serde_json::json!({"display": content}) - } - } - _ => { - // thinking / answer: send plain text content - serde_json::Value::String(content.clone()) - } - }; - let persisted_content = match &chunk_type { - AiChunkType::ToolCall | AiChunkType::ToolResult => data_json.to_string(), - _ => content.clone(), - }; - let persisted_type = match &chunk_type { - AiChunkType::Thinking => StreamChunkType::Thinking, - AiChunkType::Answer => StreamChunkType::Answer, - AiChunkType::ToolCall => StreamChunkType::ToolCall, - AiChunkType::ToolResult => StreamChunkType::ToolResult, - }; - recorded.lock().await.push(StreamChunk { - chunk_type: persisted_type, - content: persisted_content, - }); - - let mut sse_json = serde_json::json!({ - "event": event, - "data": data_json, - }); - if let Some(children_id) = chunk.children_id { - sse_json.as_object_mut().unwrap().insert( - "children_id".to_string(), - serde_json::Value::String(children_id), - ); - } - - let sse = format!( - "data: {}\n\n", - serde_json::to_string(&sse_json).unwrap_or_default() - ); - let _ = tx.send(sse).await; - - // Also broadcast via NATS for other viewers - let natts_chunk = ChatStreamChunkEvent { - conversation_id: conv_id, - message_id: msg_id, - seq, - content: chunk.content, - done: false, - error: None, - chunk_type: Some(event.to_string()), - model_name: Some(model), - }; - queue.publish_chat_chunk(&natts_chunk).await; - }) as Pin + Send>> - }); - - let cancel_wait = { - let cache_for_check = cache.clone(); - let conv_id_for_check = conversation_id; - async move { - let mut interval = tokio::time::interval(std::time::Duration::from_millis(250)); - loop { - interval.tick().await; - if cache_for_check - .is_chat_stream_cancelled(conv_id_for_check) - .await - { - break; - } - } - } - }; - - // Resolve max_tokens from model config (unlimited if not set) - let max_tokens = model_record - .max_output_tokens - .map(|v| v as u32) - .unwrap_or(u32::MAX); - - let execution = chat_execution::execute_chat_stream( - messages, - tools, - &model_name, - &config, - 0.7, // temperature - max_tokens, // max_tokens from model config - max_tool_depth, - tool_registry.as_ref(), - service.db.clone(), - service.cache.clone(), - service.config.clone(), - project_id, - Uuid::nil(), // sender_uid 閳?unknown in Chat API context - embed_service, - on_chunk, - Some(conversation_id), - Some(service.queue_producer.clone()), - ); - - let result = tokio::select! { - result = execution => Some(result), - _ = cancel_wait => { - cancelled.store(true, Ordering::Release); - None - } - }; - - // Clear stream active state and cancel flag (streaming finished) - let _ = cache.clear_chat_stream_active(conversation_id).await; - let _ = cache.clear_chat_stream_cancelled(conversation_id).await; - let was_cancelled = cancelled.load(Ordering::Acquire); - - match result { - Some(Ok(stream_result)) => { - if was_cancelled { - let partial_chunks = recorded_chunks.lock().await.clone(); - if let Some(msg) = persist_assistant_message_from_chunks( - &service, - conversation_id, - user_message_id, - assistant_msg_id, - &model_name, - &partial_chunks, - &stream_result.content, - stream_result.input_tokens, - stream_result.output_tokens, - "cancelled", - ) - .await - { - update_conversation_after_response(&service, conversation_id, &msg).await; - } - let _ = tx - .send("data: {\"event\":\"done\",\"data\":\"stopped\"}\n\n".to_string()) - .await; - return; - } - // Build ordered content blocks from stream chunks, merging - // consecutive blocks of the same role (thinking/assistant/tool_call/tool_result). - let raw_blocks: Vec<(String, String)> = stream_result - .chunks - .iter() - .filter(|c| { - matches!( - c.chunk_type, - StreamChunkType::Thinking - | StreamChunkType::Answer - | StreamChunkType::ToolCall - | StreamChunkType::ToolResult - ) - }) - .map(|chunk| { - let role = match chunk.chunk_type { - StreamChunkType::Thinking => "thinking", - StreamChunkType::ToolCall => "tool_call", - StreamChunkType::ToolResult => "tool_result", - _ => "assistant", - }; - (role.to_string(), chunk.content.clone()) - }) - .collect(); - - let merged_blocks = merge_consecutive_blocks(raw_blocks); - // Apply thinking normalization to the fully merged thinking - // blocks 閳?per-token normalization is meaningless since each - // chunk is a single token. - let normalized_blocks: Vec<(String, String)> = merged_blocks - .into_iter() - .map(|(role, content)| { - if role == "thinking" { - (role, normalize_thinking_content(&content)) - } else { - (role, content) - } - }) - .collect(); - let content_blocks: Vec = normalized_blocks - .iter() - .map(|(role, content)| serde_json::json!({ "role": role, "content": content })) - .collect(); - let content_value = if content_blocks.is_empty() { - serde_json::json!([{ "role": "assistant", "content": stream_result.content }]) - } else { - serde_json::json!(content_blocks) - }; - - // Persist assistant message - let assistant_msg = ai_message::ActiveModel { - id: Set(assistant_msg_id), - conversation_id: Set(conversation_id), - parent_message_id: Set(Some(user_message_id)), - role: Set("assistant".to_string()), - content: Set(content_value), - model: Set(Some(model_name.clone())), - is_fork_origin: Set(false), - stop_reason: Set(Some("stop".to_string())), - input_tokens: Set(Some(stream_result.input_tokens as i32)), - output_tokens: Set(Some(stream_result.output_tokens as i32)), - latency_ms: Set(None), - metadata: Set(None), - room_id: Set(None), - version_group_id: Set(Some(assistant_msg_id)), - version_number: Set(1), - is_latest: Set(true), - created_at: Set(chrono::Utc::now()), - }; - - let saved = assistant_msg.insert(service.db.writer()).await; - - if let Ok(msg) = &saved { - update_conversation_after_response(&service, conversation_id, msg).await; - - // After AI response, check/update conversation title and emit via SSE - if let Ok(Some(conv)) = ai_conversation::Entity::find_by_id(conversation_id) - .one(service.db.reader()) - .await - { - let existing_title = conv.title.clone(); - let needs_title = existing_title - .as_deref() - .map(|t| t.is_empty() || t == "New Chat") - .unwrap_or(true); - - if needs_title { - // Generate title from first user message - let first_user_msg = AiMessage::find() - .filter(ai_message::Column::ConversationId.eq(conversation_id)) - .filter(ai_message::Column::Role.eq("user")) - .order_by_asc(ai_message::Column::CreatedAt) - .one(service.db.reader()) - .await - .ok() - .flatten(); - - if let Some(user_msg) = first_user_msg { - let content = match &user_msg.content { - serde_json::Value::String(s) => s.clone(), - serde_json::Value::Array(arr) => arr - .first() - .and_then(|f| f.get("content")) - .and_then(|c| c.as_str()) - .unwrap_or("") - .to_string(), - other => other.to_string(), - }; - - // Simple title extraction: first meaningful words - let title = content - .split_whitespace() - .filter(|w| w.len() > 2) - .take(5) - .collect::>() - .join(" "); - - if !title.is_empty() { - let truncated: String = title.chars().take(40).collect(); - - // Save title to DB - let mut active: ai_conversation::ActiveModel = conv.into(); - active.title = Set(Some(truncated.clone())); - active.updated_at = Set(chrono::Utc::now()); - let _ = active.update(service.db.writer()).await; - - // Emit title via SSE - let title_payload = - serde_json::json!({"title": truncated}).to_string(); - let _ = tx - .send(format!( - "data: {{\"event\":\"title\",\"data\":{}}}\n\n", - title_payload - )) - .await; - } - } - } else if let Some(title) = &existing_title { - // Title already set (e.g. by AI tool) 閳?emit it - let title_payload = serde_json::json!({"title": title}).to_string(); - let _ = tx - .send(format!( - "data: {{\"event\":\"title\",\"data\":{}}}\n\n", - title_payload - )) - .await; - } - } - } - - // Record billing after successful AI response - let billing_result = if is_personal { - agent::billing::record_user_ai_usage( - &service.db, - user_id, - billing_version_id, - stream_result.input_tokens, - stream_result.output_tokens, - ) - .await - } else { - agent::billing::record_ai_usage( - &service.db, - project_id, - user_id, - billing_version_id, - stream_result.input_tokens, - stream_result.output_tokens, - ) - .await - }; - - let mut billing_failed = false; - match billing_result { - Ok(agent::billing::BillingResult::Success(record)) => { - tracing::info!( - cost = record.cost, - deducted_from = record.deducted_from.as_str(), - personal = is_personal, - "chat_billing_deducted" - ); - } - Ok(agent::billing::BillingResult::InsufficientBalance { message }) => { - billing_failed = true; - tracing::warn!( - project_id = %project_id, - user_id = %user_id, - personal = is_personal, - "chat_billing_insufficient_balance" - ); - let payload = serde_json::json!({"event":"billing_error","data":message}); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - } - Err(e) => { - billing_failed = true; - tracing::error!(error = %e, "chat_billing_error"); - let payload = serde_json::json!({ - "event":"billing_error", - "data": format!("Billing failed: {}", e), - }); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - } - } - - // Broadcast final chat message with token usage - let final_msg = ChatMessageEvent { - message_id: assistant_msg_id, - conversation_id, - project_id: conv_project_id, - sender_id: Uuid::nil(), - role: "assistant".to_string(), - content: stream_result.content.clone(), - model: Some(model_name.clone()), - input_tokens: Some(stream_result.input_tokens as i32), - output_tokens: Some(stream_result.output_tokens as i32), - timestamp: chrono::Utc::now(), - }; - let _ = queue.publish_chat_message(&final_msg).await; - - // Send final SSE done event - let done_data = if billing_failed { - "billing_error" - } else { - "ok" - }; - let _ = tx - .send(format!( - "data: {{\"event\":\"done\",\"data\":\"{}\"}}\n\n", - done_data - )) - .await; - } - None => { - let partial_chunks = recorded_chunks.lock().await.clone(); - if let Some(msg) = persist_assistant_message_from_chunks( - &service, - conversation_id, - user_message_id, - assistant_msg_id, - &model_name, - &partial_chunks, - "", - 0, - 0, - "cancelled", - ) - .await - { - update_conversation_after_response(&service, conversation_id, &msg).await; - let final_msg = ChatMessageEvent { - message_id: assistant_msg_id, - conversation_id, - project_id: conv_project_id, - sender_id: Uuid::nil(), - role: "assistant".to_string(), - content: assistant_plain_text(&msg.content), - model: Some(model_name.clone()), - input_tokens: msg.input_tokens, - output_tokens: msg.output_tokens, - timestamp: chrono::Utc::now(), - }; - let _ = queue.publish_chat_message(&final_msg).await; - } - let _ = tx - .send("data: {\"event\":\"done\",\"data\":\"stopped\"}\n\n".to_string()) - .await; - } - Some(Err(e)) => { - let partial_chunks = recorded_chunks.lock().await.clone(); - if let Some(msg) = persist_assistant_message_from_chunks( - &service, - conversation_id, - user_message_id, - assistant_msg_id, - &model_name, - &partial_chunks, - "", - 0, - 0, - "error", - ) - .await - { - update_conversation_after_response(&service, conversation_id, &msg).await; - let final_msg = ChatMessageEvent { - message_id: assistant_msg_id, - conversation_id, - project_id: conv_project_id, - sender_id: Uuid::nil(), - role: "assistant".to_string(), - content: assistant_plain_text(&msg.content), - model: Some(model_name.clone()), - input_tokens: msg.input_tokens, - output_tokens: msg.output_tokens, - timestamp: chrono::Utc::now(), - }; - let _ = queue.publish_chat_message(&final_msg).await; - } - let payload = serde_json::json!({"event":"error","data": e.to_string()}); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - let _ = tx - .send("data: {\"event\":\"done\",\"data\":\"error\"}\n\n".to_string()) - .await; - } - } - }); - - Box::pin(ReceiverStream::new(rx).map(|msg| Ok(actix_web::web::Bytes::from(msg)))) -} - -fn content_value_from_chunks(chunks: &[StreamChunk], fallback: &str) -> Option { - let raw_blocks: Vec<(String, String)> = chunks - .iter() - .filter(|c| { - matches!( - c.chunk_type, - StreamChunkType::Thinking - | StreamChunkType::Answer - | StreamChunkType::ToolCall - | StreamChunkType::ToolResult - ) - }) - .map(|chunk| { - let role = match chunk.chunk_type { - StreamChunkType::Thinking => "thinking", - StreamChunkType::ToolCall => "tool_call", - StreamChunkType::ToolResult => "tool_result", - _ => "assistant", - }; - (role.to_string(), chunk.content.clone()) - }) - .collect(); - - let merged_blocks = merge_consecutive_blocks(raw_blocks); - let normalized_blocks: Vec<(String, String)> = merged_blocks - .into_iter() - .map(|(role, content)| { - if role == "thinking" { - (role, normalize_thinking_content(&content)) - } else { - (role, content) - } - }) - .filter(|(_, content)| !content.is_empty()) - .collect(); - - if normalized_blocks.is_empty() && fallback.is_empty() { - return None; - } - - let content_blocks: Vec = normalized_blocks - .iter() - .map(|(role, content)| serde_json::json!({ "role": role, "content": content })) - .collect(); - Some(if content_blocks.is_empty() { - serde_json::json!([{ "role": "assistant", "content": fallback }]) - } else { - serde_json::json!(content_blocks) - }) -} - -fn assistant_plain_text(content: &serde_json::Value) -> String { - match content { - serde_json::Value::String(s) => s.clone(), - serde_json::Value::Array(arr) => arr - .iter() - .filter(|item| item.get("role").and_then(|r| r.as_str()) != Some("thinking")) - .filter_map(|item| item.get("content").and_then(|c| c.as_str())) - .collect::>() - .join("\n"), - other => other.to_string(), - } -} - -async fn persist_assistant_message_from_chunks( - service: &AppService, - conversation_id: Uuid, - user_message_id: Uuid, - assistant_msg_id: Uuid, - model_name: &str, - chunks: &[StreamChunk], - fallback: &str, - input_tokens: i64, - output_tokens: i64, - stop_reason: &str, -) -> Option { - let content = content_value_from_chunks(chunks, fallback)?; - let assistant_msg = ai_message::ActiveModel { - id: Set(assistant_msg_id), - conversation_id: Set(conversation_id), - parent_message_id: Set(Some(user_message_id)), - role: Set("assistant".to_string()), - content: Set(content), - model: Set(Some(model_name.to_string())), - is_fork_origin: Set(false), - stop_reason: Set(Some(stop_reason.to_string())), - input_tokens: Set(Some(input_tokens as i32)), - output_tokens: Set(Some(output_tokens as i32)), - latency_ms: Set(None), - metadata: Set(None), - room_id: Set(None), - version_group_id: Set(Some(assistant_msg_id)), - version_number: Set(1), - is_latest: Set(true), - created_at: Set(chrono::Utc::now()), - }; - - match assistant_msg.insert(service.db.writer()).await { - Ok(msg) => Some(msg), - Err(e) => { - tracing::warn!(error = %e, conversation_id = %conversation_id, "failed to persist partial assistant message"); - None - } - } -} - -/// Update conversation metadata after an AI assistant message is saved. -async fn update_conversation_after_response( - service: &AppService, - conversation_id: Uuid, - assistant_msg: &ai_message::Model, -) { - use models::ai::ai_conversation; - use sea_orm::EntityTrait; - - if let Ok(Some(conv)) = ai_conversation::Entity::find_by_id(conversation_id) - .one(service.db.reader()) - .await - { - let input_tokens = assistant_msg.input_tokens.unwrap_or(0) as i64; - let output_tokens = assistant_msg.output_tokens.unwrap_or(0) as i64; - let total_tokens = input_tokens + output_tokens; - - let previous_token_total = conv.token_usage_total.unwrap_or(0); - let mut active: ai_conversation::ActiveModel = conv.into(); - if let Ok(count) = AiMessage::find() - .filter(ai_message::Column::ConversationId.eq(conversation_id)) - .count(service.db.reader()) - .await - { - active.message_count = Set(count as i32); - } - active.token_usage_total = Set(Some(previous_token_total + total_tokens as i32)); - active.updated_at = Set(chrono::Utc::now()); - let _ = active.update(service.db.writer()).await; - } -} - -/// Build ChatRequestMessage list from ai_message conversation history. -async fn build_messages_from_history( - service: &AppService, - conversation_id: Uuid, -) -> Result, String> { - let conversation = service - .find_conversation(conversation_id) - .await - .map_err(|e| format!("conversation lookup error: {}", e))?; - let project_id = conversation.project_id; - - let msgs = AiMessage::find() - .filter(ai_message::Column::ConversationId.eq(conversation_id)) - .filter(ai_message::Column::IsLatest.eq(true)) - .order_by_asc(ai_message::Column::CreatedAt) - .all(service.db.reader()) - .await - .map_err(|e| format!("db error: {}", e))?; - - let mut chat_messages = Vec::new(); - - for msg in &msgs { - let role = msg.role.as_str(); - let content = match &msg.content { - serde_json::Value::String(s) => s.clone(), - serde_json::Value::Array(arr) => { - // Content is ordered blocks: [{role:"thinking",content:"..."}, {role:"assistant","content":"..."}, ...] - // For assistant messages: concatenate all "assistant" blocks - // For user/system messages: take the first block's content - if role == "assistant" { - arr.iter() - .filter(|item| { - item.get("role").and_then(|r| r.as_str()) != Some("thinking") - }) - .filter_map(|item| item.get("content").and_then(|c| c.as_str())) - .collect::>() - .join("\n") - } else if let Some(first) = arr.first() { - first - .get("content") - .and_then(|c| c.as_str()) - .unwrap_or("") - .to_string() - } else { - String::new() - } - } - other => other.to_string(), - }; - - if role == "user" { - match service - .build_message_context_prompts(project_id, msg.metadata.as_ref()) - .await - { - Ok(prompts) => { - for prompt in prompts { - chat_messages.push(ChatRequestMessage::system(prompt)); - } - } - Err(error) => { - tracing::warn!( - conversation_id = %conversation_id, - message_id = %msg.id, - error = %error, - "failed to build chat message context prompts" - ); - } - } - } - - match role { - "user" => chat_messages.push(ChatRequestMessage::user(content)), - "assistant" => chat_messages.push(ChatRequestMessage::assistant(Some(content), None)), - "system" => chat_messages.push(ChatRequestMessage::system(content)), - _ => chat_messages.push(ChatRequestMessage::user(content)), - } - } - - Ok(chat_messages) -} - -/// Merge consecutive content blocks of the same role into single blocks. -/// This transforms many small per-chunk blocks into clean interleaved segments: -/// [thinking, thinking, assistant, assistant] -> [thinking, assistant] -/// Per-token chunks are concatenated directly; the model sends \n inside -/// the token content where needed, not between tokens. -fn merge_consecutive_blocks(blocks: Vec<(String, String)>) -> Vec<(String, String)> { - let mut merged: Vec<(String, String)> = Vec::new(); - for (role, content) in blocks { - if content.is_empty() { - continue; - } - if let Some(last) = merged.last_mut() { - if last.0 == role && role != "tool_call" && role != "tool_result" { - last.1.push_str(&content); - continue; - } - } - merged.push((role, content)); - } - merged -} diff --git a/libs/api/chat/subagent.rs b/libs/api/chat/subagent.rs deleted file mode 100644 index 27198c1..0000000 --- a/libs/api/chat/subagent.rs +++ /dev/null @@ -1,376 +0,0 @@ -//! SSE endpoint for watching a specific sub-agent's stream output. -//! -//! `GET /api/ai/subagent/{conversation_id}/{children_id}/stream` -//! -//! Prefers Redis PubSub for low-latency live delivery and falls back to NATS -//! if Redis is unavailable. The subject/channel is -//! `chat.subagent.chunk.{conversation_id}.{children_id}`. - -use actix_web::{HttpResponse, Result, web}; -use futures::StreamExt; -use models::ai::ai_subagent_session; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; -use service::AppService; -use uuid::Uuid; - -use crate::error::ApiError; - -async fn find_subagent_session( - service: &AppService, - conversation_id: Uuid, - children_id: &str, -) -> Option { - ai_subagent_session::Entity::find() - .filter(ai_subagent_session::Column::ConversationId.eq(conversation_id)) - .filter(ai_subagent_session::Column::ChildrenId.eq(children_id)) - .order_by_desc(ai_subagent_session::Column::CreatedAt) - .one(service.db.reader()) - .await - .ok() - .flatten() -} - -fn terminal_event_for_status(status: &str) -> &'static str { - match status { - "stopped" | "cancelled" => "stopped", - "error" => "error", - _ => "done", - } -} - -async fn send_session_snapshot( - tx: &tokio::sync::mpsc::Sender, - session: &ai_subagent_session::Model, - include_output: bool, -) -> bool { - if include_output && !session.output.is_empty() { - let payload = serde_json::json!({ - "event": "token", - "data": { - "content": session.output, - "children_id": session.children_id, - }, - }); - if tx.send(format!("data: {}\n\n", payload)).await.is_err() { - return false; - } - } - - let event = terminal_event_for_status(&session.status); - let payload = serde_json::json!({ - "event": event, - "data": { - "content": "", - "children_id": session.children_id, - "error": session.error_message, - }, - }); - tx.send(format!("data: {}\n\n", payload)).await.is_ok() -} - -/// Create an SSE stream for watching a sub-agent's stream output from Redis PubSub. -pub fn create_subagent_sse_stream_redis( - service: AppService, - conversation_id: Uuid, - children_id: String, -) -> std::pin::Pin< - Box> + Send>, -> { - let (tx, rx) = tokio::sync::mpsc::channel::(200); - - tokio::spawn(async move { - if let Some(session) = find_subagent_session(&service, conversation_id, &children_id).await - { - let _ = send_session_snapshot(&tx, &session, true).await; - return; - } - - let redis_url = match service.config.redis_url() { - Ok(url) => url, - Err(e) => { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string(&e.to_string()).unwrap_or_default() - )) - .await; - return; - } - }; - let client = match redis::Client::open(redis_url) { - Ok(client) => client, - Err(e) => { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string(&e.to_string()).unwrap_or_default() - )) - .await; - return; - } - }; - - let pubsub = match client.get_async_pubsub().await { - Ok(pubsub) => pubsub, - Err(e) => { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string(&e.to_string()).unwrap_or_default() - )) - .await; - return; - } - }; - let (mut sink, mut stream) = pubsub.split(); - let channel = format!("chat.subagent.chunk.{}.{}", conversation_id, children_id); - if let Err(e) = sink.subscribe(channel.as_str()).await { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string(&e.to_string()).unwrap_or_default() - )) - .await; - return; - } - - let _ = tx.send(":ok\n\n".to_string()).await; - - let mut session_poll = tokio::time::interval(std::time::Duration::from_millis(500)); - let stream_started = tokio::time::Instant::now(); - let mut sent_content = false; - loop { - tokio::select! { - Some(msg) = stream.next() => { - let payload: String = match msg.get_payload() { - Ok(v) => v, - Err(e) => { - let _ = tx.send(format!("data: {{\"event\":\"error\",\"data\":{}}}\n\n", serde_json::to_string(&e.to_string()).unwrap_or_default())).await; - break; - } - }; - let event_type = if let Ok(parsed) = serde_json::from_str::(&payload) { - parsed - .get("chunk_type") - .and_then(|v| v.as_str()) - .unwrap_or("chunk") - .to_string() - } else { - "chunk".to_string() - }; - if matches!(event_type.as_str(), "token" | "thinking") { - sent_content = true; - } - let sse = format!("data: {{\"event\":\"{}\",\"data\":{}}}\n\n", event_type, payload); - if tx.send(sse).await.is_err() { - break; - } - if matches!(event_type.as_str(), "done" | "stopped" | "error") { - break; - } - } - _ = session_poll.tick() => { - if let Some(session) = find_subagent_session(&service, conversation_id, &children_id).await { - let _ = send_session_snapshot(&tx, &session, !sent_content).await; - break; - } - if stream_started.elapsed() > std::time::Duration::from_secs(90) { - let payload = serde_json::json!({ - "event": "error", - "data": { - "content": "sub-agent stream timed out waiting for terminal state", - "children_id": children_id, - }, - }); - let _ = tx.send(format!("data: {}\n\n", payload)).await; - break; - } - } - } - } - }); - - Box::pin( - tokio_stream::wrappers::ReceiverStream::new(rx).map(|s| Ok(actix_web::web::Bytes::from(s))), - ) -} - -/// Create an SSE stream for watching a sub-agent's stream output via NATS fallback. -pub fn create_subagent_sse_stream_nats( - service: AppService, - conversation_id: Uuid, - children_id: String, -) -> std::pin::Pin< - Box> + Send>, -> { - let (tx, rx) = tokio::sync::mpsc::channel::(200); - - tokio::spawn(async move { - let nats = match &service.queue_producer.nats { - Some(n) => n.clone(), - None => { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string("NATS not available").unwrap_or_default() - )) - .await; - return; - } - }; - - let subject = format!("chat.subagent.chunk.{}.{}", conversation_id, children_id); - let mut sub = match nats.subscribe(&subject).await { - Ok(s) => s, - Err(e) => { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string(&e.to_string()).unwrap_or_default() - )) - .await; - return; - } - }; - - let _ = tx.send(":ok\n\n".to_string()).await; - - loop { - match sub.next().await { - Some(msg) => { - let payload = String::from_utf8_lossy(&msg.payload); - let event_type = - if let Ok(parsed) = serde_json::from_str::(&payload) { - parsed - .get("chunk_type") - .and_then(|v| v.as_str()) - .unwrap_or("chunk") - .to_string() - } else { - "chunk".to_string() - }; - let sse = format!( - "data: {{\"event\":\"{}\",\"data\":{}}}\n\n", - event_type, payload - ); - if tx.send(sse).await.is_err() { - break; - } - } - None => break, - } - } - }); - - Box::pin( - tokio_stream::wrappers::ReceiverStream::new(rx).map(|s| Ok(actix_web::web::Bytes::from(s))), - ) -} - -/// SSE endpoint for watching a sub-agent's stream output. -/// -/// `GET /api/ai/subagent/{conversation_id}/{children_id}/stream` -#[utoipa::path( - get, - path = "/api/ai/subagent/{conversation_id}/{children_id}/stream", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("children_id" = String, Path, description = "Sub-agent children ID"), - ), - responses( - (status = 200, description = "SSE stream of sub-agent events"), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn subagent_stream_watch( - service: web::Data, - session: session::Session, - path: web::Path<(Uuid, String)>, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let (conversation_id, children_id) = path.into_inner(); - - // Verify access to the conversation - let _conv = service - .find_conversation_owned(conversation_id, user_id) - .await?; - - let redis_stream = create_subagent_sse_stream_redis( - service.get_ref().clone(), - conversation_id, - children_id.clone(), - ); - let response = HttpResponse::Ok() - .content_type("text/event-stream") - .insert_header(("Cache-Control", "no-cache")) - .insert_header(("X-Accel-Buffering", "no")) - .streaming(redis_stream); - - Ok(response.into()) -} - -/// NATS fallback retained for deployments where Redis PubSub is unavailable. -pub async fn subagent_stream_watch_nats( - service: web::Data, - session: session::Session, - path: web::Path<(Uuid, String)>, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let (conversation_id, children_id) = path.into_inner(); - - let _conv = service - .find_conversation_owned(conversation_id, user_id) - .await?; - - let response = HttpResponse::Ok() - .content_type("text/event-stream") - .insert_header(("Cache-Control", "no-cache")) - .insert_header(("X-Accel-Buffering", "no")) - .streaming(create_subagent_sse_stream_nats( - service.get_ref().clone(), - conversation_id, - children_id, - )); - - Ok(response.into()) -} - -#[utoipa::path( - post, - path = "/api/ai/subagent/{conversation_id}/{children_id}/stop", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ("children_id" = String, Path, description = "Sub-agent children ID"), - ), - responses( - (status = 200, description = "Sub-agent stop requested"), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn subagent_stop( - service: web::Data, - session: session::Session, - path: web::Path<(Uuid, String)>, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let (conversation_id, children_id) = path.into_inner(); - - let _conv = service - .find_conversation_full_access(conversation_id, user_id) - .await?; - - service - .cache - .set_sub_agent_cancelled(conversation_id, &children_id) - .await; - - Ok(crate::api_success()) -} diff --git a/libs/api/chat/watch.rs b/libs/api/chat/watch.rs deleted file mode 100644 index 0eb6077..0000000 --- a/libs/api/chat/watch.rs +++ /dev/null @@ -1,166 +0,0 @@ -//! SSE endpoint for watching a chat conversation in real-time via NATS. -//! -//! Unlike the primary SSE stream (which triggers AI execution), this endpoint -//! passively subscribes to NATS Core subjects and forwards chat messages and -//! stream chunks to connected clients. This enables multiple viewers to watch -//! the same AI conversation in real-time. - -use actix_web::{HttpResponse, Result, web}; -use futures::StreamExt; -use service::AppService; -use std::pin::Pin; -use uuid::Uuid; - -use crate::error::ApiError; - -/// SSE endpoint for watching a chat conversation. -/// -/// `GET /api/ai/conversations/{conversation_id}/watch` -/// -/// Subscribes to NATS Core subjects (`chat.chunk.{id}` and `chat.message.{id}`) -/// and forwards received events as SSE to the connected client. -/// -/// SSE events: -/// - `chunk` — a stream chunk (thinking, token, tool_call, tool_result, done, error) -/// - `message` — a complete chat message -/// - `error` — an error event -pub fn create_watch_sse_stream( - service: AppService, - conversation_id: Uuid, -) -> Pin> + Send>> { - let (tx, rx) = tokio::sync::mpsc::channel::(200); - - tokio::spawn(async move { - let nats = match &service.queue_producer.nats { - Some(n) => n.clone(), - None => { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string("NATS not available").unwrap_or_default() - )) - .await; - return; - } - }; - - // Subscribe to chat chunks - let chunk_subject = format!("chat.chunk.{}", conversation_id); - let mut chunk_sub = match nats.subscribe(&chunk_subject).await { - Ok(s) => s, - Err(e) => { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string(&e.to_string()).unwrap_or_default() - )) - .await; - return; - } - }; - - // Subscribe to chat messages - let msg_subject = format!("chat.message.{}", conversation_id); - let mut msg_sub = match nats.subscribe(&msg_subject).await { - Ok(s) => s, - Err(e) => { - let _ = tx - .send(format!( - "data: {{\"event\":\"error\",\"data\":{}}}\n\n", - serde_json::to_string(&e.to_string()).unwrap_or_default() - )) - .await; - return; - } - }; - - let _ = tx.send(":ok\n\n".to_string()).await; - - loop { - tokio::select! { - chunk_msg = chunk_sub.next() => { - match chunk_msg { - Some(msg) => { - let payload = String::from_utf8_lossy(&msg.payload); - // Parse to get chunk_type for the event field - let event_type = if let Ok(parsed) = serde_json::from_str::(&payload) { - parsed.get("chunk_type") - .and_then(|v| v.as_str()) - .unwrap_or("chunk") - .to_string() - } else { - "chunk".to_string() - }; - let sse = format!( - "data: {{\"event\":\"{}\",\"data\":{}}}\n\n", - event_type, payload - ); - if tx.send(sse).await.is_err() { - break; - } - } - None => break, - } - } - msg = msg_sub.next() => { - match msg { - Some(msg) => { - let payload = String::from_utf8_lossy(&msg.payload); - let sse = format!( - "data: {{\"event\":\"message\",\"data\":{}}}\n\n", - payload - ); - if tx.send(sse).await.is_err() { - break; - } - } - None => break, - } - } - } - } - }); - - Box::pin( - tokio_stream::wrappers::ReceiverStream::new(rx).map(|s| Ok(actix_web::web::Bytes::from(s))), - ) -} - -#[utoipa::path( - get, - path = "/api/ai/conversations/{conversation_id}/watch", - params( - ("conversation_id" = Uuid, Path, description = "Conversation ID"), - ), - responses( - (status = 200, description = "SSE stream of conversation events"), - (status = 404, description = "Not found"), - ), - tag = "AI Chat" -)] -pub async fn conversation_watch( - service: web::Data, - session: session::Session, - path: web::Path, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let conversation_id = path.into_inner(); - - // Verify access (view-only is sufficient) - let _conv = service - .find_conversation_owned(conversation_id, user_id) - .await?; - - let response = HttpResponse::Ok() - .content_type("text/event-stream") - .insert_header(("Cache-Control", "no-cache")) - .insert_header(("X-Accel-Buffering", "no")) - .streaming(create_watch_sse_stream( - service.get_ref().clone(), - conversation_id, - )); - - Ok(response.into()) -} diff --git a/libs/api/dist.rs b/libs/api/dist.rs deleted file mode 100644 index 306effe..0000000 --- a/libs/api/dist.rs +++ /dev/null @@ -1,178 +0,0 @@ -use actix_web::{HttpRequest, HttpResponse, http::header, web}; -use mime_guess2::MimeGuess; - -fn cache_control_header(path: &str) -> &'static str { - if path == "index.html" { - "no-cache, no-store, must-revalidate" - } else if path.ends_with(".js") - || path.ends_with(".css") - || path.ends_with(".woff2") - || path.ends_with(".woff") - || path.ends_with(".ttf") - || path.ends_with(".otf") - || path.ends_with(".png") - || path.ends_with(".jpg") - || path.ends_with(".jpeg") - || path.ends_with(".gif") - || path.ends_with(".svg") - || path.ends_with(".ico") - || path.ends_with(".webp") - || path.ends_with(".avif") - || path.ends_with(".map") - { - "public, max-age=31536000, immutable" - } else { - "no-cache" - } -} - -/// Returns true if the client explicitly accepts the given encoding via Accept-Encoding header. -/// "*" is treated as accepting all encodings. -fn accepts_encoding(req: &HttpRequest, target: &str) -> bool { - let header_val = match req.headers().get(header::ACCEPT_ENCODING) { - Some(v) => v.to_str().unwrap_or(""), - None => return false, - }; - for part in header_val.split(',') { - let part = part.trim(); - // Strip quality value if present - let enc = part.split(';').next().unwrap_or(part).trim(); - if enc.eq_ignore_ascii_case(target) || enc == "*" { - return true; - } - } - false -} - -/// Determines the Content-Type for a path, with explicit fallback for common frontend extensions. -fn content_type_for_path(path: &str) -> String { - if let Some(ext) = path.rsplit('.').next() { - match ext { - "html" | "htm" => return "text/html; charset=utf-8".into(), - "js" | "mjs" => return "application/javascript; charset=utf-8".into(), - "jsx" | "ts" | "tsx" => return "text/plain; charset=utf-8".into(), - "css" => return "text/css; charset=utf-8".into(), - "json" => return "application/json; charset=utf-8".into(), - "svg" => return "image/svg+xml".into(), - "png" => return "image/png".into(), - "jpg" | "jpeg" => return "image/jpeg".into(), - "gif" => return "image/gif".into(), - "webp" => return "image/webp".into(), - "ico" => return "image/x-icon".into(), - "woff" => return "font/woff".into(), - "woff2" => return "font/woff2".into(), - "ttf" => return "font/ttf".into(), - "otf" => return "font/otf".into(), - "txt" => return "text/plain; charset=utf-8".into(), - "xml" => return "application/xml; charset=utf-8".into(), - _ => {} - } - } - MimeGuess::from_path(path) - .first_or_octet_stream() - .to_string() -} - -/// Build an HttpResponse for the given asset. -/// Sets Content-Type, Cache-Control, ETag, and Content-Encoding (only if non-empty). -fn build_asset_response( - req: &HttpRequest, - data: &[u8], - etag: &str, - path: &str, - cc: &str, - content_encoding: &str, -) -> HttpResponse { - // 304 Not Modified when client has the same ETag - if let Some(if_none_match) = req.headers().get(header::IF_NONE_MATCH) { - if let Ok(client_etag) = if_none_match.to_str() { - if client_etag == etag { - let mut resp = HttpResponse::NotModified(); - resp.insert_header(("Cache-Control", cc)); - resp.insert_header(("ETag", etag)); - if !content_encoding.is_empty() { - resp.insert_header(("Content-Encoding", content_encoding)); - } - return resp.finish(); - } - } - } - - let mime = content_type_for_path(path); - let mut resp = HttpResponse::Ok(); - resp.content_type(mime); - resp.insert_header(("Cache-Control", cc)); - resp.insert_header(("ETag", etag)); - if !content_encoding.is_empty() { - resp.insert_header(("Content-Encoding", content_encoding)); - } - resp.body(data.to_vec()) -} - -pub async fn serve_frontend(req: HttpRequest, path: web::Path) -> HttpResponse { - let path = path.into_inner(); - let path_str = if path.is_empty() || path == "/" { - "index.html" - } else { - path.as_str() - }; - let cc = cache_control_header(path_str); - - // Try brotli/gzip compressed variant first (best compression), - // then fall back to uncompressed if client doesn't accept the encoding. - let compressed = crate::frontend::get_frontend_asset_compressed(path_str); - let uncompressed = crate::frontend::get_frontend_asset_with_etag(path_str); - - let (data, encoding, etag, content_path) = if let Some((c_data, c_enc, c_etag)) = compressed { - if accepts_encoding(&req, c_enc) { - (c_data, c_enc, c_etag, path_str) - } else if let Some((u_data, u_etag)) = uncompressed { - // Client doesn't accept the pre-compressed encoding — serve uncompressed. - (u_data, "", u_etag, path_str) - } else { - // No uncompressed fallback — still serve compressed (client must handle it). - (c_data, c_enc, c_etag, path_str) - } - } else if let Some((data, etag)) = uncompressed { - (data, "", etag, path_str) - } else { - // Path not found — try index.html as SPA fallback. - match crate::frontend::get_frontend_asset_with_etag("index.html") { - Some((data, etag)) => (data, "", etag, "index.html"), - None => return HttpResponse::NotFound().finish(), - } - }; - - if !encoding.is_empty() && accepts_encoding(&req, &encoding) { - build_asset_response(&req, data, etag, content_path, cc, &encoding) - } else { - build_asset_response(&req, data, etag, content_path, cc, "") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_content_type_for_html() { - assert_eq!( - content_type_for_path("index.html"), - "text/html; charset=utf-8" - ); - } - - #[test] - fn test_content_type_for_hashed_js() { - assert_eq!( - content_type_for_path("index-De8T6ILu.js"), - "application/javascript; charset=utf-8" - ); - } - - #[test] - fn test_content_type_for_extensionless() { - let ct = content_type_for_path("auth/login"); - assert!(!ct.is_empty()); - } -} diff --git a/libs/api/error.rs b/libs/api/error.rs deleted file mode 100644 index ac716d9..0000000 --- a/libs/api/error.rs +++ /dev/null @@ -1,113 +0,0 @@ -use actix_web::{HttpResponse, ResponseError}; -use serde::Serialize; -use service::error::AppError; -use utoipa::openapi::schema::{KnownFormat, ObjectBuilder, SchemaFormat, Type}; -use utoipa::openapi::{RefOr, Schema}; -use utoipa::{PartialSchema, ToSchema}; - -#[derive(Debug, Serialize, ToSchema)] -pub struct ApiResponse { - pub code: i32, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, -} - -impl ApiResponse { - pub fn ok(data: T) -> Self { - Self { - code: 0, - message: "ok".to_string(), - data: Some(data), - } - } - - pub fn to_response(self) -> HttpResponse { - HttpResponse::Ok().json(self) - } -} - -pub fn api_success() -> HttpResponse { - HttpResponse::Ok().json(ApiResponse { - code: 0, - message: "ok".to_string(), - data: None::<()>, - }) -} - -#[derive(Debug, Serialize, ToSchema)] -pub struct ApiErrorResponse { - pub code: i32, - pub error: String, - pub message: String, -} - -#[derive(Debug)] -pub struct ApiError(pub AppError); - -impl std::fmt::Display for ApiError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.user_message().fmt(f) - } -} - -impl std::error::Error for ApiError {} - -impl From for ApiError { - fn from(e: AppError) -> Self { - ApiError(e) - } -} - -impl From for ApiError { - fn from(e: room::RoomError) -> Self { - ApiError(e.into()) - } -} - -impl ResponseError for ApiError { - fn error_response(&self) -> HttpResponse { - let err = &self.0; - let status = actix_web::http::StatusCode::from_u16(err.http_status_code()) - .unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR); - let resp = ApiErrorResponse { - code: err.code(), - error: err.slug().to_string(), - message: err.user_message(), - }; - HttpResponse::build(status).json(resp) - } -} - -impl PartialSchema for ApiError { - fn schema() -> RefOr { - RefOr::T(Schema::Object( - ObjectBuilder::new() - .property( - "code", - ObjectBuilder::new() - .schema_type(Type::Integer) - .format(Some(SchemaFormat::KnownFormat(KnownFormat::Int32))) - .description(Some("Error numeric code")), - ) - .property( - "error", - ObjectBuilder::new() - .schema_type(Type::String) - .description(Some("Error slug identifier")), - ) - .property( - "message", - ObjectBuilder::new() - .schema_type(Type::String) - .description(Some("Human-readable error message")), - ) - .required("code") - .required("error") - .required("message") - .into(), - )) - } -} - -impl ToSchema for ApiError {} diff --git a/libs/api/frontend.rs b/libs/api/frontend.rs deleted file mode 100644 index 9712d7a..0000000 --- a/libs/api/frontend.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Frontend assets module — auto-generated by build.rs. -//! The actual content is generated at $OUT_DIR/frontend.rs by the build script. - -include!(concat!(env!("OUT_DIR"), "/frontend.rs")); diff --git a/libs/api/gen_api.rs b/libs/api/gen_api.rs deleted file mode 100644 index 15e00d9..0000000 --- a/libs/api/gen_api.rs +++ /dev/null @@ -1,11 +0,0 @@ -use utoipa::OpenApi; - -fn main() { - let out = api::openapi::OpenApiDoc::openapi().to_pretty_json(); - if let Ok(out) = out { - std::fs::write("openapi.json", out).unwrap(); - } else { - panic!("Failed to generate openapi.json"); - } - std::process::exit(0); -} diff --git a/libs/api/git/archive.rs b/libs/api/git/archive.rs deleted file mode 100644 index 4fd736c..0000000 --- a/libs/api/git/archive.rs +++ /dev/null @@ -1,195 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::archive::{ - ArchiveCachedResponse, ArchiveInvalidateAllResponse, ArchiveInvalidateResponse, - ArchiveListResponse, ArchiveQuery, ArchiveResponse, ArchiveSummaryResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/archive", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("commit_oid" = String, Query), - ("format" = String, Query), - ("prefix" = Option, Query), - ("max_depth" = Option, Query), - ("path_filter" = Option, Query), - ), - responses( - (status = 200, description = "Get archive", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_archive( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_archive(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/archive/list", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("commit_oid" = String, Query), - ("format" = String, Query), - ("prefix" = Option, Query), - ("max_depth" = Option, Query), - ("path_filter" = Option, Query), - ), - responses( - (status = 200, description = "List archive entries", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_archive_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_archive_list(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/archive/summary", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("commit_oid" = String, Query), - ("format" = String, Query), - ("prefix" = Option, Query), - ("max_depth" = Option, Query), - ("path_filter" = Option, Query), - ), - responses( - (status = 200, description = "Get archive summary", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_archive_summary( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_archive_summary(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/archive/cached", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("commit_oid" = String, Query), - ("format" = String, Query), - ("prefix" = Option, Query), - ("max_depth" = Option, Query), - ("path_filter" = Option, Query), - ), - responses( - (status = 200, description = "Check if archive is cached", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_archive_cached( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_archive_cached(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/archive/invalidate", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("commit_oid" = String, Query), - ("format" = String, Query), - ("prefix" = Option, Query), - ("max_depth" = Option, Query), - ("path_filter" = Option, Query), - ), - responses( - (status = 200, description = "Invalidate archive cache", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_archive_invalidate( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_archive_invalidate(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/archive/invalidate/{commit_oid}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("commit_oid" = String, Path), - ), - responses( - (status = 200, description = "Invalidate all archive caches for commit", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_archive_invalidate_all( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, commit_oid) = path.into_inner(); - let resp = service - .git_archive_invalidate_all(namespace, repo_name, commit_oid, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/blame.rs b/libs/api/git/blame.rs deleted file mode 100644 index 111ccb4..0000000 --- a/libs/api/git/blame.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::blame::BlameQuery; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/blame/{commit_oid}/{tail}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("commit_oid" = String, Path), - ("tail" = String, Path, description = "File path within the repository"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, body = Vec), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_blame_file( - service: web::Data, - session: Session, - path: web::Path<(String, String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, commit_oid, file_path) = path.into_inner(); - let mut req = query.into_inner(); - req.commit_oid = commit_oid; - req.path = file_path; - let resp = service - .git_blame_file(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/blob.rs b/libs/api/git/blob.rs deleted file mode 100644 index 83471f1..0000000 --- a/libs/api/git/blob.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::blob::{ - BlobContentResponse, BlobCreateRequest, BlobCreateResponse, BlobExistsResponse, BlobGetQuery, - BlobInfoResponse, BlobIsBinaryResponse, BlobSizeResponse, GitReadmeQuery, GitReadmeResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/readme", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("ref" = Option, Query, description = "Git reference (branch, tag, commit). Defaults to the repository's default branch."), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get README content", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_readme( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_readme(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/blob/{oid}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Blob object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get blob info", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_blob_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_blob_get(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/blob/{oid}/exists", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Blob object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check blob exists", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_blob_exists( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_blob_exists(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/blob/{oid}/is-binary", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Blob object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if blob is binary", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_blob_is_binary( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_blob_is_binary(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/blob/{oid}/content", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Blob object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get blob content", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_blob_content( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_blob_content(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/blob/{oid}/size", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Blob object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get blob size", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_blob_size( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_blob_size(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/blob", - request_body = BlobCreateRequest, - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Create blob", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_blob_create( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_blob_create(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/branch.rs b/libs/api/git/branch.rs deleted file mode 100644 index 4f9b6e4..0000000 --- a/libs/api/git/branch.rs +++ /dev/null @@ -1,579 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::branch::{ - BranchCreateRequest, BranchDiffQuery, BranchDiffResponse, BranchExistsResponse, - BranchFastForwardResponse, BranchInfoResponse, BranchIsAncestorQuery, BranchIsAncestorResponse, - BranchIsConflictedResponse, BranchIsDetachedResponse, BranchIsHeadResponse, - BranchIsMergedQuery, BranchIsMergedResponse, BranchListQuery, BranchMergeBaseQuery, - BranchMergeBaseResponse, BranchMoveRequest, BranchRenameRequest, BranchSetUpstreamRequest, - BranchSummaryResponse, BranchTrackingDiffResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List branches", body = ApiResponse>), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_list(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/summary", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get branch summary", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_summary( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_summary(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/{name}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Branch name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get branch", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_branch_get(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/current", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get current branch", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_current( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_current(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/{name}/exists", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Branch name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check branch exists", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_exists( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_branch_exists(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/{name}/is-head", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Branch name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if branch is HEAD", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_is_head( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_branch_is_head(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/is-detached", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if HEAD is detached", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_is_detached( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_is_detached(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/{name}/upstream", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Branch name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get upstream branch", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_upstream( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_branch_upstream(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/diff", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get branch diff", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_diff( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_diff(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/{name}/tracking-difference", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Branch name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tracking difference", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_tracking_difference( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_branch_tracking_difference(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/branches", - request_body = BranchCreateRequest, - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Create branch", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_create( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_create(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/branches/{name}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Branch name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Delete branch"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - service - .git_branch_delete(namespace, repo_name, name, &session) - .await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true }))) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/branches/remote/{name}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Remote branch name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Delete remote branch"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_delete_remote( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - service - .git_branch_delete_remote(namespace, repo_name, name, &session) - .await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true }))) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git/branches/rename", - request_body = BranchRenameRequest, - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Rename branch", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_rename( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_rename(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git/branches/move", - request_body = BranchMoveRequest, - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Move branch", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_move( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_move(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git/branches/upstream", - request_body = BranchSetUpstreamRequest, - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Set upstream branch"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_set_upstream( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service - .git_branch_set_upstream(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true }))) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/is-merged", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if branch is merged", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_is_merged( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_is_merged(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/merge-base", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get merge base", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_merge_base( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_merge_base(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/is-ancestor", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if branch is ancestor", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_is_ancestor( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_is_ancestor(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/branches/fast-forward/{target}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("target" = String, Path, description = "Target branch name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Fast-forward branch", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_fast_forward( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, target) = path.into_inner(); - let resp = service - .git_branch_fast_forward(namespace, repo_name, target, None, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/branches/is-conflicted", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if branch has conflicts", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_branch_is_conflicted( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_branch_is_conflicted(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/branch_protection.rs b/libs/api/git/branch_protection.rs deleted file mode 100644 index 78bd4d4..0000000 --- a/libs/api/git/branch_protection.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::error::ApiError; -use crate::{ApiResponse, api_success}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::branch_protection::{ - ApprovalCheckResult, BranchProtectionCreateRequest, BranchProtectionResponse, - BranchProtectionUpdateRequest, -}; -use session::Session; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct ProtectionCheckQuery { - pub pr_number: i64, -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/branch-protections", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List branch protection rules", body = ApiResponse>), - ), - tag = "Git" -)] -pub async fn branch_protection_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo) = path.into_inner(); - let rules = service - .branch_protection_list(namespace, repo, &session) - .await?; - Ok(ApiResponse::ok(rules).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/branch-protections/{id}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("id" = i64, Path, description = "Rule id"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get a branch protection rule", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn branch_protection_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, id) = path.into_inner(); - let rule = service - .branch_protection_get(namespace, repo, id, &session) - .await?; - Ok(ApiResponse::ok(rule).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/branch-protections", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = BranchProtectionCreateRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Create a branch protection rule", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn branch_protection_create( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo) = path.into_inner(); - let rule = service - .branch_protection_create(namespace, repo, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(rule).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/branch-protections/{id}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("id" = i64, Path, description = "Rule id"), - ), - request_body = BranchProtectionUpdateRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Update a branch protection rule", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn branch_protection_update( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, id) = path.into_inner(); - let rule = service - .branch_protection_update(namespace, repo, id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(rule).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/branch-protections/{id}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("id" = i64, Path, description = "Rule id"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Delete a branch protection rule"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn branch_protection_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, id) = path.into_inner(); - service - .branch_protection_delete(namespace, repo, id, &session) - .await?; - Ok(api_success()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/branch-protections/check-approvals", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("pr_number" = i64, Query, description = "Pull request number"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check approval count against branch protection", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn branch_protection_check_approvals( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo) = path.into_inner(); - let result = service - .branch_protection_check_approvals(namespace, repo, query.pr_number, &session) - .await?; - Ok(ApiResponse::ok(result).to_response()) -} diff --git a/libs/api/git/commit.rs b/libs/api/git/commit.rs deleted file mode 100644 index 02ad5f3..0000000 --- a/libs/api/git/commit.rs +++ /dev/null @@ -1,1002 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::commit::{ - CommitAmendRequest, CommitAncestorsQuery, CommitCherryPickAbortRequest, - CommitCherryPickRequest, CommitCreateRequest, CommitCreateResponse, CommitDescendantsQuery, - CommitGetQuery, CommitGraphReactResponse, CommitLogQuery, CommitLogResponse, - CommitResolveQuery, CommitRevertAbortRequest, CommitRevertRequest, CommitWalkQuery, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit metadata", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_commit_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_get(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/exists", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Check if commit exists", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_exists( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_exists(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/is-commit", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Check if object is a commit", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_is_commit( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_is_commit(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/message", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit message", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_message( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_message(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/summary", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit summary", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_summary( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_summary(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/short-id", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit short ID", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_short_id( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_short_id(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/author", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit author", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_author( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_author(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/tree-id", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit tree ID", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_tree_id( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_tree_id(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/parent-count", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit parent count", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_parent_count( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_parent_count(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/parent-ids", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit parent IDs", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_parent_ids( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_parent_ids(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/parent/{index}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("index" = usize, Path), - ), - responses( - (status = 200, description = "Get commit parent", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_parent( - service: web::Data, - session: Session, - path: web::Path<(String, String, String, usize)>, -) -> Result { - let (namespace, repo_name, oid, index) = path.into_inner(); - let resp = service - .git_commit_parent(namespace, repo_name, oid, index, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/first-parent", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit first parent", body = ApiResponse>), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_first_parent( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_first_parent(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/is-merge", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Check if commit is a merge", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_is_merge( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_is_merge(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("rev" = Option, Query), - ("per_page" = Option, Query), - ("page" = Option, Query), - ), - responses( - (status = 200, description = "Get commit log (paginated)", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_log( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_log(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/count", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit count", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_count( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_count( - namespace, - repo_name, - query.from.clone(), - query.to.clone(), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/refs", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit refs", body = ApiResponse>), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_refs( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_refs(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/branches", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit branches", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_branches( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_branches(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/tags", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit tags", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_tags( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_tags(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/is-tip", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Check if commit is a tip", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_is_tip( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_is_tip(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/ref-count", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit ref count", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_ref_count( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_ref_count(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/reflog", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit reflog", body = ApiResponse>), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_reflog( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, - refname: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_reflog( - namespace, - repo_name, - query.into_inner(), - refname.refname.clone(), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/graph", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit graph", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_graph( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_graph(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -/// Returns commit graph data enriched with full commit metadata (author, timestamp, -/// parents, lane_index) for use with @gitgraph/react on the frontend. -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/graph-react", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit graph for gitgraph-react", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_commit_graph_react( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_graph_react(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/walk", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Walk commits", body = ApiResponse>), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_walk( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_walk(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/ancestors", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit ancestors", body = ApiResponse>), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_ancestors( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_ancestors(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/descendants", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get commit descendants", body = ApiResponse>), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_descendants( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_commit_descendants(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/commits/resolve", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Resolve revision to commit", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_resolve_rev( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_resolve_rev(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/commits", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - request_body = CommitCreateRequest, - responses( - (status = 200, description = "Create commit", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_create( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_commit_create(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/amend", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - request_body = CommitAmendRequest, - responses( - (status = 200, description = "Amend commit", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_amend( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name, _oid) = path.into_inner(); - let resp = service - .git_commit_amend(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/cherry-pick", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - request_body = CommitCherryPickRequest, - responses( - (status = 200, description = "Cherry-pick commit", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_cherry_pick( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name, _oid) = path.into_inner(); - let resp = service - .git_commit_cherry_pick(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/cherry-pick/abort", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - request_body = CommitCherryPickAbortRequest, - responses( - (status = 200, description = "Abort cherry-pick", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_cherry_pick_abort( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name, _oid) = path.into_inner(); - service - .git_commit_cherry_pick_abort(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(true).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/revert", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - request_body = CommitRevertRequest, - responses( - (status = 200, description = "Revert commit", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_revert( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name, _oid) = path.into_inner(); - let resp = service - .git_commit_revert(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/commits/{oid}/revert/abort", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - request_body = CommitRevertAbortRequest, - responses( - (status = 200, description = "Abort revert", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_commit_revert_abort( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name, _oid) = path.into_inner(); - service - .git_commit_revert_abort(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(true).to_response()) -} - -// Query helpers -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct CommitCountQuery { - pub from: Option, - pub to: Option, -} - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct CommitReflogQuery { - pub refname: Option, -} diff --git a/libs/api/git/contributors.rs b/libs/api/git/contributors.rs deleted file mode 100644 index 1bd21d5..0000000 --- a/libs/api/git/contributors.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::contributors::{ContributorsQuery, ContributorsResponse}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/contributors", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "List of contributors", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" - -)] -pub async fn git_contributors( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_contributors(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/diff.rs b/libs/api/git/diff.rs deleted file mode 100644 index 33d709d..0000000 --- a/libs/api/git/diff.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::diff::{ - DiffCommitQuery, DiffPatchIdResponse, DiffQuery, DiffResultResponse, DiffStatsResponse, - SideBySideDiffQuery, SideBySideDiffResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/diff", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("old_tree" = String, Query, description = "Old tree OID (commit or tree SHA)"), - ("new_tree" = String, Query, description = "New tree OID (commit or tree SHA)"), - ), - responses( - (status = 200, description = "Tree to tree diff", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_diff_tree_to_tree( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_diff_tree_to_tree(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/diff/commit/{commit}", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("commit" = String, Path, description = "Commit identifier"), - ), - responses( - (status = 200, description = "Commit to workdir diff", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_diff_commit_to_workdir( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, commit) = path.into_inner(); - let mut req = query.into_inner(); - req.commit = commit; - let resp = service - .git_diff_commit_to_workdir(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/diff/commit/{commit}/index", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("commit" = String, Path, description = "Commit identifier"), - ), - responses( - (status = 200, description = "Commit to index diff", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_diff_commit_to_index( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, commit) = path.into_inner(); - let mut req = query.into_inner(); - req.commit = commit; - let resp = service - .git_diff_commit_to_index(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/diff/workdir", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "Workdir to index diff", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_diff_workdir_to_index( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_diff_workdir_to_index(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/diff/index", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "Index to tree diff", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_diff_index_to_tree( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_diff_index_to_tree(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/diff/stats", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "Diff statistics", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_diff_stats( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_diff_stats(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/diff/patch-id", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "Patch ID", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_diff_patch_id( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_diff_patch_id(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/diff/side-by-side", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "Side-by-side diff", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_diff_side_by_side( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_diff_side_by_side(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/init.rs b/libs/api/git/init.rs deleted file mode 100644 index 00167f2..0000000 --- a/libs/api/git/init.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::init::GitInitRequest; -use session::Session; - -fn sanitize_repo_path(path: &str) -> Result { - if path.contains("..") || path.contains('~') { - return Err(ApiError(service::error::AppError::BadRequest( - "Invalid repository path".to_string(), - ))); - } - if path.starts_with('/') - || path.starts_with('\\') - || (path.len() >= 3 && path.as_bytes()[1] == b':' && path.as_bytes()[2] == b'\\') - { - return Err(ApiError(service::error::AppError::BadRequest( - "Absolute paths are not allowed".to_string(), - ))); - } - Ok(path.to_string()) -} - -#[utoipa::path( - post, - path = "/api/git/init", - request_body = GitInitRequest, - responses( - (status = 200, description = "Bare repository initialized", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_init_bare( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service.git_init_bare(body.into_inner(), &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/git/open/{path}", - params( - ("path" = String, Path, description = "Repository path"), - ), - responses( - (status = 200, description = "Open repository", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_open( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let path = sanitize_repo_path(&path.into_inner())?; - let resp = service.git_open(path, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/git/open/{path}/bare", - params( - ("path" = String, Path, description = "Repository path"), - ), - responses( - (status = 200, description = "Open bare repository", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_open_bare( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let path = sanitize_repo_path(&path.into_inner())?; - let resp = service.git_open_bare(path, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/git/is-repo/{path}", - params( - ("path" = String, Path, description = "Repository path"), - ), - responses( - (status = 200, description = "Check if path is a repository", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_is_repo( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let path = sanitize_repo_path(&path.into_inner())?; - let resp = service.git_is_repo(path, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/path", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "Repository path", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_repo_path( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_repo_path(namespace, repo_name, &session) - .await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "path": resp }))) -} diff --git a/libs/api/git/mod.rs b/libs/api/git/mod.rs deleted file mode 100644 index 328c1cc..0000000 --- a/libs/api/git/mod.rs +++ /dev/null @@ -1,410 +0,0 @@ -pub mod archive; -pub mod blame; -pub mod blob; -pub mod branch; -pub mod branch_protection; -pub mod commit; -pub mod contributors; -pub mod diff; -pub mod init; -pub mod refs; -pub mod repo; -pub mod star; -pub mod tag; -pub mod tree; -pub mod watch; -pub mod webhook; - -use actix_web::web; - -pub fn init_git_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/repos/{namespace}/{repo}/git") - .route("/archive", web::get().to(archive::git_archive)) - .route("/archive/list", web::get().to(archive::git_archive_list)) - .route( - "/archive/summary", - web::get().to(archive::git_archive_summary), - ) - .route( - "/archive/cached", - web::get().to(archive::git_archive_cached), - ) - .route( - "/archive/invalidate", - web::get().to(archive::git_archive_invalidate), - ) - .route( - "/archive/invalidate-all/{commit_oid}", - web::get().to(archive::git_archive_invalidate_all), - ) - .route( - "/blame/{commit_oid}/{tail:.*}", - web::get().to(blame::git_blame_file), - ) - // blob - .route("/blob", web::post().to(blob::git_blob_create)) - .route("/blob/{oid}", web::get().to(blob::git_blob_get)) - .route("/blob/{oid}/exists", web::get().to(blob::git_blob_exists)) - .route( - "/blob/{oid}/is-binary", - web::get().to(blob::git_blob_is_binary), - ) - .route("/blob/{oid}/content", web::get().to(blob::git_blob_content)) - .route("/blob/{oid}/size", web::get().to(blob::git_blob_size)) - .route("/readme", web::get().to(blob::git_readme)) - .route("/branches", web::get().to(branch::git_branch_list)) - .route( - "/branches/summary", - web::get().to(branch::git_branch_summary), - ) - .route("/branches", web::post().to(branch::git_branch_create)) - .route( - "/branches/current", - web::get().to(branch::git_branch_current), - ) - .route( - "/branches/remote/{name}", - web::delete().to(branch::git_branch_delete_remote), - ) - .route( - "/branches/rename", - web::patch().to(branch::git_branch_rename), - ) - .route("/branches/move", web::patch().to(branch::git_branch_move)) - .route( - "/branches/upstream", - web::patch().to(branch::git_branch_set_upstream), - ) - .route("/branches/diff", web::get().to(branch::git_branch_diff)) - .route( - "/branches/is-detached", - web::get().to(branch::git_branch_is_detached), - ) - .route( - "/branches/is-merged", - web::get().to(branch::git_branch_is_merged), - ) - .route( - "/branches/merge-base", - web::get().to(branch::git_branch_merge_base), - ) - .route( - "/branches/is-ancestor", - web::get().to(branch::git_branch_is_ancestor), - ) - .route( - "/branches/fast-forward/{target}", - web::post().to(branch::git_branch_fast_forward), - ) - .route( - "/branches/is-conflicted", - web::get().to(branch::git_branch_is_conflicted), - ) - // parameterized routes with {name} - .route("/branches/{name}", web::get().to(branch::git_branch_get)) - .route( - "/branches/{name}", - web::delete().to(branch::git_branch_delete), - ) - .route( - "/branches/{name}/exists", - web::get().to(branch::git_branch_exists), - ) - .route( - "/branches/{name}/is-head", - web::get().to(branch::git_branch_is_head), - ) - .route( - "/branches/{name}/upstream", - web::get().to(branch::git_branch_upstream), - ) - .route( - "/branches/{name}/tracking-difference", - web::get().to(branch::git_branch_tracking_difference), - ) - // commit - specific routes first, then parameterized routes - .route("/commits", web::get().to(commit::git_commit_log)) - .route("/commits/count", web::get().to(commit::git_commit_count)) - .route("/commits", web::post().to(commit::git_commit_create)) - .route("/commits/graph", web::get().to(commit::git_commit_graph)) - .route( - "/commits/graph-react", - web::get().to(commit::git_commit_graph_react), - ) - .route("/commits/walk", web::get().to(commit::git_commit_walk)) - .route( - "/commits/resolve/{rev}", - web::get().to(commit::git_commit_resolve_rev), - ) - .route("/commits/reflog", web::get().to(commit::git_commit_reflog)) - .route( - "/commits/branches", - web::get().to(commit::git_commit_branches), - ) - .route("/commits/tags", web::get().to(commit::git_commit_tags)) - // parameterized routes with {oid} - .route("/commits/{oid}", web::get().to(commit::git_commit_get)) - .route("/commits/{oid}", web::patch().to(commit::git_commit_amend)) - .route( - "/commits/{oid}/exists", - web::get().to(commit::git_commit_exists), - ) - .route( - "/commits/{oid}/is-commit", - web::get().to(commit::git_commit_is_commit), - ) - .route( - "/commits/{oid}/message", - web::get().to(commit::git_commit_message), - ) - .route( - "/commits/{oid}/summary", - web::get().to(commit::git_commit_summary), - ) - .route( - "/commits/{oid}/short-id", - web::get().to(commit::git_commit_short_id), - ) - .route( - "/commits/{oid}/author", - web::get().to(commit::git_commit_author), - ) - .route( - "/commits/{oid}/tree-id", - web::get().to(commit::git_commit_tree_id), - ) - .route( - "/commits/{oid}/parent-count", - web::get().to(commit::git_commit_parent_count), - ) - .route( - "/commits/{oid}/parent-ids", - web::get().to(commit::git_commit_parent_ids), - ) - .route( - "/commits/{oid}/parent/{index}", - web::get().to(commit::git_commit_parent), - ) - .route( - "/commits/{oid}/first-parent", - web::get().to(commit::git_commit_first_parent), - ) - .route( - "/commits/{oid}/is-merge", - web::get().to(commit::git_commit_is_merge), - ) - .route( - "/commits/{oid}/refs", - web::get().to(commit::git_commit_refs), - ) - .route( - "/commits/{oid}/is-tip", - web::get().to(commit::git_commit_is_tip), - ) - .route( - "/commits/{oid}/ref-count", - web::get().to(commit::git_commit_ref_count), - ) - .route( - "/commits/{oid}/ancestors", - web::get().to(commit::git_commit_ancestors), - ) - .route( - "/commits/{oid}/descendants", - web::get().to(commit::git_commit_descendants), - ) - .route( - "/commits/{oid}/cherry-pick", - web::post().to(commit::git_commit_cherry_pick), - ) - .route( - "/commits/{oid}/cherry-pick/abort", - web::post().to(commit::git_commit_cherry_pick_abort), - ) - .route( - "/commits/{oid}/revert", - web::post().to(commit::git_commit_revert), - ) - .route( - "/commits/{oid}/revert/abort", - web::post().to(commit::git_commit_revert_abort), - ) - .route( - "/contributors", - web::get().to(contributors::git_contributors), - ) - // diff - .route("/diff", web::get().to(diff::git_diff_tree_to_tree)) - .route( - "/diff/commit/{commit}", - web::get().to(diff::git_diff_commit_to_workdir), - ) - .route( - "/diff/commit/{commit}/index", - web::get().to(diff::git_diff_commit_to_index), - ) - .route( - "/diff/workdir", - web::get().to(diff::git_diff_workdir_to_index), - ) - .route("/diff/index", web::get().to(diff::git_diff_index_to_tree)) - .route("/diff/stats", web::get().to(diff::git_diff_stats)) - .route("/diff/patch-id", web::get().to(diff::git_diff_patch_id)) - .route( - "/diff/side-by-side", - web::get().to(diff::git_diff_side_by_side), - ) - // refs - specific routes first, then parameterized routes - .route("/refs", web::get().to(refs::git_ref_list)) - .route("/refs", web::post().to(refs::git_ref_create)) - .route("/refs/rename", web::patch().to(refs::git_ref_rename)) - .route("/refs/update", web::patch().to(refs::git_ref_update)) - // parameterized routes with {name} - .route("/refs/{name}", web::get().to(refs::git_ref_get)) - .route("/refs/{name}", web::delete().to(refs::git_ref_delete)) - .route("/refs/{name}/exists", web::get().to(refs::git_ref_exists)) - .route("/refs/{name}/target", web::get().to(refs::git_ref_target)) - // repo (description, config, merge) - .route("/description", web::get().to(repo::git_description_get)) - .route("/description", web::put().to(repo::git_description_set)) - .route( - "/description", - web::delete().to(repo::git_description_reset), - ) - .route( - "/description/exists", - web::get().to(repo::git_description_exists), - ) - .route("/git", web::patch().to(repo::git_update_repo)) - .route("/config/entries", web::get().to(repo::git_config_entries)) - .route("/config/{key}", web::get().to(repo::git_config_get)) - .route("/config/{key}", web::put().to(repo::git_config_set)) - .route("/config/{key}", web::delete().to(repo::git_config_delete)) - .route("/config/{key}/has", web::get().to(repo::git_config_has)) - .route( - "/merge/analysis/{their_oid}", - web::get().to(repo::git_merge_analysis), - ) - .route( - "/merge/analysis/{ref_name}/{their_oid}", - web::get().to(repo::git_merge_analysis_for_ref), - ) - .route( - "/merge/base/{oid1}/{oid2}", - web::get().to(repo::git_merge_base), - ) - .route("/merge/commits", web::post().to(repo::git_merge_commits)) - .route("/merge/trees", web::post().to(repo::git_merge_trees)) - .route("/merge/abort", web::post().to(repo::git_merge_abort)) - .route( - "/merge/in-progress", - web::get().to(repo::git_merge_is_in_progress), - ) - .route("/merge/heads", web::get().to(repo::git_mergehead_list)) - .route( - "/merge/is-conflicted", - web::get().to(repo::git_merge_is_conflicted), - ) - .route("/star", web::post().to(star::git_star)) - .route("/star", web::delete().to(star::git_unstar)) - .route("/star/is-starred", web::get().to(star::git_is_starred)) - .route("/star/count", web::get().to(star::git_star_count)) - .route("/star/users", web::get().to(star::git_star_user_list)) - .route( - "/branch-protections", - web::get().to(branch_protection::branch_protection_list), - ) - .route( - "/branch-protections", - web::post().to(branch_protection::branch_protection_create), - ) - .route( - "/branch-protections/check-approvals", - web::get().to(branch_protection::branch_protection_check_approvals), - ) - .route( - "/branch-protections/{id}", - web::get().to(branch_protection::branch_protection_get), - ) - .route( - "/branch-protections/{id}", - web::patch().to(branch_protection::branch_protection_update), - ) - .route( - "/branch-protections/{id}", - web::delete().to(branch_protection::branch_protection_delete), - ) - .route("/tags", web::get().to(tag::git_tag_list)) - .route("/tags/names", web::get().to(tag::git_tag_list_names)) - .route("/tags/summary", web::get().to(tag::git_tag_summary)) - .route("/tags/count", web::get().to(tag::git_tag_count)) - .route("/tags", web::post().to(tag::git_tag_create)) - .route( - "/tags/lightweight", - web::post().to(tag::git_tag_create_lightweight), - ) - .route("/tags/rename", web::patch().to(tag::git_tag_rename)) - .route( - "/tags/message", - web::patch().to(tag::git_tag_update_message), - ) - .route("/tags/{name}", web::get().to(tag::git_tag_get)) - .route("/tags/{name}", web::delete().to(tag::git_tag_delete)) - .route("/tags/{name}/exists", web::get().to(tag::git_tag_exists)) - .route("/tags/{name}/target", web::get().to(tag::git_tag_target)) - .route( - "/tags/{name}/is-annotated", - web::get().to(tag::git_tag_is_annotated), - ) - .route("/tags/{name}/message", web::get().to(tag::git_tag_message)) - .route("/tags/{name}/tagger", web::get().to(tag::git_tag_tagger)) - .route("/tree/{oid}", web::get().to(tree::git_tree_get)) - .route("/tree/{oid}/exists", web::get().to(tree::git_tree_exists)) - .route("/tree/{oid}/list", web::get().to(tree::git_tree_list)) - .route( - "/tree/{oid}/entry/{index}", - web::get().to(tree::git_tree_entry), - ) - .route( - "/tree/{oid}/entry-by-path", - web::get().to(tree::git_tree_entry_by_path), - ) - .route( - "/tree/{commit}/commit-entry-by-path", - web::get().to(tree::git_tree_entry_by_commit_path), - ) - .route( - "/tree/{oid}/entry-count", - web::get().to(tree::git_tree_entry_count), - ) - .route( - "/tree/{oid}/is-empty", - web::get().to(tree::git_tree_is_empty), - ) - .route("/tree/diff-stats", web::get().to(tree::git_tree_diffstats)) - .route("/watch", web::post().to(watch::git_watch)) - .route("/watch", web::delete().to(watch::git_unwatch)) - .route("/watch/is-watched", web::get().to(watch::git_is_watched)) - .route("/watch/count", web::get().to(watch::git_watch_count)) - .route("/watch/users", web::get().to(watch::git_watch_user_list)) - .route("/webhooks", web::get().to(webhook::git_webhook_list)) - .route("/webhooks", web::post().to(webhook::git_webhook_create)) - .route( - "/webhooks/{webhook_id}", - web::get().to(webhook::git_webhook_get), - ) - .route( - "/webhooks/{webhook_id}", - web::patch().to(webhook::git_webhook_update), - ) - .route( - "/webhooks/{webhook_id}", - web::delete().to(webhook::git_webhook_delete), - ), - ); -} - -pub fn init_git_toplevel_routes(cfg: &mut web::ServiceConfig) { - cfg.service(web::scope("/git").route("/is-repo/{path}", web::get().to(init::git_is_repo))); -} diff --git a/libs/api/git/refs.rs b/libs/api/git/refs.rs deleted file mode 100644 index 573b5b3..0000000 --- a/libs/api/git/refs.rs +++ /dev/null @@ -1,226 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::refs::{ - RefCreateRequest, RefDeleteResponse, RefExistsResponse, RefInfoResponse, RefListQuery, - RefRenameQuery, RefTargetResponse, RefUpdateRequest, RefUpdateResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/refs", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "List of refs", body = ApiResponse>), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_ref_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_ref_list(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/refs/{name}", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Ref name"), - ), - responses( - (status = 200, description = "Ref info", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_ref_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_ref_get(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/refs", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = RefCreateRequest, - responses( - (status = 200, description = "Ref created", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_ref_create( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_ref_create(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/refs/{name}", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Ref name"), - ), - responses( - (status = 200, description = "Ref deleted", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_ref_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_ref_delete(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git/refs/rename", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "Ref renamed", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_ref_rename( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_ref_rename(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - put, - path = "/api/repos/{namespace}/{repo}/git/refs", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = RefUpdateRequest, - responses( - (status = 200, description = "Ref updated", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_ref_update( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_ref_update(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/refs/{name}/exists", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Ref name"), - ), - responses( - (status = 200, description = "Ref exists check", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_ref_exists( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_ref_exists(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/refs/{name}/target", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Ref name"), - ), - responses( - (status = 200, description = "Ref target", body = ApiResponse), - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_ref_target( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let resp = service - .git_ref_target(namespace, repo_name, name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/repo.rs b/libs/api/git/repo.rs deleted file mode 100644 index c4c9167..0000000 --- a/libs/api/git/repo.rs +++ /dev/null @@ -1,542 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::repo::{ - ConfigBoolResponse, ConfigDeleteQuery, ConfigEntriesQuery, ConfigGetQuery, ConfigSetRequest, - ConfigSnapshotResponse, DescriptionQuery, DescriptionResponse, GitUpdateRepoRequest, - MergeAnalysisQuery, MergeAnalysisResponse, MergeCommitsRequest, MergeRefAnalysisQuery, - MergeTreesRequest, MergeheadInfoResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/description", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get repository description", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_description_get( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_description_get(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - put, - path = "/api/repos/{namespace}/{repo}/git/description", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = DescriptionQuery, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Set repository description", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_description_set( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_description_set(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/description", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Reset repository description", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_description_reset( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_description_reset(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/description/exists", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if repository description exists", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_description_exists( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_description_exists(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({"exists": resp})).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/config/entries", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List repository config entries", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_config_entries( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_config_entries(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/config/{key}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("key" = String, Path, description = "Config key"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get repository config value", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_config_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, key) = path.into_inner(); - let mut req = query.into_inner(); - req.key = key; - let resp = service - .git_config_get(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({"value": resp})).to_response()) -} - -#[utoipa::path( - put, - path = "/api/repos/{namespace}/{repo}/git/config/{key}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("key" = String, Path, description = "Config key"), - ), - request_body = ConfigSetRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Set repository config value"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_config_set( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name, key) = path.into_inner(); - let mut req = body.into_inner(); - req.key = key; - service - .git_config_set(namespace, repo_name, req, &session) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/config/{key}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("key" = String, Path, description = "Config key"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Delete repository config key"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_config_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, key) = path.into_inner(); - let mut req = query.into_inner(); - req.key = key; - service - .git_config_delete(namespace, repo_name, req, &session) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/config/{key}/has", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("key" = String, Path, description = "Config key"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if repository config key exists", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_config_has( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, key) = path.into_inner(); - let mut req = query.into_inner(); - req.key = key; - let resp = service - .git_config_has(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/merge/analysis/{their_oid}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("their_oid" = String, Path, description = "The OID to analyze merge against"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Perform merge analysis", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_merge_analysis( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, their_oid) = path.into_inner(); - let mut req = query.into_inner(); - req.their_oid = their_oid; - let resp = service - .git_merge_analysis(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/merge/analysis/{ref_name}/{their_oid}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("ref_name" = String, Path, description = "Reference name"), - ("their_oid" = String, Path, description = "The OID to analyze merge against"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Perform merge analysis for a specific ref", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_merge_analysis_for_ref( - service: web::Data, - session: Session, - path: web::Path<(String, String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, ref_name, their_oid) = path.into_inner(); - let mut req = query.into_inner(); - req.ref_name = ref_name; - req.their_oid = their_oid; - let resp = service - .git_merge_analysis_for_ref(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/merge/base/{oid1}/{oid2}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid1" = String, Path, description = "First commit OID"), - ("oid2" = String, Path, description = "Second commit OID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get merge base of two commits", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_merge_base( - service: web::Data, - session: Session, - path: web::Path<(String, String, String, String)>, -) -> Result { - let (namespace, repo_name, oid1, oid2) = path.into_inner(); - let resp = service - .git_merge_base(namespace, repo_name, oid1, oid2, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({"merge_base": resp})).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/merge/commits", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = MergeCommitsRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Merge commits"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_merge_commits( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service - .git_merge_commits(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/merge/trees", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = MergeTreesRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Merge trees"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_merge_trees( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service - .git_merge_trees(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/merge/abort", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Abort an in-progress merge"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_merge_abort( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service - .git_merge_abort(namespace, repo_name, &session) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/merge/in-progress", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if merge is in progress", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_merge_is_in_progress( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_merge_is_in_progress(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({"in_progress": resp})).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/merge/heads", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List merge heads", body = ApiResponse>), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_mergehead_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_mergehead_list(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/merge/is-conflicted", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if merge has conflicts", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_merge_is_conflicted( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_merge_is_conflicted(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({"is_conflicted": resp})).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = GitUpdateRepoRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Update repository settings"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_update_repo( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service - .git_update_repo(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(crate::api_success()) -} diff --git a/libs/api/git/star.rs b/libs/api/git/star.rs deleted file mode 100644 index 09d0216..0000000 --- a/libs/api/git/star.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::star::{StarCountResponse, StarUserListResponse}; -use session::Session; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct StarPagerQuery { - pub page: Option, - #[serde(alias = "par_page")] - pub per_page: Option, -} - -impl From for service::Pager { - fn from(q: StarPagerQuery) -> Self { - service::Pager { - page: q.page.unwrap_or(1), - per_page: q.per_page.unwrap_or(20), - } - } -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/star", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Star the repository"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_star( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service.git_star(namespace, repo_name, &session).await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/star", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Unstar the repository"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_unstar( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service.git_unstar(namespace, repo_name, &session).await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/star/is-starred", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if the current user has starred the repository", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_is_starred( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_is_starred(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({"is_starred": resp})).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/star/count", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get star count for the repository", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_star_count( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_star_count(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/star/users", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("page" = Option, Query, description = "Page number"), - ("per_page" = Option, Query, description = "Items per page"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List users who starred the repository", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_star_user_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_star_user_list(namespace, repo_name, query.into_inner().into(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/tag.rs b/libs/api/git/tag.rs deleted file mode 100644 index b2a52f8..0000000 --- a/libs/api/git/tag.rs +++ /dev/null @@ -1,434 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::tag::{ - TagCountResponse, TagCreateLightweightRequest, TagCreateRequest, TagExistsResponse, - TagGetQuery, TagInfoResponse, TagIsAnnotatedResponse, TagMessageResponse, TagRenameQuery, - TagSummaryResponse, TagTaggerResponse, TagTargetQuery, TagTargetResponse, - TagUpdateMessageRequest, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List all tags", body = ApiResponse>), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service.git_tag_list(namespace, repo_name, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/names", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List all tag names", body = ApiResponse>), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_list_names( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_tag_list_names(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/summary", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tag summary", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_summary( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_tag_summary(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/count", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tag count", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_count( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_tag_count(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/tags", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = TagCreateRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Create an annotated tag", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_create( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_tag_create(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/tags/lightweight", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = TagCreateLightweightRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Create a lightweight tag", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_create_lightweight( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_tag_create_lightweight(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/{name}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Tag name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get a tag by name", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let mut req = query.into_inner(); - req.name = name; - let resp = service - .git_tag_get(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/tags/{name}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Tag name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Delete a tag"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let mut req = query.into_inner(); - req.name = name; - service - .git_tag_delete(namespace, repo_name, req, &session) - .await?; - Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true }))) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/{name}/exists", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Tag name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if a tag exists", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_exists( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let mut req = query.into_inner(); - req.name = name; - let resp = service - .git_tag_exists(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/{name}/target", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Tag name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tag target OID", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_target( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let mut req = query.into_inner(); - req.name = name; - let resp = service - .git_tag_target(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/{name}/is-annotated", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Tag name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if a tag is annotated", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_is_annotated( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let mut req = query.into_inner(); - req.name = name; - let resp = service - .git_tag_is_annotated(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/{name}/message", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Tag name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tag message", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_message( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let mut req = query.into_inner(); - req.name = name; - let resp = service - .git_tag_message(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git/tags/rename", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = TagRenameQuery, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Rename a tag", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_rename( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_tag_rename(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git/tags/message", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = TagUpdateMessageRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Update tag message", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_update_message( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_tag_update_message(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tags/{name}/tagger", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("name" = String, Path, description = "Tag name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tag tagger info", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tag_tagger( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, name) = path.into_inner(); - let mut req = query.into_inner(); - req.name = name; - let resp = service - .git_tag_tagger(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/tree.rs b/libs/api/git/tree.rs deleted file mode 100644 index 2d1238e..0000000 --- a/libs/api/git/tree.rs +++ /dev/null @@ -1,278 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::tree::{ - TreeDiffQuery, TreeDiffStatsResponse, TreeEntryByCommitPathQuery, TreeEntryByPathQuery, - TreeEntryCountResponse, TreeEntryQuery, TreeEntryResponse, TreeExistsResponse, TreeGetQuery, - TreeInfoResponse, TreeIsEmptyResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/{oid}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Tree object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tree info", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_tree_get(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/{oid}/exists", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Tree object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if tree exists", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_exists( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_tree_exists(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/{oid}/list", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Tree object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List tree entries", body = ApiResponse>), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_list( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_tree_list(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/{oid}/entry/{index}", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Tree object ID"), - ("index" = usize, Path, description = "Entry index"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tree entry by index", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_entry( - service: web::Data, - session: Session, - path: web::Path<(String, String, String, usize)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid, index) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - req.index = index; - let resp = service - .git_tree_entry(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/{oid}/entry-by-path", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Tree object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tree entry by path", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_entry_by_path( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_tree_entry_by_path(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/{commit}/commit-entry-by-path", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("commit" = String, Path, description = "Commit OID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tree entry by commit path", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_entry_by_commit_path( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, commit) = path.into_inner(); - let mut req = query.into_inner(); - req.commit = commit; - let resp = service - .git_tree_entry_by_commit_path(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/{oid}/entry-count", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Tree object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tree entry count", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_entry_count( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_tree_entry_count(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/{oid}/is-empty", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("oid" = String, Path, description = "Tree object ID"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if tree is empty", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_is_empty( - service: web::Data, - session: Session, - path: web::Path<(String, String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - let mut req = query.into_inner(); - req.oid = oid; - let resp = service - .git_tree_is_empty(namespace, repo_name, req, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/tree/diff-stats", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get tree diff stats", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_tree_diffstats( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_tree_diffstats(namespace, repo_name, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/watch.rs b/libs/api/git/watch.rs deleted file mode 100644 index 7f91300..0000000 --- a/libs/api/git/watch.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::watch::{GitWatchRequest, WatchCountResponse, WatchUserListResponse}; -use session::Session; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct WatchPagerQuery { - pub page: Option, - #[serde(alias = "par_page")] - pub per_page: Option, -} - -impl From for service::Pager { - fn from(q: WatchPagerQuery) -> Self { - service::Pager { - page: q.page.unwrap_or(1), - per_page: q.per_page.unwrap_or(20), - } - } -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/watch", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = GitWatchRequest, - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Watch the repository"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_watch( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service - .git_watch(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/watch", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Unwatch the repository"), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_unwatch( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - service.git_unwatch(namespace, repo_name, &session).await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/watch/is-watched", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Check if the current user is watching the repository", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_is_watched( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_is_watched(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({"is_watched": resp})).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/watch/count", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "Get watch count for the repository", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_watch_count( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_watch_count(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/watch/users", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("page" = Option, Query, description = "Page number"), - ("per_page" = Option, Query, description = "Items per page"), - ), - responses( - (status = 401, description = "Unauthorized", body = ApiResponse), - (status = 200, description = "List users who are watching the repository", body = ApiResponse), - (status = 404, description = "Not found", body = ApiResponse), - ), - tag = "Git" -)] -pub async fn git_watch_user_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_watch_user_list(namespace, repo_name, query.into_inner().into(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/git/webhook.rs b/libs/api/git/webhook.rs deleted file mode 100644 index ae5690d..0000000 --- a/libs/api/git/webhook.rs +++ /dev/null @@ -1,153 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::webhook::{ - CreateWebhookParams, UpdateWebhookParams, WebhookListResponse, WebhookResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/webhooks", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - responses( - (status = 200, description = "List webhooks", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Git" -)] -pub async fn git_webhook_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_webhook_list(namespace, repo_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repos/{namespace}/{repo}/git/webhooks", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ), - request_body = CreateWebhookParams, - responses( - (status = 200, description = "Create webhook", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Git" -)] -pub async fn git_webhook_create( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let resp = service - .git_webhook_create(namespace, repo_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repos/{namespace}/{repo}/git/webhooks/{webhook_id}", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("webhook_id" = i64, Path, description = "Webhook ID"), - ), - responses( - (status = 200, description = "Get webhook", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Git" -)] -pub async fn git_webhook_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo_name, webhook_id) = path.into_inner(); - let resp = service - .git_webhook_get(namespace, repo_name, webhook_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repos/{namespace}/{repo}/git/webhooks/{webhook_id}", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("webhook_id" = i64, Path, description = "Webhook ID"), - ), - request_body = UpdateWebhookParams, - responses( - (status = 200, description = "Update webhook", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Git" -)] -pub async fn git_webhook_update( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo_name, webhook_id) = path.into_inner(); - let resp = service - .git_webhook_update( - namespace, - repo_name, - webhook_id, - body.into_inner(), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repos/{namespace}/{repo}/git/webhooks/{webhook_id}", - params( - ("namespace" = String, Path, description = "Repository namespace"), - ("repo" = String, Path, description = "Repository name"), - ("webhook_id" = i64, Path, description = "Webhook ID"), - ), - responses( - (status = 200, description = "Delete webhook"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Git" -)] -pub async fn git_webhook_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo_name, webhook_id) = path.into_inner(); - service - .git_webhook_delete(namespace, repo_name, webhook_id, &session) - .await?; - Ok(ApiResponse::ok(true).to_response()) -} diff --git a/libs/api/issue/assignee.rs b/libs/api/issue/assignee.rs deleted file mode 100644 index 16ec87a..0000000 --- a/libs/api/issue/assignee.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/assignees", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "List issue assignees", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_assignee_list( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_assignee_list(project, issue_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/assignees", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - request_body = service::issue::IssueAssignUserRequest, - responses( - (status = 200, description = "Add assignee to issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_assignee_add( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_assignee_add(project, issue_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}/assignees/{assignee_id}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("assignee_id" = String, Path), - ), - responses( - (status = 200, description = "Remove assignee from issue"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_assignee_remove( - service: web::Data, - session: Session, - path: web::Path<(String, i64, String)>, -) -> Result { - let (project, issue_number, assignee_id) = path.into_inner(); - let assignee_uuid = uuid::Uuid::parse_str(&assignee_id) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - service - .issue_assignee_remove(project, issue_number, assignee_uuid, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/issue/comment.rs b/libs/api/issue/comment.rs deleted file mode 100644 index 296c61e..0000000 --- a/libs/api/issue/comment.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/comments", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("page" = Option, Query), - ("per_page" = Option, Query), - ), - responses( - (status = 200, description = "List issue comments", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_comment_list( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - query: web::Query, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_comment_list( - project, - issue_number, - Some(query.page.unwrap_or(1)), - Some(query.per_page.unwrap_or(20)), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/comments/{comment_id}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("comment_id" = i64, Path), - ), - responses( - (status = 200, description = "Get issue comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_comment_get( - service: web::Data, - session: Session, - path: web::Path<(String, i64, i64)>, -) -> Result { - let (project, issue_number, comment_id) = path.into_inner(); - let resp = service - .issue_comment_get(project, issue_number, comment_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/comments", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - request_body = service::issue::IssueCommentCreateRequest, - responses( - (status = 200, description = "Create issue comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_comment_create( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_comment_create(project, issue_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/issue/{project}/issues/{number}/comments/{comment_id}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("comment_id" = i64, Path), - ), - request_body = service::issue::IssueCommentUpdateRequest, - responses( - (status = 200, description = "Update issue comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_comment_update( - service: web::Data, - session: Session, - path: web::Path<(String, i64, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number, comment_id) = path.into_inner(); - let resp = service - .issue_comment_update( - project, - issue_number, - comment_id, - body.into_inner(), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}/comments/{comment_id}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("comment_id" = i64, Path), - ), - responses( - (status = 200, description = "Delete issue comment"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_comment_delete( - service: web::Data, - session: Session, - path: web::Path<(String, i64, i64)>, -) -> Result { - let (project, issue_number, comment_id) = path.into_inner(); - service - .issue_comment_delete(project, issue_number, comment_id, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/issue/comment_reaction.rs b/libs/api/issue/comment_reaction.rs deleted file mode 100644 index 90b09f3..0000000 --- a/libs/api/issue/comment_reaction.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/comments/{comment_id}/reactions", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("comment_id" = i64, Path), - ), - responses( - (status = 200, description = "List comment reactions", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_comment_reaction_list( - service: web::Data, - session: Session, - path: web::Path<(String, i64, i64)>, -) -> Result { - let (project, issue_number, comment_id) = path.into_inner(); - let resp = service - .issue_comment_reaction_list(project, issue_number, comment_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/comments/{comment_id}/reactions", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("comment_id" = i64, Path), - ), - request_body = service::issue::ReactionAddRequest, - responses( - (status = 200, description = "Add reaction to comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_comment_reaction_add( - service: web::Data, - session: Session, - path: web::Path<(String, i64, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number, comment_id) = path.into_inner(); - let resp = service - .issue_comment_reaction_add( - project, - issue_number, - comment_id, - body.into_inner(), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}/comments/{comment_id}/reactions/{reaction}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("comment_id" = i64, Path), - ("reaction" = String, Path), - ), - responses( - (status = 200, description = "Remove reaction from comment"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_comment_reaction_remove( - service: web::Data, - session: Session, - path: web::Path<(String, i64, i64, String)>, -) -> Result { - let (project, issue_number, comment_id, reaction) = path.into_inner(); - service - .issue_comment_reaction_remove(project, issue_number, comment_id, reaction, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/issue/issue_label.rs b/libs/api/issue/issue_label.rs deleted file mode 100644 index c537419..0000000 --- a/libs/api/issue/issue_label.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::issue::IssueAddLabelsByNamesRequest; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/labels", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "List issue labels", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_label_list( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_label_list(project, issue_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/labels", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - request_body = service::issue::IssueAddLabelRequest, - responses( - (status = 200, description = "Add label to issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_label_add( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_label_add(project, issue_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}/labels/{label_id}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("label_id" = i64, Path), - ), - responses( - (status = 200, description = "Remove label from issue"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_label_remove( - service: web::Data, - session: Session, - path: web::Path<(String, i64, i64)>, -) -> Result { - let (project, issue_number, label_id) = path.into_inner(); - service - .issue_label_remove(project, issue_number, label_id, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/labels/bulk", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - request_body = IssueAddLabelsByNamesRequest, - responses( - (status = 200, description = "Add labels to issue by name", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_label_add_bulk( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_label_add_by_names(project, issue_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/issue/label.rs b/libs/api/issue/label.rs deleted file mode 100644 index d8edd14..0000000 --- a/libs/api/issue/label.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/labels", - params(("project" = String, Path)), - responses( - (status = 200, description = "List labels", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Issues" -)] -pub async fn label_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project = path.into_inner(); - let resp = service.label_list(project, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/labels", - params(("project" = String, Path)), - request_body = service::issue::CreateLabelRequest, - responses( - (status = 200, description = "Create label", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn label_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project = path.into_inner(); - let resp = service - .label_create(project, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/labels/{label_id}", - params( - ("project" = String, Path), - ("label_id" = i64, Path), - ), - responses( - (status = 200, description = "Delete label"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn label_delete( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, label_id) = path.into_inner(); - service.label_delete(project, label_id, &session).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/issue/mod.rs b/libs/api/issue/mod.rs deleted file mode 100644 index 6004da0..0000000 --- a/libs/api/issue/mod.rs +++ /dev/null @@ -1,363 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -pub mod assignee; -pub mod comment; -pub mod comment_reaction; -pub mod issue_label; -pub mod label; -pub mod pull_request; -pub mod reaction; -pub mod repo; -pub mod subscriber; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct ListQuery { - pub state: Option, - pub page: Option, - pub per_page: Option, -} - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct PagerQuery { - pub page: Option, - pub per_page: Option, -} - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues", - params( - ("project" = String, Path), - ("state" = Option, Query), - ("page" = Option, Query), - ("per_page" = Option, Query), - ), - responses( - (status = 200, description = "List issues", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_list( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project = path.into_inner(); - let resp = service - .issue_list( - project, - query.state.clone(), - Some(query.page.unwrap_or(1)), - Some(query.per_page.unwrap_or(20)), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Get issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_get( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, number) = path.into_inner(); - let resp = service.issue_get(project, number, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues", - params(("project" = String, Path)), - request_body = service::issue::IssueCreateRequest, - responses( - (status = 200, description = "Create issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project = path.into_inner(); - let resp = service - .issue_create(project, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/issue/{project}/issues/{number}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - request_body = service::issue::IssueUpdateRequest, - responses( - (status = 200, description = "Update issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_update( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project, number) = path.into_inner(); - let resp = service - .issue_update(project, number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/close", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Close issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_close( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, number) = path.into_inner(); - let resp = service.issue_close(project, number, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/reopen", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Reopen issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_reopen( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, number) = path.into_inner(); - let resp = service.issue_reopen(project, number, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Delete issue"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_delete( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, number) = path.into_inner(); - service.issue_delete(project, number, &session).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/summary", - params(("project" = String, Path)), - responses( - (status = 200, description = "Get issue summary", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_summary( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project = path.into_inner(); - let resp = service.issue_summary(project, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -pub fn init_issue_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("issue/{project}") - .route("/issues", web::get().to(issue_list)) - .route("/issues", web::post().to(issue_create)) - .route("/issues/summary", web::get().to(issue_summary)) - .route("/issues/{number}", web::get().to(issue_get)) - .route("/issues/{number}", web::patch().to(issue_update)) - .route("/issues/{number}", web::delete().to(issue_delete)) - .route("/issues/{number}/close", web::post().to(issue_close)) - .route("/issues/{number}/reopen", web::post().to(issue_reopen)) - .route( - "/issues/{number}/labels", - web::get().to(issue_label::issue_label_list), - ) - .route( - "/issues/{number}/labels", - web::post().to(issue_label::issue_label_add), - ) - .route( - "/issues/{number}/labels/bulk", - web::post().to(issue_label::issue_label_add_bulk), - ) - .route( - "/issues/{number}/labels/{label_id}", - web::delete().to(issue_label::issue_label_remove), - ) - .route( - "/issues/{number}/comments", - web::get().to(comment::issue_comment_list), - ) - .route( - "/issues/{number}/comments/{comment_id}", - web::get().to(comment::issue_comment_get), - ) - .route( - "/issues/{number}/comments", - web::post().to(comment::issue_comment_create), - ) - .route( - "/issues/{number}/comments/{comment_id}", - web::patch().to(comment::issue_comment_update), - ) - .route( - "/issues/{number}/comments/{comment_id}", - web::delete().to(comment::issue_comment_delete), - ) - .route( - "/issues/{number}/comments/{comment_id}/reactions", - web::get().to(comment_reaction::issue_comment_reaction_list), - ) - .route( - "/issues/{number}/comments/{comment_id}/reactions", - web::post().to(comment_reaction::issue_comment_reaction_add), - ) - .route( - "/issues/{number}/comments/{comment_id}/reactions/{reaction}", - web::delete().to(comment_reaction::issue_comment_reaction_remove), - ) - .route( - "/issues/{number}/assignees", - web::get().to(assignee::issue_assignee_list), - ) - .route( - "/issues/{number}/assignees", - web::post().to(assignee::issue_assignee_add), - ) - .route( - "/issues/{number}/assignees/{assignee_id}", - web::delete().to(assignee::issue_assignee_remove), - ) - .route( - "/issues/{number}/subscribers", - web::get().to(subscriber::issue_subscriber_list), - ) - .route( - "/issues/{number}/subscribe", - web::post().to(subscriber::issue_subscribe), - ) - .route( - "/issues/{number}/subscribe", - web::delete().to(subscriber::issue_unsubscribe), - ) - .route( - "/issues/{number}/reactions", - web::get().to(reaction::issue_reaction_list), - ) - .route( - "/issues/{number}/reactions", - web::post().to(reaction::issue_reaction_add), - ) - .route( - "/issues/{number}/reactions/{reaction}", - web::delete().to(reaction::issue_reaction_remove), - ) - .route( - "/issues/{number}/repos", - web::get().to(repo::issue_repo_list), - ) - .route( - "/issues/{number}/repos", - web::post().to(repo::issue_repo_link), - ) - .route( - "/issues/{number}/repos/{repo_id}", - web::delete().to(repo::issue_repo_unlink), - ) - .route( - "/issues/{number}/pulls", - web::get().to(pull_request::issue_pull_request_list), - ) - .route( - "/issues/{number}/pulls", - web::post().to(pull_request::issue_pull_request_link), - ) - .route( - "/issues/{number}/pulls/{repo_id}/{pr_number}", - web::delete().to(pull_request::issue_pull_request_unlink), - ) - // labels (at project level) - .route("/labels", web::get().to(label::label_list)) - .route("/labels", web::post().to(label::label_create)) - .route("/labels/{label_id}", web::delete().to(label::label_delete)), - ); -} diff --git a/libs/api/issue/pull_request.rs b/libs/api/issue/pull_request.rs deleted file mode 100644 index a173283..0000000 --- a/libs/api/issue/pull_request.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/pulls", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "List issue pull requests", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_pull_request_list( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_pull_request_list(project, issue_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/pulls", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - request_body = service::issue::IssueLinkPullRequestRequest, - responses( - (status = 200, description = "Link pull request to issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_pull_request_link( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_pull_request_link(project, issue_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}/pulls/{repo_id}/{pr_number}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("repo_id" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Unlink pull request from issue"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_pull_request_unlink( - service: web::Data, - session: Session, - path: web::Path<(String, i64, String, i64)>, -) -> Result { - let (project, issue_number, repo_id, pr_number) = path.into_inner(); - let repo_uuid = uuid::Uuid::parse_str(&repo_id) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - service - .issue_pull_request_unlink(project, issue_number, repo_uuid, pr_number, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/issue/reaction.rs b/libs/api/issue/reaction.rs deleted file mode 100644 index 364aa25..0000000 --- a/libs/api/issue/reaction.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/reactions", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "List issue reactions", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_reaction_list( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_reaction_list(project, issue_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/reactions", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - request_body = service::issue::ReactionAddRequest, - responses( - (status = 200, description = "Add reaction to issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_reaction_add( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_reaction_add(project, issue_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}/reactions/{reaction}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("reaction" = String, Path), - ), - responses( - (status = 200, description = "Remove reaction from issue"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_reaction_remove( - service: web::Data, - session: Session, - path: web::Path<(String, i64, String)>, -) -> Result { - let (project, issue_number, reaction) = path.into_inner(); - service - .issue_reaction_remove(project, issue_number, reaction, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/issue/repo.rs b/libs/api/issue/repo.rs deleted file mode 100644 index eb321f6..0000000 --- a/libs/api/issue/repo.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/repos", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "List issue repos", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_repo_list( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_repo_list(project, issue_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/repos", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - request_body = service::issue::IssueLinkRepoRequest, - responses( - (status = 200, description = "Link repo to issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_repo_link( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_repo_link(project, issue_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}/repos/{repo_id}", - params( - ("project" = String, Path), - ("number" = i64, Path), - ("repo_id" = String, Path), - ), - responses( - (status = 200, description = "Unlink repo from issue"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_repo_unlink( - service: web::Data, - session: Session, - path: web::Path<(String, i64, String)>, -) -> Result { - let (project, issue_number, repo_id) = path.into_inner(); - let repo_uuid = uuid::Uuid::parse_str(&repo_id) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - service - .issue_repo_unlink(project, issue_number, repo_uuid, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/issue/subscriber.rs b/libs/api/issue/subscriber.rs deleted file mode 100644 index 4731c08..0000000 --- a/libs/api/issue/subscriber.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/issue/{project}/issues/{number}/subscribers", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "List issue subscribers", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_subscriber_list( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_subscriber_list(project, issue_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/issue/{project}/issues/{number}/subscribe", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Subscribe to issue", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_subscribe( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, issue_number) = path.into_inner(); - let resp = service - .issue_subscribe(project, issue_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/issue/{project}/issues/{number}/subscribe", - params( - ("project" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Unsubscribe from issue"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Issues" -)] -pub async fn issue_unsubscribe( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project, issue_number) = path.into_inner(); - service - .issue_unsubscribe(project, issue_number, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/lib.rs b/libs/api/lib.rs deleted file mode 100644 index 6cbce97..0000000 --- a/libs/api/lib.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub mod agent; -pub mod auth; -pub mod chat; -pub mod dist; -pub mod error; -pub mod git; -pub mod issue; -pub mod openapi; -pub mod project; -pub mod pull_request; -pub mod robots; -pub mod room; -pub mod route; -pub mod search; -pub mod sidemap; -pub mod skill; -pub mod user; - -// Auto-generated frontend module (from build.rs) serving embedded dist/ assets -#[allow(dead_code)] -mod frontend; - -pub use error::{ApiError, ApiResponse, api_success}; diff --git a/libs/api/openapi.rs b/libs/api/openapi.rs deleted file mode 100644 index 468a10b..0000000 --- a/libs/api/openapi.rs +++ /dev/null @@ -1,769 +0,0 @@ -#![allow(unused_imports, dead_code)] -//! OpenAPI 3.0 specification for the entire API surface. -//! -//! This module aggregates all `#[utoipa::path]` annotated handlers from every -//! API module and all `#[derive(utoipa::ToSchema)]` types used in request / -//! response bodies, so that `utoipa` can produce a single `openapi.json`. - -use utoipa::OpenApi; - -// Pull request query type defined in api::pull_request - -// Room query types defined in api::room - -#[derive(OpenApi)] -#[openapi( - paths( - // Auth - crate::auth::login::api_auth_login, - crate::auth::register::api_auth_register, - crate::auth::logout::api_auth_logout, - crate::auth::captcha::api_auth_captcha, - crate::auth::me::api_auth_me, - crate::auth::password::api_user_change_password, - crate::auth::password::api_user_request_password_reset, - crate::auth::password::api_user_confirm_password_reset, - crate::auth::totp::api_2fa_enable, - crate::auth::totp::api_2fa_verify, - crate::auth::totp::api_2fa_disable, - crate::auth::totp::api_2fa_status, - crate::auth::email::api_email_get, - crate::auth::email::api_email_change, - crate::auth::email::api_email_verify, - // Agent - // Agent (CRUD) - crate::agent::code_review::trigger_code_review, - crate::agent::issue_triage::triage_issue, - crate::agent::pr_summary::generate_pr_description, - crate::agent::provider::provider_list, - crate::agent::provider::provider_get, - crate::agent::provider::provider_create, - crate::agent::provider::provider_update, - crate::agent::provider::provider_delete, - crate::agent::model::model_list, - crate::agent::model::model_catalog, - crate::agent::model::model_get, - crate::agent::model::model_create, - crate::agent::model::model_update, - crate::agent::model::model_delete, - crate::agent::model_version::model_version_list, - crate::agent::model_version::model_version_get, - crate::agent::model_version::model_version_create, - crate::agent::model_version::model_version_update, - crate::agent::model_version::model_version_delete, - crate::agent::model_pricing::model_pricing_list, - crate::agent::model_pricing::model_pricing_get, - crate::agent::model_pricing::model_pricing_create, - crate::agent::model_pricing::model_pricing_update, - crate::agent::model_pricing::model_pricing_delete, - crate::agent::model_capability::model_capability_list, - crate::agent::model_capability::model_capability_get, - crate::agent::model_capability::model_capability_create, - crate::agent::model_capability::model_capability_update, - crate::agent::model_capability::model_capability_delete, - crate::agent::model_parameter_profile::model_parameter_profile_list, - crate::agent::model_parameter_profile::model_parameter_profile_get, - crate::agent::model_parameter_profile::model_parameter_profile_create, - crate::agent::model_parameter_profile::model_parameter_profile_update, - crate::agent::model_parameter_profile::model_parameter_profile_delete, - // Git init (top-level) - crate::git::init::git_init_bare, - crate::git::init::git_open, - crate::git::init::git_open_bare, - crate::git::init::git_is_repo, - // Git archive - crate::git::archive::git_archive, - crate::git::archive::git_archive_list, - crate::git::archive::git_archive_summary, - crate::git::archive::git_archive_cached, - crate::git::archive::git_archive_invalidate, - crate::git::archive::git_archive_invalidate_all, - // Git blame - crate::git::blame::git_blame_file, - // Git blob - crate::git::blob::git_readme, - crate::git::blob::git_blob_create, - crate::git::blob::git_blob_get, - crate::git::blob::git_blob_exists, - crate::git::blob::git_blob_is_binary, - crate::git::blob::git_blob_content, - crate::git::blob::git_blob_size, - // Git branch - crate::git::branch::git_branch_list, - crate::git::branch::git_branch_summary, - crate::git::branch::git_branch_create, - crate::git::branch::git_branch_get, - crate::git::branch::git_branch_delete, - crate::git::branch::git_branch_current, - crate::git::branch::git_branch_exists, - crate::git::branch::git_branch_is_head, - crate::git::branch::git_branch_upstream, - crate::git::branch::git_branch_tracking_difference, - crate::git::branch::git_branch_delete_remote, - crate::git::branch::git_branch_rename, - crate::git::branch::git_branch_move, - crate::git::branch::git_branch_set_upstream, - crate::git::branch::git_branch_diff, - crate::git::branch::git_branch_is_detached, - crate::git::branch::git_branch_is_merged, - crate::git::branch::git_branch_merge_base, - crate::git::branch::git_branch_is_ancestor, - crate::git::branch::git_branch_fast_forward, - crate::git::branch::git_branch_is_conflicted, - // Git commit - crate::git::commit::git_commit_log, - crate::git::commit::git_commit_count, - crate::git::commit::git_commit_create, - crate::git::commit::git_commit_graph, - crate::git::commit::git_commit_graph_react, - crate::git::commit::git_commit_walk, - crate::git::commit::git_commit_resolve_rev, - crate::git::commit::git_commit_get, - crate::git::commit::git_commit_amend, - crate::git::commit::git_commit_exists, - crate::git::commit::git_commit_is_commit, - crate::git::commit::git_commit_message, - crate::git::commit::git_commit_summary, - crate::git::commit::git_commit_short_id, - crate::git::commit::git_commit_author, - crate::git::commit::git_commit_tree_id, - crate::git::commit::git_commit_parent_count, - crate::git::commit::git_commit_parent_ids, - crate::git::commit::git_commit_parent, - crate::git::commit::git_commit_first_parent, - crate::git::commit::git_commit_is_merge, - crate::git::commit::git_commit_refs, - crate::git::commit::git_commit_branches, - crate::git::commit::git_commit_tags, - crate::git::commit::git_commit_is_tip, - crate::git::commit::git_commit_ref_count, - crate::git::commit::git_commit_reflog, - crate::git::commit::git_commit_ancestors, - crate::git::commit::git_commit_descendants, - crate::git::commit::git_commit_cherry_pick, - crate::git::commit::git_commit_cherry_pick_abort, - crate::git::commit::git_commit_revert, - crate::git::commit::git_commit_revert_abort, - // Git contributors - crate::git::contributors::git_contributors, - // Git diff - crate::git::diff::git_diff_tree_to_tree, - crate::git::diff::git_diff_commit_to_workdir, - crate::git::diff::git_diff_commit_to_index, - crate::git::diff::git_diff_workdir_to_index, - crate::git::diff::git_diff_index_to_tree, - crate::git::diff::git_diff_stats, - crate::git::diff::git_diff_patch_id, - crate::git::diff::git_diff_side_by_side, - // Git refs - crate::git::refs::git_ref_list, - crate::git::refs::git_ref_create, - crate::git::refs::git_ref_get, - crate::git::refs::git_ref_delete, - crate::git::refs::git_ref_rename, - crate::git::refs::git_ref_update, - crate::git::refs::git_ref_exists, - crate::git::refs::git_ref_target, - // Git repo - crate::git::repo::git_description_get, - crate::git::repo::git_description_set, - crate::git::repo::git_description_reset, - crate::git::repo::git_description_exists, - crate::git::repo::git_update_repo, - crate::git::repo::git_config_entries, - crate::git::repo::git_config_get, - crate::git::repo::git_config_set, - crate::git::repo::git_config_delete, - crate::git::repo::git_config_has, - crate::git::repo::git_merge_analysis, - crate::git::repo::git_merge_analysis_for_ref, - crate::git::repo::git_merge_base, - crate::git::repo::git_merge_commits, - crate::git::repo::git_merge_trees, - crate::git::repo::git_merge_abort, - crate::git::repo::git_merge_is_in_progress, - crate::git::repo::git_mergehead_list, - crate::git::repo::git_merge_is_conflicted, - // Git star - crate::git::star::git_star, - crate::git::star::git_unstar, - crate::git::star::git_is_starred, - crate::git::star::git_star_count, - crate::git::star::git_star_user_list, - // Git branch protection - crate::git::branch_protection::branch_protection_list, - crate::git::branch_protection::branch_protection_get, - crate::git::branch_protection::branch_protection_create, - crate::git::branch_protection::branch_protection_update, - crate::git::branch_protection::branch_protection_delete, - crate::git::branch_protection::branch_protection_check_approvals, - // Git tag - crate::git::tag::git_tag_list, - crate::git::tag::git_tag_list_names, - crate::git::tag::git_tag_summary, - crate::git::tag::git_tag_count, - crate::git::tag::git_tag_create, - crate::git::tag::git_tag_create_lightweight, - crate::git::tag::git_tag_rename, - crate::git::tag::git_tag_update_message, - crate::git::tag::git_tag_get, - crate::git::tag::git_tag_delete, - crate::git::tag::git_tag_exists, - crate::git::tag::git_tag_target, - crate::git::tag::git_tag_is_annotated, - crate::git::tag::git_tag_message, - crate::git::tag::git_tag_tagger, - // Git tree - crate::git::tree::git_tree_get, - crate::git::tree::git_tree_exists, - crate::git::tree::git_tree_list, - crate::git::tree::git_tree_entry, - crate::git::tree::git_tree_entry_by_path, - crate::git::tree::git_tree_entry_by_commit_path, - crate::git::tree::git_tree_entry_count, - crate::git::tree::git_tree_is_empty, - crate::git::tree::git_tree_diffstats, - // Git watch - crate::git::watch::git_watch, - crate::git::watch::git_unwatch, - crate::git::watch::git_is_watched, - crate::git::watch::git_watch_count, - crate::git::watch::git_watch_user_list, - // Git webhook - crate::git::webhook::git_webhook_list, - crate::git::webhook::git_webhook_create, - crate::git::webhook::git_webhook_get, - crate::git::webhook::git_webhook_update, - crate::git::webhook::git_webhook_delete, - // Issue - crate::issue::issue_list, - crate::issue::issue_get, - crate::issue::issue_create, - crate::issue::issue_update, - crate::issue::issue_close, - crate::issue::issue_reopen, - crate::issue::issue_delete, - crate::issue::issue_summary, - crate::issue::issue_label::issue_label_list, - crate::issue::issue_label::issue_label_add, - crate::issue::issue_label::issue_label_add_bulk, - crate::issue::issue_label::issue_label_remove, - crate::issue::label::label_list, - crate::issue::label::label_create, - crate::issue::label::label_delete, - crate::issue::comment::issue_comment_list, - crate::issue::comment::issue_comment_get, - crate::issue::comment::issue_comment_create, - crate::issue::comment::issue_comment_update, - crate::issue::comment::issue_comment_delete, - crate::issue::comment_reaction::issue_comment_reaction_list, - crate::issue::comment_reaction::issue_comment_reaction_add, - crate::issue::comment_reaction::issue_comment_reaction_remove, - crate::issue::assignee::issue_assignee_list, - crate::issue::assignee::issue_assignee_add, - crate::issue::assignee::issue_assignee_remove, - crate::issue::subscriber::issue_subscriber_list, - crate::issue::subscriber::issue_subscribe, - crate::issue::subscriber::issue_unsubscribe, - crate::issue::reaction::issue_reaction_list, - crate::issue::reaction::issue_reaction_add, - crate::issue::reaction::issue_reaction_remove, - crate::issue::repo::issue_repo_list, - crate::issue::repo::issue_repo_link, - crate::issue::repo::issue_repo_unlink, - crate::issue::pull_request::issue_pull_request_list, - crate::issue::pull_request::issue_pull_request_link, - crate::issue::pull_request::issue_pull_request_unlink, - // Project - crate::project::init::project_create, - crate::project::info::project_info, - crate::project::repo::project_repos, - crate::project::repo::project_repo_create, - crate::project::members::project_members, - crate::project::members::project_members_grouped, - crate::project::members::project_update_member_role, - crate::project::members::project_remove_member, - crate::project::members::project_role_priorities, - crate::project::members::project_upsert_role_priority, - crate::project::members::project_delete_role_priority, - crate::project::labels::project_labels, - crate::project::labels::project_create_label, - crate::project::labels::project_get_label, - crate::project::labels::project_update_label, - crate::project::labels::project_delete_label, - crate::project::like::project_like, - crate::project::like::project_unlike, - crate::project::like::project_is_like, - crate::project::like::project_likes_count, - crate::project::like::project_like_users, - crate::project::watch::project_watch, - crate::project::watch::project_unwatch, - crate::project::watch::project_is_watch, - crate::project::watch::project_watches_count, - crate::project::watch::project_watch_users, - // Boards - crate::project::board::board_list, - crate::project::board::board_get, - crate::project::board::board_create, - crate::project::board::board_update, - crate::project::board::board_delete, - crate::project::board::column_create, - crate::project::board::column_update, - crate::project::board::column_delete, - crate::project::board::card_create, - crate::project::board::card_update, - crate::project::board::card_move, - crate::project::board::card_delete, - crate::project::settings::project_exchange_name, - crate::project::settings::project_exchange_visibility, - crate::project::settings::project_exchange_title, - crate::project::audit::project_audit_logs, - crate::project::audit::project_audit_log, - crate::project::audit::project_log_audit, - crate::project::activity::project_activities, - crate::project::activity::project_log_activity, - crate::project::message_favorite::project_message_favorites, - crate::project::message_favorite::project_message_favorite_add, - crate::project::message_favorite::project_message_favorite_remove, - crate::project::stats::project_stats, - crate::project::billing::project_billing, - crate::project::billing::project_billing_history, - crate::project::billing::project_billing_errors, - crate::project::invitation::project_my_invitations, - crate::project::invitation::project_invitations, - crate::project::invitation::project_invite_user, - crate::project::invitation::project_accept_invitation, - crate::project::invitation::project_reject_invitation, - crate::project::invitation::project_cancel_invitation, - crate::project::join_settings::project_join_settings, - crate::project::join_settings::project_update_join_settings, - crate::project::join_request::project_my_join_requests, - crate::project::join_request::project_join_requests, - crate::project::join_request::project_submit_join_request, - crate::project::join_request::project_process_join_request, - crate::project::join_request::project_cancel_join_request, - crate::project::join_answers::project_join_answers, - crate::project::join_answers::project_submit_join_answers, - crate::project::transfer_repo::project_transfer_repo, - // Pull request - crate::pull_request::pull_request::pull_request_list, - crate::pull_request::pull_request::pull_request_get, - crate::pull_request::pull_request::pull_request_create, - crate::pull_request::pull_request::pull_request_update, - crate::pull_request::pull_request::pull_request_delete, - crate::pull_request::pull_request::pull_request_close, - crate::pull_request::pull_request::pull_request_reopen, - crate::pull_request::pull_request::pull_request_summary, - crate::pull_request::pull_request::review_list, - crate::pull_request::pull_request::review_submit, - crate::pull_request::pull_request::review_update, - crate::pull_request::pull_request::review_delete, - crate::pull_request::pull_request::review_comment_list, - crate::pull_request::pull_request::review_comment_create, - crate::pull_request::pull_request::review_comment_update, - crate::pull_request::pull_request::review_comment_delete, - crate::pull_request::pull_request::pr_diff_side_by_side, - crate::pull_request::pull_request::pr_commits_list, - crate::pull_request::review_comment::review_comment_resolve, - crate::pull_request::review_comment::review_comment_unresolve, - crate::pull_request::review_comment::review_comment_reply, - crate::pull_request::review_request::review_request_list, - crate::pull_request::review_request::review_request_create, - crate::pull_request::review_request::review_request_delete, - crate::pull_request::review_request::review_request_dismiss, - crate::pull_request::merge::merge_analysis, - crate::pull_request::merge::merge_conflict_check, - crate::pull_request::merge::merge_execute, - crate::pull_request::merge::merge_abort, - crate::pull_request::merge::merge_is_in_progress, - // Room - crate::room::room::room_list, - crate::room::room::room_get, - crate::room::room::room_create, - crate::room::room::room_update, - crate::room::room::room_delete, - crate::room::room::project_presence, - crate::room::category::category_list, - crate::room::category::category_create, - crate::room::category::category_update, - crate::room::category::category_delete, - crate::room::message::message_list, - crate::room::message::message_create, - crate::room::message::message_update, - crate::room::message::message_revoke, - crate::room::message::message_get, - crate::room::thread::thread_list, - crate::room::thread::thread_create, - crate::room::thread::thread_messages, - crate::room::member::participant_list, - crate::room::member::access_grant, - crate::room::member::access_revoke, - crate::room::member::state_set_read_seq, - crate::room::member::state_update_dnd, - crate::room::pin::pin_list, - crate::room::pin::pin_add, - crate::room::pin::pin_remove, - crate::room::ai::ai_list, - crate::room::ai::ai_upsert, - crate::room::ai::ai_delete, - crate::room::notification::notification_list, - crate::room::notification::notification_mark_read, - crate::room::notification::notification_mark_all_read, - crate::room::notification::notification_archive, - crate::room::draft_and_history::message_edit_history, - crate::room::draft_and_history::mention_list, - crate::room::draft_and_history::mention_read_all, - // Search - crate::search::service::search, - crate::search::service::search_messages, - crate::room::reaction::message_search, - // User - crate::user::profile::get_my_profile, - crate::user::profile::update_my_profile, - crate::user::profile::get_profile_by_username, - crate::user::avatar::upload_avatar, - crate::user::preferences::get_preferences, - crate::user::preferences::update_preferences, - crate::user::billing::user_billing, - crate::user::billing::user_billing_errors, - crate::user::billing::user_billing_history, - crate::user::ssh_key::add_ssh_key, - crate::user::ssh_key::list_ssh_keys, - crate::user::ssh_key::get_ssh_key, - crate::user::ssh_key::update_ssh_key, - crate::user::ssh_key::delete_ssh_key, - crate::user::access_key::create_access_key, - crate::user::access_key::list_access_keys, - crate::user::access_key::delete_access_key, - crate::user::notification::get_notification_preferences, - crate::user::notification::update_notification_preferences, - crate::user::chpc::get_my_contribution_heatmap, - crate::user::chpc::get_contribution_heatmap, - crate::user::projects::get_current_user_projects, - crate::user::projects::get_user_projects, - crate::user::repository::get_current_user_repos, - crate::user::repository::get_user_repos, - crate::user::subscribe::subscribe_target, - crate::user::subscribe::unsubscribe_target, - crate::user::subscribe::is_subscribed_to_target, - crate::user::subscribe::get_subscribers, - crate::user::subscribe::get_subscription_count, - crate::user::subscribe::get_subscriber_count, - crate::user::subscribe::get_following_list, - crate::user::summary::get_user_summary, - crate::user::user_activity::get_user_activity, - crate::user::stars::get_user_stars, - crate::user::user_info::get_user_info, - // Skill - crate::skill::skill_list, - crate::skill::skill_get, - crate::skill::skill_create, - crate::skill::skill_update, - crate::skill::skill_delete, - crate::skill::skill_scan, - // AI Chat - crate::chat::handlers::conversation::conversation_create, - crate::chat::handlers::conversation::conversation_list, - crate::chat::handlers::conversation::conversation_get, - crate::chat::handlers::conversation::conversation_update, - crate::chat::handlers::conversation::conversation_delete, - crate::chat::handlers::message::message_list, - crate::chat::handlers::message::message_create, - crate::chat::handlers::message::message_get, - crate::chat::handlers::message::message_stop, - crate::chat::handlers::message::message_resend, - crate::chat::handlers::message::message_children, - crate::chat::handlers::message::message_stream, - crate::chat::handlers::fork::message_fork, - crate::chat::handlers::share::conversation_share, - crate::chat::handlers::share::shared_conversation_get, - ), - components( - schemas( - // Core API types - crate::error::ApiError, - // Pager - service::Pager, - // Issue - service::issue::IssueCreateRequest, - service::issue::IssueUpdateRequest, - service::issue::IssueResponse, - service::issue::IssueListResponse, - service::issue::IssueSummaryResponse, - service::issue::IssueCommentCreateRequest, - service::issue::IssueCommentUpdateRequest, - service::issue::IssueCommentResponse, - service::issue::IssueCommentListResponse, - service::issue::IssueLabelResponse, - service::issue::IssueAddLabelRequest, - service::issue::IssueAddLabelsByNamesRequest, - service::issue::LabelResponse, - service::issue::CreateLabelRequest, - service::issue::ReactionAddRequest, - service::issue::ReactionListResponse, - service::issue::ReactionResponse, - service::issue::IssueAssignUserRequest, - service::issue::IssueAssigneeResponse, - service::issue::IssueSubscriberResponse, - service::issue::IssueRepoResponse, - service::issue::IssueLinkRepoRequest, - service::issue::IssuePullRequestResponse, - service::issue::IssueLinkPullRequestRequest, - // Pull request - service::pull_request::PullRequestCreateRequest, - service::pull_request::PullRequestUpdateRequest, - service::pull_request::PullRequestResponse, - service::pull_request::PullRequestListResponse, - service::pull_request::PullRequestSummaryResponse, - service::pull_request::PrCommitsListResponse, - service::pull_request::PrCommitResponse, - service::pull_request::ReviewSubmitRequest, - service::pull_request::ReviewUpdateRequest, - service::pull_request::ReviewResponse, - service::pull_request::ReviewListResponse, - service::pull_request::ReviewCommentCreateRequest, - service::pull_request::ReviewCommentUpdateRequest, - service::pull_request::ReviewCommentResponse, - service::pull_request::ReviewCommentListResponse, - service::pull_request::ReviewCommentListQuery, - service::pull_request::ReviewCommentReplyRequest, - service::pull_request::ReviewRequestCreateRequest, - service::pull_request::ReviewRequestResponse, - service::pull_request::ReviewRequestListResponse, - service::git::diff::SideBySideDiffResponse, - service::git::diff::SideBySideDiffQuery, - service::pull_request::MergeAnalysisResponse, - service::pull_request::MergeConflictResponse, - service::pull_request::MergeRequest, - service::pull_request::MergeResponse, - // Git branch protection - service::git::branch_protection::BranchProtectionResponse, - service::git::branch_protection::BranchProtectionCreateRequest, - service::git::branch_protection::BranchProtectionUpdateRequest, - service::git::branch_protection::ApprovalCheckResult, - service::git::branch_protection::ReviewerInfo, - // Project - service::project::init::ProjectInitParams, - service::project::init::ProjectInitResponse, - service::project::info::ProjectInfoRelational, - service::project::repo::ProjectRepositoryPagination, - service::project::repo::ProjectRepositoryItem, - service::project::repo::ProjectRepoCreateParams, - service::project::repo::ProjectRepoCreateResponse, - service::project::members::MemberListResponse, - service::project::members::GroupedMemberListResponse, - service::project::members::UpdateMemberRoleRequest, - service::project::members::RolePriorityListResponse, - service::project::members::RolePriorityInfo, - service::project::members::UpsertRolePriorityRequest, - service::project::labels::LabelListResponse, - service::project::labels::LabelResponse, - service::project::labels::CreateLabelParams, - service::project::labels::UpdateLabelParams, - service::project::like::LikeUserInfo, - service::project::watch::WatchUserInfo, - service::project::audit::AuditLogResponse, - service::project::audit::AuditLogParams, - service::project::activity::ActivityLogResponse, - service::project::activity::ActivityLogParams, - service::project::activity::ActivityLogListResponse, - service::project::message_favorite::ProjectMessageFavoriteQuery, - service::project::message_favorite::ProjectMessageFavoriteItem, - service::project::message_favorite::ProjectMessageFavoriteResponse, - service::project::stats::ProjectStatsResponse, - service::project::stats::ActivityBreakdownItem, - service::project::stats::ProjectStatsActivityItem, - // Skill - service::skill::info::SkillResponse, - service::skill::manage::CreateSkillRequest, - service::skill::manage::UpdateSkillRequest, - service::skill::manage::DeleteSkillResponse, - crate::skill::ScanResponse, - // Boards - service::project::board::BoardResponse, - service::project::board::BoardWithColumnsResponse, - service::project::board::ColumnResponse, - service::project::board::ColumnWithCardsResponse, - service::project::board::CardResponse, - service::project::board::CreateBoardParams, - service::project::board::UpdateBoardParams, - service::project::board::CreateColumnParams, - service::project::board::UpdateColumnParams, - service::project::board::CreateCardParams, - service::project::board::UpdateCardParams, - service::project::board::MoveCardParams, - service::project::billing::ProjectBillingCurrentResponse, - service::project::billing::ProjectBillingHistoryResponse, - service::project::billing::ProjectBillingHistoryQuery, - service::project::billing::BillingErrorItem, - service::project::billing::BillingErrorsResponse, - service::project::invitation::InvitationListResponse, - service::project::join_settings::JoinSettingsResponse, - service::project::join_settings::UpdateJoinSettingsRequest, - service::project::join_request::JoinRequestListResponse, - service::project::join_request::SubmitJoinRequest, - service::project::join_request::ProcessJoinRequest, - service::project::join_answers::JoinAnswersListResponse, - service::project::join_answers::AnswerRequest, - service::project::transfer_repo::TransferRepoParams, - service::project::transfer_repo::TransferRepoResponse, - // Agent - service::agent::sync::SyncModelsResponse, - service::agent::code_review::TriggerCodeReviewRequest, - service::agent::code_review::TriggerCodeReviewResponse, - service::agent::code_review::CommentCreated, - service::agent::issue_triage::IssueTriageSuggestion, - service::agent::issue_triage::IssueTriageResponse, - service::agent::pr_summary::GeneratePrDescriptionRequest, - service::agent::pr_summary::GeneratePrDescriptionResponse, - service::agent::provider::ProviderResponse, - service::agent::provider::CreateProviderRequest, - service::agent::provider::UpdateProviderRequest, - service::agent::model::ModelResponse, - service::agent::model::ModelWithPricingResponse, - service::agent::model::ModelListResponse, - service::agent::model::CreateModelRequest, - service::agent::model::UpdateModelRequest, - service::agent::model_version::ModelVersionResponse, - service::agent::model_version::CreateModelVersionRequest, - service::agent::model_version::UpdateModelVersionRequest, - service::agent::model_pricing::ModelPricingResponse, - service::agent::model_pricing::CreateModelPricingRequest, - service::agent::model_pricing::UpdateModelPricingRequest, - service::agent::model_capability::ModelCapabilityResponse, - service::agent::model_capability::CreateModelCapabilityRequest, - service::agent::model_capability::UpdateModelCapabilityRequest, - service::agent::model_parameter_profile::ModelParameterProfileResponse, - service::agent::model_parameter_profile::CreateModelParameterProfileRequest, - service::agent::model_parameter_profile::UpdateModelParameterProfileRequest, - // User - service::user::profile::ProfileResponse, - service::user::profile::UpdateProfileParams, - crate::user::avatar::AvatarUploadResponse, - service::user::preferences::PreferencesResponse, - service::user::preferences::PreferencesParams, - service::user::ssh_key::SshKeyResponse, - service::user::ssh_key::SshKeyListResponse, - service::user::ssh_key::AddSshKeyParams, - service::user::ssh_key::UpdateSshKeyParams, - service::user::access_key::AccessKeyResponse, - service::user::access_key::AccessKeyListResponse, - service::user::access_key::CreateAccessKeyParams, - service::user::notification::NotificationPreferencesResponse, - service::user::notification::NotificationPreferencesParams, - service::user::chpc::ContributionHeatmapResponse, - service::user::chpc::ContributionHeatmapQuery, - service::user::projects::UserProjectsResponse, - service::user::projects::UserProjectsQuery, - service::user::repository::UserReposResponse, - service::user::repository::UserReposQuery, - service::user::subscribe::SubscriptionInfo, - service::user::subscribe::UserCard, - service::user::summary::UserSummaryResponse, - service::user::user_activity::UserActivityItem, - service::user::user_activity::UserActivityResponse, - service::user::stars::RepoStarItem, - service::user::stars::ProjectFollowItem, - service::user::stars::UserStarsResponse, - service::user::user_info::UserInfoExternal, - service::user::billing::UserBillingResponse, - service::user::billing::UserBillingErrorsResponse, - service::user::billing::UserBillingErrorItem, - service::user::billing::UserBillingHistoryQuery, - service::user::billing::UserBillingHistoryItem, - service::user::billing::UserBillingHistoryResponse, - // Room - room::RoomResponse, - room::RoomCreateRequest, - room::RoomUpdateRequest, - room::RoomCategoryResponse, - room::RoomCategoryCreateRequest, - room::RoomCategoryUpdateRequest, - room::RoomParticipantResponse, - room::RoomParticipantListResponse, - room::RoomAccessGrantRequest, - room::RoomAccessRevokeRequest, - room::RoomMemberReadSeqRequest, - room::RoomUserStateUpdateDndRequest, - room::RoomUserStateResponse, - room::RoomMessageResponse, - room::RoomMessageCreateRequest, - room::RoomMessageUpdateRequest, - room::RoomMessageListResponse, - room::RoomThreadResponse, - room::RoomThreadCreateRequest, - room::RoomPinResponse, - room::RoomAiResponse, - room::RoomAiUpsertRequest, - room::NotificationResponse, - room::NotificationListResponse, - room::NotificationType, - // Presence - room::presence::PresenceChanged, - room::presence::PresenceStatus, - // Auth service types - service::auth::login::LoginParams, - service::auth::register::RegisterParams, - service::auth::password::ChangePasswordParams, - service::auth::password::ConfirmResetPasswordParams, - service::auth::password::ResetPasswordParams, - service::auth::captcha::CaptchaQuery, - service::auth::captcha::CaptchaResponse, - service::auth::me::ContextMe, - service::auth::totp::Enable2FAResponse, - service::auth::totp::Verify2FAParams, - service::auth::totp::Disable2FAParams, - service::auth::totp::Get2FAStatusResponse, - service::auth::email::EmailChangeRequest, - service::auth::email::EmailVerifyRequest, - service::auth::email::EmailResponse, - // Git init - service::git::init::GitInitRequest, - service::git::init::GitInitResponse, - // Git blob - service::git::blob::GitReadmeQuery, - service::git::blob::GitReadmeResponse, - // Git webhook - service::git::webhook::WebhookEvent, - service::git::webhook::CreateWebhookParams, - service::git::webhook::UpdateWebhookParams, - service::git::webhook::WebhookResponse, - service::git::webhook::WebhookListResponse, - // Search - service::search::SearchResponse, - service::search::SearchResultSet, - service::search::SearchResultSet, - service::search::SearchResultSet, - service::search::SearchResultSet, - service::search::ProjectSearchItem, - service::search::RepoSearchItem, - service::search::IssueSearchItem, - service::search::UserSearchItem, - service::search::GlobalMessageSearchResponse, - service::search::GlobalMessageSearchItem, - // AI Chat - crate::chat::handlers::types::CreateConversationParams, - crate::chat::handlers::types::UpdateConversationParams, - crate::chat::handlers::types::ConversationResponse, - crate::chat::handlers::types::CreateMessageParams, - crate::chat::handlers::types::MessageContent, - crate::chat::handlers::types::MessageResponse, - crate::chat::handlers::types::ShareResponse, - crate::chat::handlers::fork::ForkConversationResponse, - ) - ), - tags( - (name = "Auth", description = "Authentication and user identity"), - (name = "Agent", description = "AI agent model management"), - (name = "Git", description = "Git repository operations"), - (name = "Issues", description = "Issue tracking"), - (name = "Project", description = "Project management"), - (name = "PullRequest", description = "Pull request management"), - (name = "Room", description = "Real-time chat rooms"), - (name = "Presence", description = "User presence and online status"), - (name = "Search", description = "Global and room message search"), - (name = "User", description = "User profiles and settings"), - (name = "AI Chat", description = "AI conversation and messaging"), - ) -)] -pub struct OpenApiDoc; diff --git a/libs/api/project/activity.rs b/libs/api/project/activity.rs deleted file mode 100644 index 6b2a473..0000000 --- a/libs/api/project/activity.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::error::AppError; -use service::project::activity::{ActivityLogListResponse, ActivityLogParams, ActivityLogResponse}; -use session::Session; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct ActivityQuery { - pub page: Option, - pub per_page: Option, - pub event_type: Option, - pub start_date: Option, - pub end_date: Option, -} - -impl From for service::project::activity::ActivityParams { - fn from(q: ActivityQuery) -> Self { - service::project::activity::ActivityParams { - event_type: q.event_type, - start_date: q.start_date, - end_date: q.end_date, - } - } -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/activities", - params( - ("project_name" = String, Path), - ("page" = Option, Query), - ("per_page" = Option, Query), - ("event_type" = Option, Query), - ("start_date" = Option, Query, description = "ISO 8601 datetime, e.g. 2025-01-01T00:00:00Z"), - ("end_date" = Option, Query, description = "ISO 8601 datetime, e.g. 2025-12-31T23:59:59Z"), - ), - responses( - (status = 200, description = "List project activities", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden — no access to this project"), - (status = 404, description = "Project not found"), - ), - tag = "Project" -)] -pub async fn project_activities( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let page = query.page; - let per_page = query.per_page; - let params = service::project::activity::ActivityParams::from(query.into_inner()); - let resp = service - .project_get_activities(project_name, page, per_page, Some(params), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/activities", - params(("project_name" = String, Path)), - request_body = ActivityLogParams, - responses( - (status = 200, description = "Activity logged", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Project not found"), - ), - tag = "Project" -)] -pub async fn project_log_activity( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let _project = service.utils_find_project_by_name(project_name).await?; - let user_uid = session.user().ok_or(AppError::Unauthorized)?; - - let resp = service - .project_log_activity(_project.id, body.repo_id, user_uid, body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/audit.rs b/libs/api/project/audit.rs deleted file mode 100644 index 9b6b1a5..0000000 --- a/libs/api/project/audit.rs +++ /dev/null @@ -1,82 +0,0 @@ -use super::PageQuery; -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/audit-logs", - params( - ("project_name" = String, Path), - ("page" = Option, Query), - ("per_page" = Option, Query), - ), - responses( - (status = 200, description = "List project audit logs", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_audit_logs( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_get_audit_logs(project_name, query.page, query.per_page, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/audit-logs/{log_id}", - params( - ("project_name" = String, Path), - ("log_id" = i64, Path), - ), - responses( - (status = 200, description = "Get project audit log", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_audit_log( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (_project_name, log_id) = path.into_inner(); - let resp = service.project_get_audit_log(log_id, &session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/audit-logs", - params(("project_name" = String, Path)), - request_body = service::project::audit::AuditLogParams, - responses( - (status = 200, description = "Log project audit event", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_log_audit( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_log_audit(project_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/avatar.rs b/libs/api/project/avatar.rs deleted file mode 100644 index 9ae7c89..0000000 --- a/libs/api/project/avatar.rs +++ /dev/null @@ -1,82 +0,0 @@ -use actix_multipart::Multipart; -use actix_web::{HttpResponse, Result, web}; -use futures_util::StreamExt; -use service::AppService; -use session::Session; - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct AvatarUploadResponse { - pub avatar_url: String, -} - -/// Upload a project's avatar. -/// Accepts a multipart form with a single file field. -#[utoipa::path( - post, - path = "/api/projects/{project_name}/avatar", - params( - ("project_name" = String, Path, description = "Project name"), - ), - responses( - (status = 200, description = "Avatar uploaded", body = crate::ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "No permission"), - ), - tag = "Project" -)] -pub async fn upload_project_avatar( - service: web::Data, - session: Session, - path: web::Path, - mut payload: Multipart, -) -> Result { - let project_name = path.into_inner(); - let max_size: usize = 2 * 1024 * 1024; // 2MB - - let mut file_data: Vec = Vec::new(); - let mut file_ext = "png".to_string(); - - while let Some(item) = payload.next().await { - let mut field = item.map_err(|e| { - crate::error::ApiError(service::error::AppError::BadRequest(e.to_string())) - })?; - - // Detect file extension from content-type - if let Some(content_type) = field.content_type() { - let ext = match content_type.essence_str() { - "image/jpeg" | "image/jpg" => "jpg", - "image/gif" => "gif", - "image/webp" => "webp", - "image/png" | _ => "png", - }; - file_ext = ext.to_string(); - } - - while let Some(chunk) = field.next().await { - let data = chunk.map_err(|e| { - crate::error::ApiError(service::error::AppError::BadRequest(e.to_string())) - })?; - if file_data.len() + data.len() > max_size { - return Err(crate::error::ApiError( - service::error::AppError::BadRequest( - "File exceeds maximum size of 2MB".to_string(), - ), - )); - } - file_data.extend_from_slice(&data); - } - } - - if file_data.is_empty() { - return Err(crate::error::ApiError( - service::error::AppError::BadRequest("No file provided".to_string()), - )); - } - - let avatar_url = service - .project_avatar_upload(&session, project_name, file_data, file_ext) - .await - .map_err(crate::error::ApiError::from)?; - - Ok(crate::ApiResponse::ok(AvatarUploadResponse { avatar_url }).to_response()) -} diff --git a/libs/api/project/billing.rs b/libs/api/project/billing.rs deleted file mode 100644 index d9837fa..0000000 --- a/libs/api/project/billing.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/billing", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get project billing", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_billing( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_billing_current(&session, project_name) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/billing/history", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get project billing history", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_billing_history( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_billing_history(&session, project_name, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/billing/errors", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get project billing errors", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn project_billing_errors( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_billing_errors(&session, project_name) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/board.rs b/libs/api/project/board.rs deleted file mode 100644 index 6e5f1bc..0000000 --- a/libs/api/project/board.rs +++ /dev/null @@ -1,324 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::project::board::{ - BoardResponse, BoardWithColumnsResponse, CardResponse, ColumnResponse, CreateBoardParams, - CreateCardParams, CreateColumnParams, MoveCardParams, UpdateBoardParams, UpdateCardParams, - UpdateColumnParams, -}; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/boards", - params( - ("project_name" = String, Path, description = "Project name"), - ), - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "List boards", body = ApiResponse>), - ), - tag = "Project" -)] -pub async fn board_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let boards = service.board_list(project_name, &session).await?; - Ok(ApiResponse::ok(boards).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/boards/{board_id}", - params( - ("project_name" = String, Path), - ("board_id" = Uuid, Path), - ), - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Get board with columns and cards", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn board_get( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, -) -> Result { - let (project_name, board_id) = path.into_inner(); - let board = service.board_get(project_name, board_id, &session).await?; - Ok(ApiResponse::ok(board).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/boards", - params( - ("project_name" = String, Path), - ), - request_body = CreateBoardParams, - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Create board", body = ApiResponse), - ), - tag = "Project" -)] -pub async fn board_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let board = service - .board_create(project_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(board).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/boards/{board_id}", - params( - ("project_name" = String, Path), - ("board_id" = Uuid, Path), - ), - request_body = UpdateBoardParams, - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Update board", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn board_update( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, - body: web::Json, -) -> Result { - let (project_name, board_id) = path.into_inner(); - let board = service - .board_update(project_name, board_id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(board).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/boards/{board_id}", - params( - ("project_name" = String, Path), - ("board_id" = Uuid, Path), - ), - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Delete board"), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn board_delete( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, -) -> Result { - let (project_name, board_id) = path.into_inner(); - service - .board_delete(project_name, board_id, &session) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/boards/{board_id}/columns", - params( - ("project_name" = String, Path), - ("board_id" = Uuid, Path), - ), - request_body = CreateColumnParams, - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Create column", body = ApiResponse), - (status = 404, description = "Board not found"), - ), - tag = "Project" -)] -pub async fn column_create( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, - body: web::Json, -) -> Result { - let (project_name, board_id) = path.into_inner(); - let column = service - .column_create(project_name, board_id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(column).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/columns/{column_id}", - params( - ("project_name" = String, Path), - ("column_id" = Uuid, Path), - ), - request_body = UpdateColumnParams, - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Update column", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn column_update( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, - body: web::Json, -) -> Result { - let (project_name, column_id) = path.into_inner(); - let column = service - .column_update(project_name, column_id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(column).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/columns/{column_id}", - params( - ("project_name" = String, Path), - ("column_id" = Uuid, Path), - ), - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Delete column"), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn column_delete( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, -) -> Result { - let (project_name, column_id) = path.into_inner(); - service - .column_delete(project_name, column_id, &session) - .await?; - Ok(crate::api_success()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/cards", - params( - ("project_name" = String, Path), - ), - request_body = CreateCardParams, - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Create card", body = ApiResponse), - ), - tag = "Project" -)] -pub async fn card_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let card = service - .card_create(project_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(card).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/cards/{card_id}", - params( - ("project_name" = String, Path), - ("card_id" = Uuid, Path), - ), - request_body = UpdateCardParams, - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Update card", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn card_update( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, - body: web::Json, -) -> Result { - let (project_name, card_id) = path.into_inner(); - let card = service - .card_update(project_name, card_id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(card).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/cards/{card_id}/move", - params( - ("project_name" = String, Path), - ("card_id" = Uuid, Path), - ), - request_body = MoveCardParams, - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Move card", body = ApiResponse), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn card_move( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, - body: web::Json, -) -> Result { - let (project_name, card_id) = path.into_inner(); - let card = service - .card_move(project_name, card_id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(card).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/cards/{card_id}", - params( - ("project_name" = String, Path), - ("card_id" = Uuid, Path), - ), - responses( - (status = 401, description = "Unauthorized"), - (status = 200, description = "Delete card"), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn card_delete( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, -) -> Result { - let (project_name, card_id) = path.into_inner(); - service.card_delete(project_name, card_id, &session).await?; - Ok(crate::api_success()) -} diff --git a/libs/api/project/info.rs b/libs/api/project/info.rs deleted file mode 100644 index efce56e..0000000 --- a/libs/api/project/info.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get project info", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_info( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service.project_info(&session, project_name).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/init.rs b/libs/api/project/init.rs deleted file mode 100644 index 9cd93b4..0000000 --- a/libs/api/project/init.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - post, - path = "/api/projects", - request_body = service::project::init::ProjectInitParams, - responses( - (status = 200, description = "Create project", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_create( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service.project_init(&session, body.into_inner()).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/invitation.rs b/libs/api/project/invitation.rs deleted file mode 100644 index 7f343cc..0000000 --- a/libs/api/project/invitation.rs +++ /dev/null @@ -1,168 +0,0 @@ -use super::PageQuery; -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use models::projects::MemberRole; -use service::AppService; -use session::Session; - -#[derive(serde::Deserialize, utoipa::ToSchema)] -pub struct InviteUserRequest { - pub email: String, - pub scope: MemberRole, -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/invitations", - params( - ("project_name" = String, Path), - ("page" = Option, Query), - ("per_page" = Option, Query), - ), - responses( - (status = 200, description = "List project invitations", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_invitations( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_get_invitations(project_name, query.page, query.per_page, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/me/invitations", - responses( - (status = 200, description = "List my invitations", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_my_invitations( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let resp = service - .project_get_my_invitations(query.page, query.per_page, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/invitations", - params(("project_name" = String, Path)), - request_body = InviteUserRequest, - responses( - (status = 200, description = "Invite user to project"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_invite_user( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - service - .project_invite_user( - project_name, - body.email.clone(), - body.scope.clone(), - &session, - ) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/invitations/accept", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Accept project invitation"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_accept_invitation( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - service - .project_accept_invitation(project_name, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/invitations/reject", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Reject project invitation"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_reject_invitation( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - service - .project_reject_invitation(project_name, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/invitations/{user_id}", - params( - ("project_name" = String, Path), - ("user_id" = String, Path), - ), - responses( - (status = 200, description = "Cancel project invitation"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_cancel_invitation( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (project_name, user_id) = path.into_inner(); - let user_uuid = uuid::Uuid::parse_str(&user_id) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - service - .project_cancel_invitation(project_name, user_uuid, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/project/join_answers.rs b/libs/api/project/join_answers.rs deleted file mode 100644 index e6a1862..0000000 --- a/libs/api/project/join_answers.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/join-requests/{request_id}/answers", - params( - ("project_name" = String, Path), - ("request_id" = i64, Path), - ), - responses( - (status = 200, description = "Get join request answers", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_join_answers( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project_name, request_id) = path.into_inner(); - let resp = service - .project_get_join_answers(project_name, request_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/join-requests/{request_id}/answers", - params( - ("project_name" = String, Path), - ("request_id" = i64, Path), - ), - request_body = Vec, - responses( - (status = 200, description = "Submit join request answers"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_submit_join_answers( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json>, -) -> Result { - let (project_name, request_id) = path.into_inner(); - service - .project_submit_join_answers(project_name, request_id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/project/join_request.rs b/libs/api/project/join_request.rs deleted file mode 100644 index d7c2094..0000000 --- a/libs/api/project/join_request.rs +++ /dev/null @@ -1,142 +0,0 @@ -use super::{JoinRequestQuery, PageQuery}; -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/join-requests", - params( - ("project_name" = String, Path), - ("status" = Option, Query), - ("page" = Option, Query), - ("per_page" = Option, Query), - ), - responses( - (status = 200, description = "List join requests", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_join_requests( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_get_join_requests( - project_name, - query.status.clone(), - query.page, - query.per_page, - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/join-requests", - params(("project_name" = String, Path)), - request_body = service::project::join_request::SubmitJoinRequest, - responses( - (status = 200, description = "Submit join request"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_submit_join_request( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_submit_join_request(project_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "request_id": resp })).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/join-requests/{request_id}", - params( - ("project_name" = String, Path), - ("request_id" = i64, Path), - ), - request_body = service::project::join_request::ProcessJoinRequest, - responses( - (status = 200, description = "Process join request"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_process_join_request( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (project_name, request_id) = path.into_inner(); - service - .project_process_join_request(project_name, request_id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/join-requests/{request_id}", - params( - ("project_name" = String, Path), - ("request_id" = i64, Path), - ), - responses( - (status = 200, description = "Cancel join request"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_cancel_join_request( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (project_name, request_id) = path.into_inner(); - service - .project_cancel_join_request(project_name, request_id, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/me/join-requests", - responses( - (status = 200, description = "List my join requests", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_my_join_requests( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let resp = service - .project_get_my_join_requests(query.page, query.per_page, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/join_settings.rs b/libs/api/project/join_settings.rs deleted file mode 100644 index 482f9c5..0000000 --- a/libs/api/project/join_settings.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/join-settings", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get join settings", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_join_settings( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_get_join_settings(project_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/join-settings", - params(("project_name" = String, Path)), - request_body = service::project::join_settings::UpdateJoinSettingsRequest, - responses( - (status = 200, description = "Update join settings", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_update_join_settings( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_update_join_settings(project_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/labels.rs b/libs/api/project/labels.rs deleted file mode 100644 index cebc106..0000000 --- a/libs/api/project/labels.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/labels", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "List project labels", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_labels( - service: web::Data, - _session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service.project_get_labels(project_name).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/labels", - params(("project_name" = String, Path)), - request_body = service::project::labels::CreateLabelParams, - responses( - (status = 200, description = "Create project label", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_create_label( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_create_label(project_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/labels/{label_id}", - params( - ("project_name" = String, Path), - ("label_id" = i64, Path), - ), - responses( - (status = 200, description = "Get project label", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_get_label( - service: web::Data, - _session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (_project_name, label_id) = path.into_inner(); - let resp = service.project_get_label(label_id).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/labels/{label_id}", - params( - ("project_name" = String, Path), - ("label_id" = i64, Path), - ), - request_body = service::project::labels::UpdateLabelParams, - responses( - (status = 200, description = "Update project label", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_update_label( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, - body: web::Json, -) -> Result { - let (_project_name, label_id) = path.into_inner(); - let resp = service - .project_update_label(label_id, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/labels/{label_id}", - params( - ("project_name" = String, Path), - ("label_id" = i64, Path), - ), - responses( - (status = 200, description = "Delete project label"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_delete_label( - service: web::Data, - session: Session, - path: web::Path<(String, i64)>, -) -> Result { - let (_project_name, label_id) = path.into_inner(); - service.project_delete_label(label_id, &session).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/project/like.rs b/libs/api/project/like.rs deleted file mode 100644 index 93e016a..0000000 --- a/libs/api/project/like.rs +++ /dev/null @@ -1,116 +0,0 @@ -use super::UserPagerQuery; -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct IsLikeResponse { - pub is_like: bool, -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/like", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Like project"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_like( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - service.project_like(&session, project_name).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/like", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Unlike project"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_unlike( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - service.project_unlike(&session, project_name).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/like", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Check if user likes project", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_is_like( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service.project_is_like(&session, project_name).await?; - Ok(ApiResponse::ok(IsLikeResponse { is_like: resp }).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/likes/count", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get like count"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_likes_count( - service: web::Data, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service.project_likes(project_name).await?; - Ok(ApiResponse::ok(serde_json::json!({ "count": resp })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/likes/users", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "List users who liked project", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_like_users( - service: web::Data, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_like_user_list(project_name, query.into_inner().into()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/members.rs b/libs/api/project/members.rs deleted file mode 100644 index 3d503b0..0000000 --- a/libs/api/project/members.rs +++ /dev/null @@ -1,193 +0,0 @@ -use super::PageQuery; -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/members", - params( - ("project_name" = String, Path), - ("page" = Option, Query), - ("per_page" = Option, Query), - ), - responses( - (status = 200, description = "List project members", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_members( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_get_members(project_name, query.page, query.per_page, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/members/grouped", - params( - ("project_name" = String, Path), - ), - responses( - (status = 200, description = "List project members grouped by role", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_members_grouped( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_get_members_grouped(project_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/role-priorities", - params( - ("project_name" = String, Path), - ), - responses( - (status = 200, description = "List project role priorities", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_role_priorities( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_get_role_priorities(project_name, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/role-priorities", - params( - ("project_name" = String, Path), - ), - request_body = service::project::members::UpsertRolePriorityRequest, - responses( - (status = 200, description = "Upsert role priority", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_upsert_role_priority( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_upsert_role_priority(project_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/role-priorities/{role_key}", - params( - ("project_name" = String, Path), - ("role_key" = String, Path), - ), - responses( - (status = 200, description = "Delete role priority"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_delete_role_priority( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (project_name, role_key) = path.into_inner(); - service - .project_delete_role_priority(project_name, role_key, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/members/role", - params(("project_name" = String, Path)), - request_body = service::project::members::UpdateMemberRoleRequest, - responses( - (status = 200, description = "Update member role"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_update_member_role( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - service - .project_update_member_role(project_name, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/members/{user_id}", - params( - ("project_name" = String, Path), - ("user_id" = String, Path), - ), - responses( - (status = 200, description = "Remove member from project"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_remove_member( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (project_name, user_id) = path.into_inner(); - let user_uuid = uuid::Uuid::parse_str(&user_id) - .map_err(|_| service::error::AppError::BadRequest("Invalid UUID".to_string()))?; - service - .project_remove_member(project_name, user_uuid, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/project/message_favorite.rs b/libs/api/project/message_favorite.rs deleted file mode 100644 index c7fb052..0000000 --- a/libs/api/project/message_favorite.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::project::message_favorite::{ - ProjectMessageFavoriteQuery, ProjectMessageFavoriteResponse, -}; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/message-favorites", - params( - ("project_name" = String, Path), - ProjectMessageFavoriteQuery, - ), - responses( - (status = 200, description = "List current user's project message favorites", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn project_message_favorites( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let resp = service - .project_message_favorites(&session, path.into_inner(), query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/messages/{message_id}/favorite", - params( - ("project_name" = String, Path), - ("message_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Favorite a project message", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn project_message_favorite_add( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, -) -> Result { - let (project_name, message_id) = path.into_inner(); - let resp = service - .project_message_favorite_add(&session, project_name, message_id) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/messages/{message_id}/favorite", - params( - ("project_name" = String, Path), - ("message_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Remove a project message favorite"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Project" -)] -pub async fn project_message_favorite_remove( - service: web::Data, - session: Session, - path: web::Path<(String, Uuid)>, -) -> Result { - let (project_name, message_id) = path.into_inner(); - service - .project_message_favorite_remove(&session, project_name, message_id) - .await?; - Ok(crate::api_success()) -} diff --git a/libs/api/project/mod.rs b/libs/api/project/mod.rs deleted file mode 100644 index 5dc5054..0000000 --- a/libs/api/project/mod.rs +++ /dev/null @@ -1,343 +0,0 @@ -pub mod activity; -pub mod audit; -pub mod avatar; -pub mod billing; -pub mod board; -pub mod info; -pub mod init; -pub mod invitation; -pub mod join_answers; -pub mod join_request; -pub mod join_settings; -pub mod labels; -pub mod like; -pub mod members; -pub mod message_favorite; -pub mod repo; -pub mod settings; -pub mod stats; -pub mod transfer_repo; -pub mod watch; - -use actix_web::web; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct PageQuery { - pub page: Option, - pub per_page: Option, -} - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct RepoPagerQuery { - pub limit: Option, - pub cursor: Option, -} - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct UserPagerQuery { - pub page: Option, - #[serde(alias = "par_page")] - pub per_page: Option, -} - -impl From for service::Pager { - fn from(q: UserPagerQuery) -> Self { - service::Pager { - page: q.page.unwrap_or(1), - per_page: q.per_page.unwrap_or(20), - } - } -} - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct JoinRequestQuery { - pub status: Option, - pub page: Option, - pub per_page: Option, -} - -pub fn init_project_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/projects") - .route("", web::post().to(init::project_create)) - .route( - "/me/invitations", - web::get().to(invitation::project_my_invitations), - ) - .route( - "/me/join-requests", - web::get().to(join_request::project_my_join_requests), - ) - .route("/{project_name}", web::get().to(info::project_info)) - .route("/{project_name}/stats", web::get().to(stats::project_stats)) - .route("/{project_name}/repos", web::get().to(repo::project_repos)) - .route( - "/{project_name}/repos", - web::post().to(repo::project_repo_create), - ) - .route( - "/{project_name}/members", - web::get().to(members::project_members), - ) - .route( - "/{project_name}/members/grouped", - web::get().to(members::project_members_grouped), - ) - .route( - "/{project_name}/members/role", - web::patch().to(members::project_update_member_role), - ) - .route( - "/{project_name}/members/{user_id}", - web::delete().to(members::project_remove_member), - ) - .route( - "/{project_name}/role-priorities", - web::get().to(members::project_role_priorities), - ) - .route( - "/{project_name}/role-priorities", - web::post().to(members::project_upsert_role_priority), - ) - .route( - "/{project_name}/role-priorities/{role_key}", - web::delete().to(members::project_delete_role_priority), - ) - .route( - "/{project_name}/labels", - web::get().to(labels::project_labels), - ) - .route( - "/{project_name}/labels", - web::post().to(labels::project_create_label), - ) - .route( - "/{project_name}/labels/{label_id}", - web::get().to(labels::project_get_label), - ) - .route( - "/{project_name}/labels/{label_id}", - web::patch().to(labels::project_update_label), - ) - .route( - "/{project_name}/labels/{label_id}", - web::delete().to(labels::project_delete_label), - ) - .route("/{project_name}/like", web::post().to(like::project_like)) - .route( - "/{project_name}/like", - web::delete().to(like::project_unlike), - ) - .route("/{project_name}/like", web::get().to(like::project_is_like)) - .route( - "/{project_name}/likes/count", - web::get().to(like::project_likes_count), - ) - .route( - "/{project_name}/likes/users", - web::get().to(like::project_like_users), - ) - .route( - "/{project_name}/watch", - web::post().to(watch::project_watch), - ) - .route( - "/{project_name}/watch", - web::delete().to(watch::project_unwatch), - ) - .route( - "/{project_name}/watch", - web::get().to(watch::project_is_watch), - ) - .route( - "/{project_name}/watches/count", - web::get().to(watch::project_watches_count), - ) - .route( - "/{project_name}/watches/users", - web::get().to(watch::project_watch_users), - ) - .route( - "/{project_name}/settings/name", - web::patch().to(settings::project_exchange_name), - ) - .route( - "/{project_name}/settings/visibility", - web::patch().to(settings::project_exchange_visibility), - ) - .route( - "/{project_name}/settings/title", - web::patch().to(settings::project_exchange_title), - ) - .route( - "/{project_name}/audit-logs", - web::get().to(audit::project_audit_logs), - ) - .route( - "/{project_name}/audit-logs/{log_id}", - web::get().to(audit::project_audit_log), - ) - .route( - "/{project_name}/audit-logs", - web::post().to(audit::project_log_audit), - ) - .route( - "/{project_name}/activities", - web::get().to(activity::project_activities), - ) - .route( - "/{project_name}/activities", - web::post().to(activity::project_log_activity), - ) - .route( - "/{project_name}/message-favorites", - web::get().to(message_favorite::project_message_favorites), - ) - .route( - "/{project_name}/messages/{message_id}/favorite", - web::post().to(message_favorite::project_message_favorite_add), - ) - .route( - "/{project_name}/messages/{message_id}/favorite", - web::delete().to(message_favorite::project_message_favorite_remove), - ) - .route( - "/{project_name}/avatar", - web::post().to(avatar::upload_project_avatar), - ) - .route( - "/{project_name}/billing", - web::get().to(billing::project_billing), - ) - .route( - "/{project_name}/billing/history", - web::get().to(billing::project_billing_history), - ) - .route( - "/{project_name}/billing/errors", - web::get().to(billing::project_billing_errors), - ) - .route( - "/{project_name}/invitations", - web::get().to(invitation::project_invitations), - ) - .route( - "/{project_name}/invitations", - web::post().to(invitation::project_invite_user), - ) - .route( - "/{project_name}/invitations/accept", - web::post().to(invitation::project_accept_invitation), - ) - .route( - "/{project_name}/invitations/reject", - web::post().to(invitation::project_reject_invitation), - ) - .route( - "/{project_name}/invitations/{user_id}", - web::delete().to(invitation::project_cancel_invitation), - ) - .route( - "/{project_name}/join-settings", - web::get().to(join_settings::project_join_settings), - ) - .route( - "/{project_name}/join-settings", - web::patch().to(join_settings::project_update_join_settings), - ) - .route( - "/{project_name}/join-requests", - web::get().to(join_request::project_join_requests), - ) - .route( - "/{project_name}/join-requests", - web::post().to(join_request::project_submit_join_request), - ) - .route( - "/{project_name}/join-requests/{request_id}", - web::patch().to(join_request::project_process_join_request), - ) - .route( - "/{project_name}/join-requests/{request_id}", - web::delete().to(join_request::project_cancel_join_request), - ) - .route( - "/{project_name}/join-requests/{request_id}/answers", - web::get().to(join_answers::project_join_answers), - ) - .route( - "/{project_name}/join-requests/{request_id}/answers", - web::post().to(join_answers::project_submit_join_answers), - ) - .route( - "/{source_project}/repos/{repo_name}/transfer", - web::post().to(transfer_repo::project_transfer_repo), - ) - .route("/{project_name}/boards", web::get().to(board::board_list)) - .route( - "/{project_name}/boards", - web::post().to(board::board_create), - ) - .route( - "/{project_name}/boards/{board_id}", - web::get().to(board::board_get), - ) - .route( - "/{project_name}/boards/{board_id}", - web::patch().to(board::board_update), - ) - .route( - "/{project_name}/boards/{board_id}", - web::delete().to(board::board_delete), - ) - .route( - "/{project_name}/boards/{board_id}/columns", - web::post().to(board::column_create), - ) - .route( - "/{project_name}/columns/{column_id}", - web::patch().to(board::column_update), - ) - .route( - "/{project_name}/columns/{column_id}", - web::delete().to(board::column_delete), - ) - .route("/{project_name}/cards", web::post().to(board::card_create)) - .route( - "/{project_name}/cards/{card_id}", - web::patch().to(board::card_update), - ) - .route( - "/{project_name}/cards/{card_id}/move", - web::post().to(board::card_move), - ) - .route( - "/{project_name}/cards/{card_id}", - web::delete().to(board::card_delete), - ) - .route( - "/{project_name}/skills", - web::get().to(crate::skill::skill_list), - ) - .route( - "/{project_name}/skills", - web::post().to(crate::skill::skill_create), - ) - .route( - "/{project_name}/skills/scan", - web::post().to(crate::skill::skill_scan), - ) - .route( - "/{project_name}/skills/{slug}", - web::get().to(crate::skill::skill_get), - ) - .route( - "/{project_name}/skills/{slug}", - web::patch().to(crate::skill::skill_update), - ) - .route( - "/{project_name}/skills/{slug}", - web::delete().to(crate::skill::skill_delete), - ), - ); -} diff --git a/libs/api/project/repo.rs b/libs/api/project/repo.rs deleted file mode 100644 index ee20c25..0000000 --- a/libs/api/project/repo.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::project::repo::{ - ProjectRepoCreateParams, ProjectRepoCreateResponse, ProjectRepositoryQuery, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/repos", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get project repositories", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Project not found"), - ), - tag = "Project" -)] -pub async fn project_repos( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_repo(&session, project_name, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/repos", - params(("project_name" = String, Path)), - request_body = ProjectRepoCreateParams, - responses( - (status = 200, description = "Create a repository", body = ApiResponse), - (status = 400, description = "Bad request"), - (status = 401, description = "Unauthorized"), - (status = 409, description = "Repository name already exists"), - ), - tag = "Project" -)] -pub async fn project_repo_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_repo_create(&session, project_name, body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/settings.rs b/libs/api/project/settings.rs deleted file mode 100644 index 3ff391c..0000000 --- a/libs/api/project/settings.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/settings/name", - params(("project_name" = String, Path)), - request_body = service::project::settings::ExchangeProjectName, - responses( - (status = 200, description = "Update project name"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_exchange_name( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - service - .project_exchange_name(&session, project_name, body.into_inner()) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/settings/visibility", - params(("project_name" = String, Path)), - request_body = service::project::settings::ExchangeProjectVisibility, - responses( - (status = 200, description = "Update project visibility"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_exchange_visibility( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - service - .project_exchange_visibility(&session, project_name, body.into_inner()) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/settings/title", - params(("project_name" = String, Path)), - request_body = service::project::settings::ExchangeProjectTitle, - responses( - (status = 200, description = "Update project title"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_exchange_title( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - service - .project_exchange_title(&session, project_name, body.into_inner()) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/project/stats.rs b/libs/api/project/stats.rs deleted file mode 100644 index 5ce8708..0000000 --- a/libs/api/project/stats.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::project::stats::ProjectStatsResponse; -use session::Session; - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/stats", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get project statistics", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden — no access to this project"), - (status = 404, description = "Project not found"), - ), - tag = "Project" -)] -pub async fn project_stats( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service.project_stats(&session, project_name).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/transfer_repo.rs b/libs/api/project/transfer_repo.rs deleted file mode 100644 index 9ad2358..0000000 --- a/libs/api/project/transfer_repo.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - post, - path = "/api/projects/{source_project}/repos/{repo_name}/transfer", - params( - ("source_project" = String, Path), - ("repo_name" = String, Path), - ), - request_body = service::project::transfer_repo::TransferRepoParams, - responses( - (status = 200, description = "Transfer repo to another project", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_transfer_repo( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (source_project_name, repo_name) = path.into_inner(); - let resp = service - .transfer_repo(&session, source_project_name, repo_name, body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/project/watch.rs b/libs/api/project/watch.rs deleted file mode 100644 index a91c489..0000000 --- a/libs/api/project/watch.rs +++ /dev/null @@ -1,116 +0,0 @@ -use super::UserPagerQuery; -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct IsWatchResponse { - pub is_watching: bool, -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/watch", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Watch project"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_watch( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - service.project_watch(&session, project_name).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/watch", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Unwatch project"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_unwatch( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - service.project_unwatch(&session, project_name).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/watch", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Check if user watches project", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_is_watch( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service.project_is_watch(&session, project_name).await?; - Ok(ApiResponse::ok(IsWatchResponse { is_watching: resp }).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/watches/count", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Get watch count"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_watches_count( - service: web::Data, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let resp = service.project_watches(project_name).await?; - Ok(ApiResponse::ok(serde_json::json!({ "count": resp })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/watches/users", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "List users watching project", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Project" -)] -pub async fn project_watch_users( - service: web::Data, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let resp = service - .project_watch_user_list(project_name, query.into_inner().into()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/pull_request/merge.rs b/libs/api/pull_request/merge.rs deleted file mode 100644 index 6db090b..0000000 --- a/libs/api/pull_request/merge.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/merge", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Get merge analysis", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn merge_analysis( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .merge_analysis(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/conflicts", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Check merge conflicts", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn merge_conflict_check( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .merge_conflict_check(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/merge", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = service::pull_request::MergeRequest, - responses( - (status = 200, description = "Execute merge", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - (status = 409, description = "Conflict"), -), - tag = "PullRequest" -)] -pub async fn merge_execute( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .merge_execute(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/merge/abort", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Abort merge"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn merge_abort( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - service - .merge_abort(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/merge/in_progress", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Check if merge is in progress"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn merge_is_in_progress( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .merge_is_in_progress(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "in_progress": resp })).to_response()) -} diff --git a/libs/api/pull_request/mod.rs b/libs/api/pull_request/mod.rs deleted file mode 100644 index fb4577a..0000000 --- a/libs/api/pull_request/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -pub mod merge; -pub mod pull_request; -pub mod review; -pub mod review_comment; -pub mod review_request; - -use actix_web::web; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct ListQuery { - pub status: Option, - pub page: Option, - pub per_page: Option, -} - -pub fn init_pull_request_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/repo_pr/{namespace}/{repo}/pulls") - .route("", web::get().to(pull_request::pull_request_list)) - .route("", web::post().to(pull_request::pull_request_create)) - .route( - "/summary", - web::get().to(pull_request::pull_request_summary), - ) - .route("/{number}", web::get().to(pull_request::pull_request_get)) - .route( - "/{number}", - web::patch().to(pull_request::pull_request_update), - ) - .route( - "/{number}", - web::delete().to(pull_request::pull_request_delete), - ) - .route( - "/{number}/close", - web::post().to(pull_request::pull_request_close), - ) - .route( - "/{number}/reopen", - web::post().to(pull_request::pull_request_reopen), - ) - // reviews (from pull_request module) - .route( - "/{pr_number}/reviews", - web::get().to(pull_request::review_list), - ) - .route( - "/{pr_number}/reviews", - web::post().to(pull_request::review_submit), - ) - .route( - "/{pr_number}/reviews", - web::patch().to(pull_request::review_update), - ) - .route( - "/{pr_number}/reviews/{reviewer_id}", - web::delete().to(pull_request::review_delete), - ) - // review comments (from pull_request module) - .route( - "/{pr_number}/comments", - web::get().to(pull_request::review_comment_list), - ) - .route( - "/{pr_number}/comments", - web::post().to(pull_request::review_comment_create), - ) - .route( - "/{pr_number}/comments/{comment_id}", - web::patch().to(pull_request::review_comment_update), - ) - .route( - "/{pr_number}/comments/{comment_id}", - web::delete().to(pull_request::review_comment_delete), - ) - .route( - "/{pr_number}/comments/{comment_id}/resolve", - web::put().to(review_comment::review_comment_resolve), - ) - .route( - "/{pr_number}/comments/{comment_id}/resolve", - web::delete().to(review_comment::review_comment_unresolve), - ) - .route( - "/{pr_number}/comments/{comment_id}/replies", - web::post().to(review_comment::review_comment_reply), - ) - .route( - "/{pr_number}/commits", - web::get().to(pull_request::pr_commits_list), - ) - .route( - "/{pr_number}/merge", - web::get().to(pull_request::merge_analysis), - ) - .route( - "/{pr_number}/conflicts", - web::get().to(pull_request::merge_conflict_check), - ) - .route( - "/{pr_number}/merge", - web::post().to(pull_request::merge_execute), - ) - .route( - "/{pr_number}/merge/abort", - web::post().to(pull_request::merge_abort), - ) - .route( - "/{pr_number}/merge/in_progress", - web::get().to(pull_request::merge_is_in_progress), - ) - // side-by-side diff - .route( - "/{pr_number}/diff/side-by-side", - web::get().to(pull_request::pr_diff_side_by_side), - ) - // review requests - .route( - "/{pr_number}/review-requests", - web::get().to(review_request::review_request_list), - ) - .route( - "/{pr_number}/review-requests", - web::post().to(review_request::review_request_create), - ) - .route( - "/{pr_number}/review-requests/{reviewer}", - web::delete().to(review_request::review_request_delete), - ) - .route( - "/{pr_number}/review-requests/{reviewer}/dismiss", - web::post().to(review_request::review_request_dismiss), - ), - ); -} diff --git a/libs/api/pull_request/pull_request.rs b/libs/api/pull_request/pull_request.rs deleted file mode 100644 index 70d7dbf..0000000 --- a/libs/api/pull_request/pull_request.rs +++ /dev/null @@ -1,682 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::git::diff::SideBySideDiffQuery; -use service::pull_request::ReviewCommentListQuery; -use session::Session; - -use super::ListQuery; - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("status" = Option, Query), - ("page" = Option, Query), - ("per_page" = Option, Query), - ), - responses( - (status = 200, description = "List pull requests", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn pull_request_list( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - query: web::Query, -) -> Result { - let (namespace, repo) = path.into_inner(); - let resp = service - .pull_request_list( - namespace, - repo, - query.status.clone(), - Some(query.page.unwrap_or(1)), - Some(query.per_page.unwrap_or(20)), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{number}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Get pull request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn pull_request_get( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, number) = path.into_inner(); - let resp = service - .pull_request_get(namespace, repo, number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - request_body = service::pull_request::PullRequestCreateRequest, - responses( - (status = 200, description = "Create pull request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn pull_request_create( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, - body: web::Json, -) -> Result { - let (namespace, repo) = path.into_inner(); - let resp = service - .pull_request_create(namespace, repo, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{number}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("number" = i64, Path), - ), - request_body = service::pull_request::PullRequestUpdateRequest, - responses( - (status = 200, description = "Update pull request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn pull_request_update( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, number) = path.into_inner(); - let resp = service - .pull_request_update(namespace, repo, number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{number}/close", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Close pull request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn pull_request_close( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, number) = path.into_inner(); - let resp = service - .pull_request_close(namespace, repo, number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{number}/reopen", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Reopen pull request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn pull_request_reopen( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, number) = path.into_inner(); - let resp = service - .pull_request_reopen(namespace, repo, number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{number}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("number" = i64, Path), - ), - responses( - (status = 200, description = "Delete pull request"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn pull_request_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, number) = path.into_inner(); - service - .pull_request_delete(namespace, repo, number, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/summary", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ), - responses( - (status = 200, description = "Get pull request summary", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn pull_request_summary( - service: web::Data, - session: Session, - path: web::Path<(String, String)>, -) -> Result { - let (namespace, repo) = path.into_inner(); - let resp = service - .pull_request_summary(namespace, repo, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/reviews", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "List pull request reviews", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_list( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_list(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/reviews", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = service::pull_request::ReviewSubmitRequest, - responses( - (status = 200, description = "Submit pull request review", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_submit( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_submit(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/reviews", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = service::pull_request::ReviewUpdateRequest, - responses( - (status = 200, description = "Update pull request review", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_update( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_update(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/reviews/{reviewer_id}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("reviewer_id" = String, Path, description = "Reviewer UUID"), - ), - responses( - (status = 200, description = "Delete pull request review"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, String)>, -) -> Result { - let (namespace, repo, pr_number, reviewer_id) = path.into_inner(); - let reviewer_uuid = uuid::Uuid::parse_str(&reviewer_id) - .map_err(|_| service::error::AppError::BadRequest("Invalid reviewer ID".to_string()))?; - service - .review_delete(namespace, repo, pr_number, reviewer_uuid, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("path" = Option, Query, description = "Filter by file path"), - ("resolved" = Option, Query, description = "Filter by resolved status"), - ("file_only" = Option, Query, description = "Only inline comments (true) or only general comments (false)"), - ), - responses( - (status = 200, description = "List pull request review comments", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_comment_list( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - query: web::Query, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_comment_list(namespace, repo, pr_number, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = service::pull_request::ReviewCommentCreateRequest, - responses( - (status = 200, description = "Create pull request review comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_comment_create( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_comment_create(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments/{comment_id}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("comment_id" = i64, Path), - ), - request_body = service::pull_request::ReviewCommentUpdateRequest, - responses( - (status = 200, description = "Update pull request review comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_comment_update( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number, comment_id) = path.into_inner(); - let resp = service - .review_comment_update( - namespace, - repo, - pr_number, - comment_id, - body.into_inner(), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments/{comment_id}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("comment_id" = i64, Path), - ), - responses( - (status = 200, description = "Delete pull request review comment"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_comment_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, i64)>, -) -> Result { - let (namespace, repo, pr_number, comment_id) = path.into_inner(); - service - .review_comment_delete(namespace, repo, pr_number, comment_id, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/merge", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Get merge analysis", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn merge_analysis( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .merge_analysis(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/conflicts", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Check merge conflicts", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn merge_conflict_check( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .merge_conflict_check(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/merge", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = service::pull_request::MergeRequest, - responses( - (status = 200, description = "Execute merge", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - (status = 409, description = "Conflict"), -), - tag = "PullRequest" -)] -pub async fn merge_execute( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .merge_execute(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/merge/abort", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Abort merge"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn merge_abort( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - service - .merge_abort(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/merge/in_progress", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "Check if merge is in progress"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn merge_is_in_progress( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .merge_is_in_progress(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "in_progress": resp })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/diff/side-by-side", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("pr_number" = i64, Path, description = "Pull request number"), - ), - responses( - (status = 200, description = "Side-by-side diff for a pull request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn pr_diff_side_by_side( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - query: web::Query, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .pr_diff_side_by_side(namespace, repo, pr_number, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/commits", - params( - ("namespace" = String, Path, description = "Project namespace"), - ("repo" = String, Path, description = "Repository name"), - ("pr_number" = i64, Path, description = "Pull request number"), - ), - responses( - (status = 200, description = "List commits in a pull request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn pr_commits_list( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .pr_commits_list(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/pull_request/review.rs b/libs/api/pull_request/review.rs deleted file mode 100644 index 748d283..0000000 --- a/libs/api/pull_request/review.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/reviews", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "List pull request reviews", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_list( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_list(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/reviews", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = service::pull_request::ReviewSubmitRequest, - responses( - (status = 200, description = "Submit pull request review", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_submit( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_submit(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/reviews", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = service::pull_request::ReviewUpdateRequest, - responses( - (status = 200, description = "Update pull request review", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_update( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_update(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/reviews/{reviewer_id}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("reviewer_id" = String, Path, description = "Reviewer UUID"), - ), - responses( - (status = 200, description = "Delete pull request review"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, String)>, -) -> Result { - let (namespace, repo, pr_number, reviewer_id) = path.into_inner(); - let reviewer_uuid = uuid::Uuid::parse_str(&reviewer_id) - .map_err(|_| service::error::AppError::BadRequest("Invalid reviewer ID".to_string()))?; - service - .review_delete(namespace, repo, pr_number, reviewer_uuid, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/pull_request/review_comment.rs b/libs/api/pull_request/review_comment.rs deleted file mode 100644 index f885bc1..0000000 --- a/libs/api/pull_request/review_comment.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::pull_request::review_comment::{ReviewCommentListQuery, ReviewCommentReplyRequest}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("path" = Option, Query, description = "Filter by file path"), - ("resolved" = Option, Query, description = "Filter by resolved status"), - ("file_only" = Option, Query, description = "Only inline comments (true) or only general comments (false)"), - ), - responses( - (status = 200, description = "List pull request review comments", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_comment_list( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - query: web::Query, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_comment_list(namespace, repo, pr_number, query.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = service::pull_request::ReviewCommentCreateRequest, - responses( - (status = 200, description = "Create pull request review comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_comment_create( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_comment_create(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments/{comment_id}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("comment_id" = i64, Path), - ), - request_body = service::pull_request::ReviewCommentUpdateRequest, - responses( - (status = 200, description = "Update pull request review comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_comment_update( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number, comment_id) = path.into_inner(); - let resp = service - .review_comment_update( - namespace, - repo, - pr_number, - comment_id, - body.into_inner(), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments/{comment_id}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("comment_id" = i64, Path), - ), - responses( - (status = 200, description = "Delete pull request review comment"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "PullRequest" -)] -pub async fn review_comment_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, i64)>, -) -> Result { - let (namespace, repo, pr_number, comment_id) = path.into_inner(); - service - .review_comment_delete(namespace, repo, pr_number, comment_id, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - put, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments/{comment_id}/resolve", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("comment_id" = i64, Path), - ), - responses( - (status = 200, description = "Mark comment as resolved", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn review_comment_resolve( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, i64)>, -) -> Result { - let (namespace, repo, pr_number, comment_id) = path.into_inner(); - let resp = service - .review_comment_resolve(namespace, repo, pr_number, comment_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments/{comment_id}/resolve", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("comment_id" = i64, Path), - ), - responses( - (status = 200, description = "Mark comment as unresolved", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn review_comment_unresolve( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, i64)>, -) -> Result { - let (namespace, repo, pr_number, comment_id) = path.into_inner(); - let resp = service - .review_comment_unresolve(namespace, repo, pr_number, comment_id, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/comments/{comment_id}/replies", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("comment_id" = i64, Path), - ), - request_body = service::pull_request::review_comment::ReviewCommentReplyRequest, - responses( - (status = 200, description = "Reply to a comment", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn review_comment_reply( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number, comment_id) = path.into_inner(); - let resp = service - .review_comment_reply( - namespace, - repo, - pr_number, - comment_id, - body.into_inner(), - &session, - ) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/pull_request/review_request.rs b/libs/api/pull_request/review_request.rs deleted file mode 100644 index f4200c7..0000000 --- a/libs/api/pull_request/review_request.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::pull_request::review_request::ReviewRequestCreateRequest; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/review-requests", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - responses( - (status = 200, description = "List review requests for a pull request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn review_request_list( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_request_list(namespace, repo, pr_number, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/review-requests", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ), - request_body = ReviewRequestCreateRequest, - responses( - (status = 200, description = "Create or update a review request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn review_request_create( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64)>, - body: web::Json, -) -> Result { - let (namespace, repo, pr_number) = path.into_inner(); - let resp = service - .review_request_create(namespace, repo, pr_number, body.into_inner(), &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/review-requests/{reviewer}", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("reviewer" = Uuid, Path), - ), - responses( - (status = 200, description = "Delete (cancel) a review request"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn review_request_delete( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, Uuid)>, -) -> Result { - let (namespace, repo, pr_number, reviewer) = path.into_inner(); - service - .review_request_delete(namespace, repo, pr_number, reviewer, &session) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - post, - path = "/api/repo_pr/{namespace}/{repo}/pulls/{pr_number}/review-requests/{reviewer}/dismiss", - params( - ("namespace" = String, Path), - ("repo" = String, Path), - ("pr_number" = i64, Path), - ("reviewer" = Uuid, Path), - ), - responses( - (status = 200, description = "Dismiss a review request", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "PullRequest" -)] -pub async fn review_request_dismiss( - service: web::Data, - session: Session, - path: web::Path<(String, String, i64, Uuid)>, -) -> Result { - let (namespace, repo, pr_number, reviewer) = path.into_inner(); - let resp = service - .review_request_dismiss(namespace, repo, pr_number, reviewer, &session) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/robots.rs b/libs/api/robots.rs deleted file mode 100644 index afd6433..0000000 --- a/libs/api/robots.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::{HttpResponse, web}; -use service::AppService; - -/// Serves robots.txt, blocking all sensitive paths from crawlers. -pub async fn robots(service: web::Data) -> HttpResponse { - let raw = service - .config - .main_domain() - .unwrap_or_else(|_| "https://gitdata.ai".to_string()); - let sitemap_base = if raw.starts_with("https://") { - raw.trim_end_matches('/').to_string() - } else if raw.starts_with("http://") { - raw.replacen("http://", "https://", 1) - } else { - format!("https://{raw}") - }; - - let body = format!( - r#"User-agent: * -Disallow: /api/ -Disallow: /health -Disallow: /metrics -Disallow: /ws/ -Disallow: /avatar/ -Disallow: /blob/ -Disallow: /media/ -Disallow: /static/ -Disallow: /assets/ - -Sitemap: {sitemap_base}/sitemap.xml -"#, - ); - - HttpResponse::Ok() - .content_type("text/plain; charset=utf-8") - .body(body) -} diff --git a/libs/api/room/ai.rs b/libs/api/room/ai.rs deleted file mode 100644 index 6d0557f..0000000 --- a/libs/api/room/ai.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/ai", - params( - ("room_id" = Uuid, Path), - ), - responses( - (status = 200, description = "List room AI configurations", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn ai_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_ai_list(room_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - put, - path = "/api/rooms/{room_id}/ai", - params( - ("room_id" = Uuid, Path), - ), - request_body = room::RoomAiUpsertRequest, - responses( - (status = 200, description = "Upsert room AI configuration", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn ai_upsert( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_ai_upsert(room_id, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/rooms/{room_id}/ai/{model_id}", - params( - ("room_id" = Uuid, Path), - ("model_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Delete room AI configuration"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn ai_delete( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let (room_id, model_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .room_ai_delete(room_id, model_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} diff --git a/libs/api/room/category.rs b/libs/api/room/category.rs deleted file mode 100644 index 4970878..0000000 --- a/libs/api/room/category.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/project_room/{project_name}/room-categories", - params( - ("project_name" = String, Path), - ), - responses( - (status = 200, description = "List room categories", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn category_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_category_list(project_name, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/project_room/{project_name}/room-categories", - params( - ("project_name" = String, Path), - ), - request_body = room::RoomCategoryCreateRequest, - responses( - (status = 200, description = "Create room category", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn category_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_category_create(project_name, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/room-categories/{category_id}", - params( - ("category_id" = Uuid, Path), - ), - request_body = room::RoomCategoryUpdateRequest, - responses( - (status = 200, description = "Update room category", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn category_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let category_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_category_update(category_id, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/room-categories/{category_id}", - params( - ("category_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Delete room category"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn category_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let category_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .room_category_delete(category_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} diff --git a/libs/api/room/draft_and_history.rs b/libs/api/room/draft_and_history.rs deleted file mode 100644 index eea6b9f..0000000 --- a/libs/api/room/draft_and_history.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/messages/{message_id}/edit-history", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Get message edit history", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn message_edit_history( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let (_room_id, message_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .get_message_edit_history(message_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[derive(Debug, serde::Deserialize)] -pub struct MentionQuery { - pub limit: Option, -} - -#[utoipa::path( - get, - path = "/api/me/mentions", - params( - ("limit" = Option, Query), - ), - responses( - (status = 200, description = "List mentions", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn mention_list( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .get_mention_notifications(query.limit, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/me/mentions/read-all", - responses( - (status = 200, description = "Mark all mentions as read"), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn mention_read_all( - service: web::Data, - session: Session, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .mark_mention_notifications_read(&ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/draft", - params( - ("room_id" = Uuid, Path), - ), - request_body = room::DraftSaveRequest, - responses( - (status = 200, description = "Save draft", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn draft_save( - service: web::Data, - session: Session, - path: web::Path, - req: web::Json, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .draft_save(room_id, req.into_inner().content, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/rooms/{room_id}/draft", - params( - ("room_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Clear draft"), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn draft_clear( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .draft_clear(room_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} diff --git a/libs/api/room/member.rs b/libs/api/room/member.rs deleted file mode 100644 index 20e9a4b..0000000 --- a/libs/api/room/member.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/participants", - params( - ("room_id" = Uuid, Path), - ), - responses( - (status = 200, description = "List room participants", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn participant_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_participant_list(room_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/access", - params( - ("room_id" = Uuid, Path), - ), - request_body = room::RoomAccessGrantRequest, - responses( - (status = 200, description = "Grant room access"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn access_grant( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .room_access_grant(room_id, body.into_inner().user_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/rooms/{room_id}/access/{user_id}", - params( - ("room_id" = Uuid, Path), - ("user_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Revoke room access"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn access_revoke( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let (room_id, target_user_id) = path.into_inner(); - let actor_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(actor_id); - service - .room - .room_access_revoke(room_id, target_user_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/rooms/{room_id}/state/read-seq", - params( - ("room_id" = Uuid, Path), - ), - request_body = room::RoomMemberReadSeqRequest, - responses( - (status = 200, description = "Set read sequence", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn state_set_read_seq( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_user_state_update_read_seq(room_id, body.into_inner().last_read_seq, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/rooms/{room_id}/state/dnd", - params( - ("room_id" = Uuid, Path), - ), - request_body = room::RoomUserStateUpdateDndRequest, - responses( - (status = 200, description = "Update DND settings", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn state_update_dnd( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_user_state_update_dnd(room_id, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/room/message.rs b/libs/api/room/message.rs deleted file mode 100644 index e1e4e4c..0000000 --- a/libs/api/room/message.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use utoipa::IntoParams; -use uuid::Uuid; - -#[derive(Debug, serde::Deserialize, IntoParams)] -pub struct MessageListQuery { - pub before_seq: Option, - pub after_seq: Option, - pub limit: Option, -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/messages", - params( - ("room_id" = Uuid, Path), - ("before_seq" = Option, Query), - ("after_seq" = Option, Query), - ("limit" = Option, Query), - ), - responses( - (status = 200, description = "List room messages", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn message_list( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_message_list( - room_id, - query.before_seq, - query.after_seq, - query.limit, - &ctx, - ) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/messages", - params( - ("room_id" = Uuid, Path), - ), - request_body = room::RoomMessageCreateRequest, - responses( - (status = 200, description = "Create room message", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn message_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_message_create(room_id, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/rooms/{room_id}/messages/{message_id}", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ), - request_body = room::RoomMessageUpdateRequest, - responses( - (status = 200, description = "Update room message", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn message_update( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, - body: web::Json, -) -> Result { - let (_room_id, message_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_message_update(message_id, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/messages/{message_id}/revoke", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Revoke room message", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn message_revoke( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let (_room_id, message_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_message_revoke(message_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/messages/{message_id}", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Get room message", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn message_get( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let (_room_id, message_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_message_get(message_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/room/mod.rs b/libs/api/room/mod.rs deleted file mode 100644 index 9b7de4e..0000000 --- a/libs/api/room/mod.rs +++ /dev/null @@ -1,200 +0,0 @@ -pub mod ai; -pub mod category; -pub mod draft_and_history; -pub mod member; -pub mod message; -pub mod notification; -pub mod pin; -pub mod reaction; -pub mod room; -pub mod thread; -pub mod upload; - -use actix_web::web; - -pub fn init_room_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("") - .route( - "/project_room/{project_name}/rooms", - web::get().to(room::room_list), - ) - .route( - "/project_room/{project_name}/rooms", - web::post().to(room::room_create), - ) - .route( - "/project/{project_id}/presence", - web::get().to(room::project_presence), - ) - .route( - "/project_room/{project_name}/room-categories", - web::get().to(category::category_list), - ) - .route( - "/project_room/{project_name}/room-categories", - web::post().to(category::category_create), - ) - .route("/rooms/{room_id}", web::get().to(room::room_get)) - .route("/rooms/{room_id}", web::patch().to(room::room_update)) - .route("/rooms/{room_id}", web::delete().to(room::room_delete)) - .route( - "/rooms/{room_id}/messages", - web::get().to(message::message_list), - ) - .route( - "/rooms/{room_id}/messages", - web::post().to(message::message_create), - ) - .route( - "/rooms/{room_id}/messages/{message_id}", - web::patch().to(message::message_update), - ) - .route( - "/rooms/{room_id}/messages/{message_id}", - web::get().to(message::message_get), - ) - .route( - "/rooms/{room_id}/messages/{message_id}/revoke", - web::post().to(message::message_revoke), - ) - // room pins - .route("/rooms/{room_id}/pins", web::get().to(pin::pin_list)) - .route( - "/rooms/{room_id}/messages/{message_id}/pin", - web::post().to(pin::pin_add), - ) - .route( - "/rooms/{room_id}/messages/{message_id}/pin", - web::delete().to(pin::pin_remove), - ) - // room threads - .route( - "/rooms/{room_id}/threads", - web::get().to(thread::thread_list), - ) - .route( - "/rooms/{room_id}/threads", - web::post().to(thread::thread_create), - ) - .route( - "/rooms/{room_id}/threads/{thread_id}/messages", - web::get().to(thread::thread_messages), - ) - .route( - "/rooms/{room_id}/threads/{thread_id}/resolve", - web::post().to(thread::thread_resolve), - ) - .route( - "/rooms/{room_id}/threads/{thread_id}/archive", - web::post().to(thread::thread_archive), - ) - // room participants - .route( - "/rooms/{room_id}/participants", - web::get().to(member::participant_list), - ) - // room access (private rooms) - .route( - "/rooms/{room_id}/access", - web::post().to(member::access_grant), - ) - .route( - "/rooms/{room_id}/access/{user_id}", - web::delete().to(member::access_revoke), - ) - // room user state - .route( - "/rooms/{room_id}/state/read-seq", - web::patch().to(member::state_set_read_seq), - ) - .route( - "/rooms/{room_id}/state/dnd", - web::patch().to(member::state_update_dnd), - ) - // room reactions - .route( - "/rooms/{room_id}/messages/{message_id}/reactions", - web::post().to(reaction::reaction_add), - ) - .route( - "/rooms/{room_id}/messages/{message_id}/reactions/{emoji}", - web::delete().to(reaction::reaction_remove), - ) - .route( - "/rooms/{room_id}/messages/{message_id}/reactions", - web::get().to(reaction::reaction_get), - ) - // batch reactions - .route( - "/rooms/{room_id}/messages/reactions/batch", - web::get().to(reaction::reaction_batch), - ) - // message search - .route( - "/rooms/{room_id}/messages/search", - web::get().to(reaction::message_search), - ) - // message edit history - .route( - "/rooms/{room_id}/messages/{message_id}/edit-history", - web::get().to(draft_and_history::message_edit_history), - ) - // mention notifications - .route( - "/me/mentions", - web::get().to(draft_and_history::mention_list), - ) - .route( - "/me/mentions/read-all", - web::post().to(draft_and_history::mention_read_all), - ) - // room AI - .route("/rooms/{room_id}/ai", web::get().to(ai::ai_list)) - .route("/rooms/{room_id}/ai", web::put().to(ai::ai_upsert)) - .route( - "/rooms/{room_id}/ai/{model_id}", - web::delete().to(ai::ai_delete), - ) - // room category management - .route( - "/room-categories/{category_id}", - web::patch().to(category::category_update), - ) - .route( - "/room-categories/{category_id}", - web::delete().to(category::category_delete), - ) - .route( - "/me/notifications", - web::get().to(notification::notification_list), - ) - .route( - "/me/notifications/{notification_id}/read", - web::post().to(notification::notification_mark_read), - ) - .route( - "/me/notifications/read-all", - web::post().to(notification::notification_mark_all_read), - ) - .route( - "/me/notifications/{notification_id}/archive", - web::post().to(notification::notification_archive), - ) - // drafts - .route( - "/rooms/{room_id}/draft", - web::post().to(draft_and_history::draft_save), - ) - .route( - "/rooms/{room_id}/draft", - web::delete().to(draft_and_history::draft_clear), - ) - // file upload - .route("/rooms/{room_id}/upload", web::post().to(upload::upload)) - .route( - "/rooms/{room_id}/attachments/{attachment_id}", - web::get().to(upload::get_attachment), - ), - ); -} diff --git a/libs/api/room/notification.rs b/libs/api/room/notification.rs deleted file mode 100644 index dc597c6..0000000 --- a/libs/api/room/notification.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use utoipa::IntoParams; -use uuid::Uuid; - -#[derive(Debug, serde::Deserialize, IntoParams)] -pub struct NotificationListQuery { - pub only_unread: Option, - pub archived: Option, - pub limit: Option, -} - -#[utoipa::path( - get, - path = "/api/me/notifications", - params( - ("only_unread" = Option, Query), - ("archived" = Option, Query), - ("limit" = Option, Query), - ), - responses( - (status = 200, description = "List notifications", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn notification_list( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .notification_list(query.only_unread, query.archived, query.limit, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/me/notifications/{notification_id}/read", - params( - ("notification_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Mark notification as read"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn notification_mark_read( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let notification_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .notification_mark_read(notification_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} - -#[utoipa::path( - post, - path = "/api/me/notifications/read-all", - responses( - (status = 200, description = "Mark all notifications as read"), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn notification_mark_all_read( - service: web::Data, - session: Session, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let count = service - .room - .notification_mark_all_read(&ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(count).to_response()) -} - -#[utoipa::path( - post, - path = "/api/me/notifications/{notification_id}/archive", - params( - ("notification_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Archive notification"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn notification_archive( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let notification_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .notification_archive(notification_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} diff --git a/libs/api/room/pin.rs b/libs/api/room/pin.rs deleted file mode 100644 index fda1964..0000000 --- a/libs/api/room/pin.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use uuid::Uuid; - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/pins", - params( - ("room_id" = Uuid, Path), - ), - responses( - (status = 200, description = "List room pins", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn pin_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_pin_list(room_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/messages/{message_id}/pin", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Add room pin", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn pin_add( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let (_room_id, message_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_pin_add(message_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/rooms/{room_id}/messages/{message_id}/pin", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Remove room pin"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn pin_remove( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let (_room_id, message_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .room_pin_remove(message_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} diff --git a/libs/api/room/reaction.rs b/libs/api/room/reaction.rs deleted file mode 100644 index 0aae497..0000000 --- a/libs/api/room/reaction.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use utoipa::IntoParams; -use uuid::Uuid; - -#[derive(Debug, serde::Deserialize, IntoParams, utoipa::ToSchema)] -pub struct ReactionRequest { - pub emoji: String, -} - -#[derive(Debug, serde::Deserialize, IntoParams)] -pub struct MessageSearchQuery { - pub q: String, - pub limit: Option, - pub offset: Option, -} - -#[derive(Debug, serde::Deserialize, IntoParams)] -pub struct ReactionBatchQuery { - /// Comma-separated list of message IDs - pub message_ids: String, -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/messages/{message_id}/reactions", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ), - request_body = ReactionRequest, - responses( - (status = 200, description = "Add reaction", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn reaction_add( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, - body: web::Json, -) -> Result { - let (_room_id, message_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .message_reaction_add(message_id, body.into_inner().emoji, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/rooms/{room_id}/messages/{message_id}/reactions/{emoji}", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ("emoji" = String, Path), - ), - responses( - (status = 200, description = "Remove reaction", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn reaction_remove( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid, String)>, -) -> Result { - let (_room_id, message_id, emoji) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .message_reaction_remove(message_id, emoji, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/messages/{message_id}/reactions", - params( - ("room_id" = Uuid, Path), - ("message_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Get reactions", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn reaction_get( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let (_room_id, message_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .message_reactions_get(message_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/messages/reactions/batch", - params( - ("room_id" = Uuid, Path), - ("message_ids" = Vec, Query, description = "List of message IDs to fetch reactions for"), - ), - responses( - (status = 200, description = "Batch get reactions", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn reaction_batch( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let message_ids: Vec = query - .message_ids - .split(',') - .filter_map(|s| Uuid::parse_str(s.trim()).ok()) - .collect(); - let resp = service - .room - .message_reactions_batch(room_id, message_ids, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/messages/search", - params( - ("room_id" = Uuid, Path), - ("q" = String, Query), - ("limit" = Option, Query), - ("offset" = Option, Query), - ), - responses( - (status = 200, description = "Search messages", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn message_search( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let req = room::RoomMessageSearchRequest { - q: query.q.clone(), - start_time: None, - end_time: None, - sender_id: None, - content_type: None, - limit: query.limit, - offset: query.offset, - }; - let resp = service - .room - .room_message_search(room_id, req, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/room/room.rs b/libs/api/room/room.rs deleted file mode 100644 index 8fec3b6..0000000 --- a/libs/api/room/room.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::presence::PresenceChanged; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use utoipa::IntoParams; -use uuid::Uuid; - -#[derive(Debug, serde::Deserialize, IntoParams)] -pub struct RoomListQuery { - pub only_public: Option, -} - -#[utoipa::path( - get, - path = "/api/project_room/{project_name}/rooms", - params( - ("project_name" = String, Path), - ("only_public" = Option, Query), - ), - responses( - (status = 200, description = "List rooms", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Room" -)] -pub async fn room_list( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_list(project_name, query.only_public, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}", - params( - ("room_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Get room", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "Room" -)] -pub async fn room_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_get(room_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/project_room/{project_name}/rooms", - params( - ("project_name" = String, Path), - ), - request_body = room::RoomCreateRequest, - responses( - (status = 200, description = "Create room", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Room" -)] -pub async fn room_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_create(project_name, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/rooms/{room_id}", - params( - ("room_id" = Uuid, Path), - ), - request_body = room::RoomUpdateRequest, - responses( - (status = 200, description = "Update room", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Room" -)] -pub async fn room_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_update(room_id, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/rooms/{room_id}", - params( - ("room_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Delete room"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), -), - tag = "Room" -)] -pub async fn room_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - service - .room - .room_delete(room_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(true).to_response()) -} - -#[utoipa::path( - get, - path = "/api/project/{project_id}/presence", - params( - ("project_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Get project presence", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Presence" -)] -pub async fn project_presence( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - - // Check project access - service - .room - .check_project_member(project_id, user_id) - .await - .map_err(ApiError::from)?; - - let presence = service.room.get_project_presence(project_id); - Ok(ApiResponse::ok(presence).to_response()) -} diff --git a/libs/api/room/thread.rs b/libs/api/room/thread.rs deleted file mode 100644 index a5e0c03..0000000 --- a/libs/api/room/thread.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use room::ws_context::WsUserContext; -use service::AppService; -use session::Session; -use utoipa::IntoParams; -use uuid::Uuid; - -#[derive(Debug, serde::Deserialize, IntoParams)] -pub struct ThreadMessagesQuery { - pub before_seq: Option, - pub after_seq: Option, - pub limit: Option, -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/threads", - params( - ("room_id" = Uuid, Path), - ), - responses( - (status = 200, description = "List room threads", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn thread_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_thread_list(room_id, &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/threads", - params( - ("room_id" = Uuid, Path), - ), - request_body = room::RoomThreadCreateRequest, - responses( - (status = 200, description = "Create room thread", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn thread_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let room_id = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_thread_create(room_id, body.into_inner(), &ctx) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/threads/{thread_id}/messages", - params( - ("room_id" = Uuid, Path), - ("thread_id" = Uuid, Path), - ("before_seq" = Option, Query), - ("after_seq" = Option, Query), - ("limit" = Option, Query), - ), - responses( - (status = 200, description = "List thread messages", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn thread_messages( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, - query: web::Query, -) -> Result { - let (_room_id, thread_id) = path.into_inner(); - let user_id = session - .user() - .ok_or_else(|| ApiError::from(service::error::AppError::Unauthorized))?; - let ctx = WsUserContext::new(user_id); - let resp = service - .room - .room_thread_messages( - thread_id, - query.before_seq, - query.after_seq, - query.limit, - &ctx, - ) - .await - .map_err(ApiError::from)?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/threads/{thread_id}/resolve", - params( - ("room_id" = Uuid, Path), - ("thread_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Resolve thread"), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn thread_resolve( - _service: web::Data, - _session: Session, - _path: web::Path<(Uuid, Uuid)>, -) -> Result { - Ok(ApiResponse::ok(true).to_response()) -} - -#[utoipa::path( - post, - path = "/api/rooms/{room_id}/threads/{thread_id}/archive", - params( - ("room_id" = Uuid, Path), - ("thread_id" = Uuid, Path), - ), - responses( - (status = 200, description = "Archive thread"), - (status = 401, description = "Unauthorized"), - ), - tag = "Room" -)] -pub async fn thread_archive( - _service: web::Data, - _session: Session, - _path: web::Path<(Uuid, Uuid)>, -) -> Result { - Ok(ApiResponse::ok(true).to_response()) -} diff --git a/libs/api/room/upload.rs b/libs/api/room/upload.rs deleted file mode 100644 index 7179761..0000000 --- a/libs/api/room/upload.rs +++ /dev/null @@ -1,229 +0,0 @@ -use actix_multipart::Multipart; -use actix_web::http::header::{CONTENT_DISPOSITION, CONTENT_TYPE}; -use actix_web::{HttpResponse, Result, web}; -use chrono::Utc; -use futures_util::StreamExt; -use models::rooms::room_attachment; -use sea_orm::{ActiveModelTrait, EntityTrait, Set}; -use service::AppService; -use session::Session; -use uuid::Uuid; - -#[derive(Debug, serde::Serialize, utoipa::ToSchema)] -pub struct UploadResponse { - pub id: String, - pub url: String, - pub file_name: String, - pub file_size: i64, - pub content_type: String, -} - -fn sanitize_key(key: &str) -> String { - key.replace(['/', '\\', ':'], "_") -} - -fn extract_filename(disposition: &actix_web::http::header::HeaderValue) -> Option { - let s = disposition.to_str().ok()?; - // Simple parsing: extract filename from Content-Disposition header - // e.g., "form-data; filename="test.png"" - for part in s.split(';') { - let part = part.trim(); - if part.starts_with("filename=") { - let value = &part[9..].trim_matches('"'); - if !value.is_empty() { - return Some(value.to_string()); - } - } - } - None -} - -pub async fn upload( - service: web::Data, - session: Session, - path: web::Path, - mut payload: Multipart, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| crate::error::ApiError(service::error::AppError::Unauthorized))?; - - let storage = service.storage.as_ref().ok_or_else(|| { - crate::error::ApiError(service::error::AppError::BadRequest( - "Storage not configured".to_string(), - )) - })?; - - let room_id = path.into_inner(); - service - .room - .require_room_access(room_id, user_id) - .await - .map_err(crate::error::ApiError::from)?; - - let max_size = service.config.storage_max_file_size(); - - let mut file_data: Vec = Vec::new(); - let mut file_name = String::new(); - let mut content_type = "application/octet-stream".to_string(); - - while let Some(item) = payload.next().await { - let mut field = item.map_err(|e| { - crate::error::ApiError(service::error::AppError::BadRequest(e.to_string())) - })?; - - if let Some(disposition) = field.headers().get(&CONTENT_DISPOSITION) { - if let Some(name) = extract_filename(disposition) { - file_name = name; - } - } - if let Some(ct) = field.headers().get(&CONTENT_TYPE) { - if let Ok(ct_str) = ct.to_str() { - if !ct_str.is_empty() && ct_str != "application/octet-stream" { - content_type = ct_str.to_string(); - } - } - } - - while let Some(chunk) = field.next().await { - let data = chunk.map_err(|e| { - crate::error::ApiError(service::error::AppError::BadRequest(e.to_string())) - })?; - if file_data.len() + data.len() > max_size { - return Err(crate::error::ApiError( - service::error::AppError::BadRequest(format!( - "File exceeds maximum size of {} bytes", - max_size - )), - )); - } - file_data.extend_from_slice(&data); - } - } - - if file_data.is_empty() { - return Err(crate::error::ApiError( - service::error::AppError::BadRequest("No file provided".to_string()), - )); - } - - if file_name.is_empty() { - file_name = format!("upload_{}", Uuid::now_v7()); - } - - // Detect content type from file extension if still octet-stream - if content_type == "application/octet-stream" { - content_type = mime_guess2::from_path(&file_name) - .first_or_octet_stream() - .to_string(); - } - - let unique_name = format!("{}_{}", Uuid::now_v7(), sanitize_key(&file_name)); - let key = format!("rooms/{}/{}", room_id, unique_name); - let file_size = file_data.len() as i64; - - let _url = storage.upload(&key, file_data).await.map_err(|e| { - crate::error::ApiError(service::error::AppError::InternalServerError(e.to_string())) - })?; - - // Write to room_attachment table (message will be linked when message is created) - let attachment_id = Uuid::now_v7(); - let attachment: room_attachment::ActiveModel = room_attachment::ActiveModel { - id: Set(attachment_id), - room: Set(room_id), - message: Set(Uuid::nil()), - uploader: Set(user_id), - file_name: Set(file_name.clone()), - file_size: Set(file_size), - content_type: Set(content_type.clone()), - s3_key: Set(key), - created_at: Set(Utc::now()), - }; - attachment.insert(&service.db).await.map_err(|e| { - crate::error::ApiError(service::error::AppError::InternalServerError(e.to_string())) - })?; - - // Return the structured attachment URL instead of the /files/... path - // (the /files/... path has no handler on the API server) - let attachment_url = format!("/api/rooms/{}/attachments/{}", room_id, attachment_id); - - Ok(crate::ApiResponse::ok(UploadResponse { - id: attachment_id.to_string(), - url: attachment_url, - file_name, - file_size, - content_type, - }) - .to_response()) -} - -#[utoipa::path( - get, - path = "/api/rooms/{room_id}/attachments/{attachment_id}", - params( - ("room_id" = Uuid, Path, description = "Room ID"), - ("attachment_id" = Uuid, Path, description = "Attachment ID"), - ), - responses( - (status = 200, description = "Download file"), - (status = 401, description = "Unauthorized"), - (status = 403, description = "Not a room member"), - (status = 404, description = "Not found"), - ), - tag = "Room" -)] -pub async fn get_attachment( - service: web::Data, - session: Session, - path: web::Path<(Uuid, Uuid)>, -) -> Result { - let user_id = session - .user() - .ok_or_else(|| crate::error::ApiError(service::error::AppError::Unauthorized))?; - - let (room_id, attachment_id) = path.into_inner(); - - service - .room - .require_room_access(room_id, user_id) - .await - .map_err(crate::error::ApiError::from)?; - - let attachment = room_attachment::Entity::find_by_id(attachment_id) - .one(&service.db) - .await - .map_err(|e| { - crate::error::ApiError(service::error::AppError::InternalServerError(e.to_string())) - })? - .ok_or_else(|| { - crate::error::ApiError(service::error::AppError::NotFound( - "attachment not found".into(), - )) - })?; - - // Ensure the attachment belongs to the requested room - if attachment.room != room_id { - return Err(crate::error::ApiError(service::error::AppError::NotFound( - "attachment not found".into(), - ))); - } - - let storage = service.storage.as_ref().ok_or_else(|| { - crate::error::ApiError(service::error::AppError::InternalServerError( - "Storage not configured".to_string(), - )) - })?; - - let (data, content_type) = storage.read(&attachment.s3_key).await.map_err(|e| { - crate::error::ApiError(service::error::AppError::InternalServerError(e.to_string())) - })?; - - Ok(HttpResponse::Ok() - .content_type(content_type.clone()) - .insert_header((CONTENT_TYPE, content_type)) - .insert_header(( - CONTENT_DISPOSITION, - format!("inline; filename=\"{}\"", attachment.file_name), - )) - .body(data)) -} diff --git a/libs/api/route.rs b/libs/api/route.rs deleted file mode 100644 index 4b23586..0000000 --- a/libs/api/route.rs +++ /dev/null @@ -1,30 +0,0 @@ -use actix_web::web; - -pub fn init_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/ws") - .route("", web::get().to(transport::handler::ws::ws_handler)) - .route( - "/ai-stream/{room_id}/{message_id}", - web::get().to(transport::handler::sse::ws_ai_stream), - ), - ); - - cfg.service( - web::scope("/api") - .configure(crate::auth::init_auth_routes) - .configure(crate::git::init_git_routes) - .configure(crate::git::init_git_toplevel_routes) - .configure(crate::chat::init_chat_routes) - .configure(crate::issue::init_issue_routes) - .configure(crate::project::init_project_routes) - .configure(crate::user::init_user_routes) - .configure(crate::pull_request::init_pull_request_routes) - .configure(crate::agent::init_agent_routes) - .configure(crate::search::init_search_routes) - .configure(crate::room::init_room_routes), - ); - - // SPA fallback — must be registered last so /api/* takes precedence - cfg.route("/{path:.*}", web::get().to(crate::dist::serve_frontend)); -} diff --git a/libs/api/search/mod.rs b/libs/api/search/mod.rs deleted file mode 100644 index 9b00e9a..0000000 --- a/libs/api/search/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod service; - -use actix_web::web; - -pub fn init_search_routes(cfg: &mut web::ServiceConfig) { - cfg.route("/search", web::to(service::search)); - cfg.route("/search/messages", web::to(service::search_messages)); -} diff --git a/libs/api/search/service.rs b/libs/api/search/service.rs deleted file mode 100644 index 2786115..0000000 --- a/libs/api/search/service.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::ApiResponse; -use crate::error::ApiError; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::search::{ - GlobalMessageSearchQuery, GlobalMessageSearchResponse, SearchQuery, SearchResponse, -}; -use session::Session; - -#[utoipa::path( - get, - path = "/api/search", - params( - ("q" = String, Query, description = "Search keyword", min_length = 1, max_length = 200), - ("type" = Option, Query, description = "Comma-separated types: projects,repos,issues,users. Default: all"), - ("page" = Option, Query, description = "Page number, default 1"), - ("per_page" = Option, Query, description = "Results per page, default 20, max 100"), - ), - responses( - (status = 200, description = "Search results", body = ApiResponse), - (status = 400, description = "Bad request"), - (status = 401, description = "Unauthorized"), - ), - tag = "Search" -)] -pub async fn search( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let resp = service.search(&session, query.into_inner()).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/search/messages", - params( - ("q" = String, Query, description = "Search keyword", min_length = 1, max_length = 200), - ("page" = Option, Query, description = "Page number, default 1"), - ("per_page" = Option, Query, description = "Results per page, default 20, max 100"), - ("room" = Option, Query, description = "Scope search to a specific room by UUID"), - ("pn" = Option, Query, description = "Scope search to a specific project by name"), - ), - responses( - (status = 200, description = "Message search results across all accessible rooms", body = ApiResponse), - (status = 400, description = "Bad request"), - (status = 401, description = "Unauthorized"), - ), - tag = "Search" -)] -pub async fn search_messages( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let resp = service - .global_message_search(&session, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/sidemap.rs b/libs/api/sidemap.rs deleted file mode 100644 index 128ea5b..0000000 --- a/libs/api/sidemap.rs +++ /dev/null @@ -1,328 +0,0 @@ -use actix_web::{HttpResponse, web}; -use db::cache::AppCache; -use models::projects::project::{Column as PCol, Entity as PEntity}; -use models::repos::repo::{Column as RCol, Entity as REntity}; -use models::users::user::{Column as UCol, Entity as UEntity}; -use sea_orm::*; -use service::AppService; - -const CACHE_KEY_PREFIX: &str = "sidemap"; -const CACHE_TTL_SECS: u64 = 8 * 3600; // 8 hours, no refresh - -/// Returns the base URL, forcing https:// prefix for public sitemap crawlers. -fn public_base(config: &config::AppConfig) -> String { - let fallback = "https://gitdata.ai".to_string(); - let base = match config.main_domain() { - Ok(b) => b.trim_end_matches('/').to_string(), - Err(_) => fallback, - }; - if base.starts_with("https://") { - base - } else if base.starts_with("http://") { - base.replacen("http://", "https://", 1) - } else { - format!("https://{base}") - } -} - -// ── Handlers ────────────────────────────────────────────────────────────────── - -/// Main sitemap index referencing all sub-sitemaps. -pub async fn sitemap(service: web::Data) -> HttpResponse { - let base = public_base(&service.config); - - let xml = format!( - r#" - - - {base}/sidemap/static - - - {base}/sidemap/users - - - {base}/sidemap/projects - - - {base}/sidemap/repos - -"# - ); - - HttpResponse::Ok() - .content_type("application/xml; charset=utf-8") - .body(xml) -} - -/// Static routes (no DB, no cache). -pub async fn sitemap_static(service: web::Data) -> HttpResponse { - let base = public_base(&service.config); - - HttpResponse::Ok() - .content_type("application/xml; charset=utf-8") - .body(build_static_xml(&base)) -} - -/// User profiles sitemap. -pub async fn sitemap_users(service: web::Data) -> HttpResponse { - let base = public_base(&service.config); - - let xml = cached_or_build(&service.cache, "users", || async { - let db = service.db.reader(); - let users: Vec<(String, String)> = UEntity::find() - .filter(UCol::Username.ne("")) - .order_by_asc(UCol::Username) - .all(db) - .await - .unwrap_or_default() - .into_iter() - .map(|u| (u.username, u.updated_at.to_rfc3339())) - .collect(); - Ok(build_users_xml(&base, &users)) - }) - .await - .unwrap_or_else(|_| build_users_xml(&base, &[])); - - HttpResponse::Ok() - .content_type("application/xml; charset=utf-8") - .body(xml) -} - -/// Public projects sitemap. -pub async fn sitemap_projects(service: web::Data) -> HttpResponse { - let base = public_base(&service.config); - - let xml = cached_or_build(&service.cache, "projects", || async { - let db = service.db.reader(); - let projects: Vec<(String, String, String)> = PEntity::find() - .filter(PCol::IsPublic.eq(true)) - .order_by_asc(PCol::Name) - .all(db) - .await - .unwrap_or_default() - .into_iter() - .map(|p| (p.name, p.id.to_string(), p.updated_at.to_rfc3339())) - .collect(); - Ok(build_projects_xml(&base, &projects)) - }) - .await - .unwrap_or_else(|_| build_projects_xml(&base, &[])); - - HttpResponse::Ok() - .content_type("application/xml; charset=utf-8") - .body(xml) -} - -/// Public repos sitemap. -pub async fn sitemap_repos(service: web::Data) -> HttpResponse { - let base = public_base(&service.config); - - let xml = cached_or_build(&service.cache, "repos", || async { - let db = service.db.reader(); - - let project_map: std::collections::HashMap = PEntity::find() - .filter(PCol::IsPublic.eq(true)) - .all(db) - .await - .unwrap_or_default() - .into_iter() - .map(|p| (p.id.to_string(), p.name)) - .collect(); - - let repos: Vec<(String, String)> = REntity::find() - .filter(RCol::IsPrivate.eq(false)) - .order_by_asc(RCol::RepoName) - .all(db) - .await - .unwrap_or_default() - .into_iter() - .filter_map(|r| { - let ns = project_map.get(&r.project.to_string())?; - Some((format!("{ns}/{}", r.repo_name), r.updated_at.to_rfc3339())) - }) - .collect(); - - Ok(build_repos_xml(&base, &repos)) - }) - .await - .unwrap_or_else(|_| build_repos_xml(&base, &[])); - - HttpResponse::Ok() - .content_type("application/xml; charset=utf-8") - .body(xml) -} - -// ── Cache helpers ──────────────────────────────────────────────────────────────── - -async fn cached_or_build(cache: &AppCache, key: &str, build: F) -> Result -where - F: FnOnce() -> Fut, - Fut: std::future::Future>, -{ - let cache_key = format!("{CACHE_KEY_PREFIX}:{key}"); - - if let Ok(xml) = get_cached(cache, &cache_key).await { - return Ok(xml); - } - - let xml = build().await?; - - let _ = set_cached(cache, &cache_key, &xml).await; - - Ok(xml) -} - -async fn get_cached(cache: &AppCache, key: &str) -> Result { - let mut conn = cache.redis_pool().get().await.map_err(|e| { - tracing::debug!("sidemap redis get pool error: {}", e); - })?; - redis::cmd("GET") - .arg(key) - .query_async::(&mut conn) - .await - .map_err(|e| { - tracing::debug!("sidemap redis get error: {}", e); - }) -} - -async fn set_cached(cache: &AppCache, key: &str, value: &str) -> Result<(), ()> { - let mut conn = cache.redis_pool().get().await.map_err(|e| { - tracing::debug!("sidemap redis set pool error: {}", e); - })?; - redis::cmd("SETEX") - .arg(key) - .arg(CACHE_TTL_SECS) - .arg(value) - .query_async::<()>(&mut conn) - .await - .map_err(|e| { - tracing::debug!("sidemap redis set error: {}", e); - }) -} - -// ── XML builders ──────────────────────────────────────────────────────────────── - -fn build_static_xml(base: &str) -> String { - let mut xml = xml_header(); - for loc in [ - "/", - "/auth/login", - "/auth/register", - "/auth/password/reset", - "/auth/reset-password", - "/auth/verify-email", - "/about", - "/pricing", - "/pricing/enterprise", - "/pricing/faq", - "/skills", - "/skills/publish", - "/skills/docs", - "/solutions", - "/solutions/rooms", - "/solutions/memory", - "/solutions/governance", - "/network", - "/network/rooms", - "/network/api", - "/docs", - ] { - xml.push_str(&url_entry(&format!("{base}{loc}"), 0.9, "daily", None)); - } - xml.push_str(""); - xml -} - -fn build_users_xml(base: &str, users: &[(String, String)]) -> String { - let mut xml = xml_header(); - for (username, updated) in users { - xml.push_str(&url_entry( - &format!("{base}/user/{username}"), - 0.6, - "weekly", - Some(updated), - )); - } - xml.push_str(""); - xml -} - -fn build_projects_xml(base: &str, projects: &[(String, String, String)]) -> String { - let mut xml = xml_header(); - for (name, _, updated) in projects { - xml.push_str(&url_entry( - &format!("{base}/project/{name}"), - 0.7, - "weekly", - Some(updated), - )); - for sub in [ - "/activity", - "/repositories", - "/issues", - "/members", - "/articles", - "/resources", - ] { - xml.push_str(&url_entry( - &format!("{base}/project/{name}{sub}"), - 0.6, - "weekly", - Some(updated), - )); - } - } - xml.push_str(""); - xml -} - -fn build_repos_xml(base: &str, repos: &[(String, String)]) -> String { - let mut xml = xml_header(); - for (path, updated) in repos { - xml.push_str(&url_entry( - &format!("{base}/repository/{path}"), - 0.7, - "daily", - Some(updated), - )); - for sub in [ - "/files", - "/commits", - "/branches", - "/tags", - "/contributors", - "/pull-requests", - ] { - xml.push_str(&url_entry( - &format!("{base}/repository/{path}{sub}"), - 0.6, - "daily", - Some(updated), - )); - } - } - xml.push_str(""); - xml -} - -fn xml_header() -> String { - String::from( - r#" - -"#, - ) -} - -fn url_entry(loc: &str, priority: f32, changefreq: &str, updated: Option<&str>) -> String { - let updated_xml = updated - .map(|d| format!("\n {d}")) - .unwrap_or_default(); - format!( - r#" - {loc}{updated_xml} - {changefreq} - {priority} - -"#, - ) -} diff --git a/libs/api/skill.rs b/libs/api/skill.rs deleted file mode 100644 index 872ff4d..0000000 --- a/libs/api/skill.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! Skill management API endpoints. - -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -use crate::{ApiResponse, error::ApiError}; - -#[derive(serde::Deserialize, utoipa::IntoParams)] -pub struct SkillPath { - pub project_name: String, - pub slug: String, -} - -#[derive(Debug, serde::Deserialize, utoipa::IntoParams)] -pub struct SkillQuery { - pub source: Option, - pub enabled: Option, -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/skills", - params( - ("project_name" = String, Path), - ), - responses( - (status = 200, description = "List skills", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Project not found"), - ), - tag = "Skill" -)] -pub async fn skill_list( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let project_name = path.into_inner(); - let project = service.project_info(&session, project_name.clone()).await?; - - let q = service::skill::info::SkillListQuery { - source: query.source.clone(), - enabled: query.enabled, - }; - - let skills = service - .skill_list(project.uid.to_string(), q, &session) - .await?; - - Ok(ApiResponse::ok(skills).to_response()) -} - -#[utoipa::path( - get, - path = "/api/projects/{project_name}/skills/{slug}", - params( - ("project_name" = String, Path), - ("slug" = String, Path), - ), - responses( - (status = 200, description = "Get skill", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Skill" -)] -pub async fn skill_get( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let SkillPath { project_name, slug } = path.into_inner(); - - let project = service.project_info(&session, project_name.clone()).await?; - - let skill = service - .skill_get(project.uid.to_string(), slug, &session) - .await?; - - Ok(ApiResponse::ok(skill).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/skills", - params(("project_name" = String, Path)), - request_body = service::skill::manage::CreateSkillRequest, - responses( - (status = 200, description = "Create skill", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 409, description = "Skill already exists"), - ), - tag = "Skill" -)] -pub async fn skill_create( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let project_name = path.into_inner(); - let project = service.project_info(&session, project_name.clone()).await?; - - let skill = service - .skill_create(project.uid.to_string(), body.into_inner(), &session) - .await?; - - Ok(ApiResponse::ok(skill).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/projects/{project_name}/skills/{slug}", - params( - ("project_name" = String, Path), - ("slug" = String, Path), - ), - request_body = service::skill::manage::UpdateSkillRequest, - responses( - (status = 200, description = "Update skill", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Skill" -)] -pub async fn skill_update( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let SkillPath { project_name, slug } = path.into_inner(); - - let project = service.project_info(&session, project_name.clone()).await?; - - let skill = service - .skill_update(project.uid.to_string(), slug, body.into_inner(), &session) - .await?; - - Ok(ApiResponse::ok(skill).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/projects/{project_name}/skills/{slug}", - params( - ("project_name" = String, Path), - ("slug" = String, Path), - ), - responses( - (status = 200, description = "Delete skill", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "Skill" -)] -pub async fn skill_delete( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let SkillPath { project_name, slug } = path.into_inner(); - - let project = service.project_info(&session, project_name.clone()).await?; - - let result = service - .skill_delete(project.uid.to_string(), slug, &session) - .await?; - - Ok(ApiResponse::ok(result).to_response()) -} - -#[utoipa::path( - post, - path = "/api/projects/{project_name}/skills/scan", - params(("project_name" = String, Path)), - responses( - (status = 200, description = "Scan repos for skills", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "Skill" -)] -pub async fn skill_scan( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let project_name = path.into_inner(); - let project = service.project_info(&session, project_name).await?; - - let result = service.skill_scan_repos(project.uid, project.uid).await?; - - Ok(ApiResponse::ok(ScanResponse { - discovered: result.discovered, - created: result.created, - updated: result.updated, - removed: result.removed, - }) - .to_response()) -} - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct ScanResponse { - pub discovered: i64, - pub created: i64, - pub updated: i64, - pub removed: i64, -} diff --git a/libs/api/user/access_key.rs b/libs/api/user/access_key.rs deleted file mode 100644 index a9b1261..0000000 --- a/libs/api/user/access_key.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - post, - path = "/api/users/me/access-keys", - request_body = service::user::access_key::CreateAccessKeyParams, - responses( - (status = 200, description = "Create access key", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn create_access_key( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .user_create_access_key(&session, body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/access-keys", - responses( - (status = 200, description = "List access keys", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn list_access_keys( - service: web::Data, - session: Session, -) -> Result { - let resp = service.user_list_access_keys(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/users/me/access-keys/{access_key_id}", - params(("access_key_id" = i64, Path)), - responses( - (status = 200, description = "Delete access key"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn delete_access_key( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let id = path.into_inner(); - service.user_delete_access_key(&session, id).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} diff --git a/libs/api/user/avatar.rs b/libs/api/user/avatar.rs deleted file mode 100644 index 756c2c1..0000000 --- a/libs/api/user/avatar.rs +++ /dev/null @@ -1,76 +0,0 @@ -use actix_multipart::Multipart; -use actix_web::{HttpResponse, Result, web}; -use futures_util::StreamExt; -use service::AppService; -use session::Session; - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct AvatarUploadResponse { - pub avatar_url: String, -} - -/// Upload current user's avatar. -/// Accepts a multipart form with a single file field. -#[utoipa::path( - post, - path = "/api/users/me/avatar", - responses( - (status = 200, description = "Avatar uploaded", body = crate::ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "User" -)] -pub async fn upload_avatar( - service: web::Data, - session: Session, - mut payload: Multipart, -) -> Result { - let max_size: usize = 2 * 1024 * 1024; // 2MB - - let mut file_data: Vec = Vec::new(); - let mut file_ext = "png".to_string(); - - while let Some(item) = payload.next().await { - let mut field = item.map_err(|e| { - crate::error::ApiError(service::error::AppError::BadRequest(e.to_string())) - })?; - - // Detect file extension from content-type - if let Some(content_type) = field.content_type() { - let ext = match content_type.essence_str() { - "image/jpeg" | "image/jpg" => "jpg", - "image/gif" => "gif", - "image/webp" => "webp", - "image/png" | _ => "png", - }; - file_ext = ext.to_string(); - } - - while let Some(chunk) = field.next().await { - let data = chunk.map_err(|e| { - crate::error::ApiError(service::error::AppError::BadRequest(e.to_string())) - })?; - if file_data.len() + data.len() > max_size { - return Err(crate::error::ApiError( - service::error::AppError::BadRequest( - "File exceeds maximum size of 2MB".to_string(), - ), - )); - } - file_data.extend_from_slice(&data); - } - } - - if file_data.is_empty() { - return Err(crate::error::ApiError( - service::error::AppError::BadRequest("No file provided".to_string()), - )); - } - - let avatar_url = service - .user_avatar_upload(session, file_data, &file_ext) - .await - .map_err(crate::error::ApiError::from)?; - - Ok(crate::ApiResponse::ok(AvatarUploadResponse { avatar_url }).to_response()) -} diff --git a/libs/api/user/billing.rs b/libs/api/user/billing.rs deleted file mode 100644 index 543412a..0000000 --- a/libs/api/user/billing.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/me/billing", - responses( - (status = 200, description = "Get user billing", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "User" -)] -pub async fn user_billing( - service: web::Data, - session: Session, -) -> Result { - let resp = service.user_billing_current(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/billing/errors", - responses( - (status = 200, description = "Get user billing errors", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "User" -)] -pub async fn user_billing_errors( - service: web::Data, - session: Session, -) -> Result { - let resp = service.user_billing_errors(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/billing/history", - params(("page" = Option, Query), ("per_page" = Option, Query)), - responses( - (status = 200, description = "Get user billing history", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "User" -)] -pub async fn user_billing_history( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let resp = service - .user_billing_history(&session, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/chpc.rs b/libs/api/user/chpc.rs deleted file mode 100644 index 32f66fd..0000000 --- a/libs/api/user/chpc.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/{username}/heatmap", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get contribution heatmap", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_contribution_heatmap( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let username = path.into_inner(); - let resp = service - .get_user_contribution_heatmap(session, username, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/heatmap", - responses( - (status = 200, description = "Get my contribution heatmap", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn get_my_contribution_heatmap( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let resp = service - .get_current_user_contribution_heatmap(session, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/mod.rs b/libs/api/user/mod.rs deleted file mode 100644 index 1749e21..0000000 --- a/libs/api/user/mod.rs +++ /dev/null @@ -1,157 +0,0 @@ -pub mod access_key; -pub mod avatar; -pub mod billing; -pub mod chpc; -pub mod notification; -pub mod preferences; -pub mod profile; -pub mod projects; -pub mod repository; -pub mod ssh_key; -pub mod stars; -pub mod subscribe; -pub mod summary; -pub mod user_activity; -pub mod user_info; - -use actix_web::web; - -pub fn init_user_routes(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/users") - .route("/me/profile", web::get().to(profile::get_my_profile)) - .route("/me/profile", web::patch().to(profile::update_my_profile)) - .route("/me/avatar", web::post().to(avatar::upload_avatar)) - .route( - "/me/preferences", - web::get().to(preferences::get_preferences), - ) - .route( - "/me/preferences", - web::patch().to(preferences::update_preferences), - ) - .route("/me/keys", web::post().to(ssh_key::add_ssh_key)) - .route("/me/keys", web::get().to(ssh_key::list_ssh_keys)) - .route("/me/keys/{key_id}", web::get().to(ssh_key::get_ssh_key)) - .route( - "/me/keys/{key_id}", - web::patch().to(ssh_key::update_ssh_key), - ) - .route( - "/me/keys/{key_id}", - web::delete().to(ssh_key::delete_ssh_key), - ) - .route( - "/me/access-keys", - web::post().to(access_key::create_access_key), - ) - .route( - "/me/access-keys", - web::get().to(access_key::list_access_keys), - ) - .route( - "/me/access-keys/{access_key_id}", - web::delete().to(access_key::delete_access_key), - ) - .route( - "/me/notifications/preferences", - web::get().to(notification::get_notification_preferences), - ) - .route( - "/me/notifications/preferences", - web::patch().to(notification::update_notification_preferences), - ) - .route( - "/me/notifications/push/vapid-key", - web::get().to(notification::get_vapid_public_key), - ) - .route( - "/me/notifications/push/subscription", - web::delete().to(notification::unsubscribe_push), - ) - .route( - "/me/heatmap", - web::get().to(chpc::get_my_contribution_heatmap), - ) - .route("/me/billing", web::get().to(billing::user_billing)) - .route( - "/me/billing/errors", - web::get().to(billing::user_billing_errors), - ) - .route( - "/me/billing/history", - web::get().to(billing::user_billing_history), - ) - .route( - "/me/projects", - web::get().to(projects::get_current_user_projects), - ) - .route( - "/me/repos", - web::get().to(repository::get_current_user_repos), - ) - // /users/{username}/... - .route( - "/{username}", - web::get().to(profile::get_profile_by_username), - ) - .route("/{username}/info", web::get().to(user_info::get_user_info)) - .route( - "/{username}/summary", - web::get().to(summary::get_user_summary), - ) - .route( - "/{username}/heatmap", - web::get().to(chpc::get_contribution_heatmap), - ) - .route( - "/{username}/keys", - web::get().to(ssh_key::list_user_ssh_keys), - ) - .route( - "/{username}/activity", - web::get().to(user_activity::get_user_activity), - ) - .route("/{username}/stars", web::get().to(stars::get_user_stars)) - .route( - "/{username}/keys/{key_id}", - web::get().to(ssh_key::get_ssh_key), - ) - .route( - "/{username}/projects", - web::get().to(projects::get_user_projects), - ) - .route( - "/{username}/repos", - web::get().to(repository::get_user_repos), - ) - .route( - "/{username}/follow", - web::post().to(subscribe::subscribe_target), - ) - .route( - "/{username}/follow", - web::delete().to(subscribe::unsubscribe_target), - ) - .route( - "/{username}/follow", - web::get().to(subscribe::is_subscribed_to_target), - ) - .route( - "/{username}/followers", - web::get().to(subscribe::get_subscribers), - ) - .route( - "/{username}/following/count", - web::get().to(subscribe::get_subscription_count), - ) - .route( - "/{username}/following", - web::get().to(subscribe::get_following_list), - ) - .route( - "/{username}/followers/count", - web::get().to(subscribe::get_subscriber_count), - ), - ); -} diff --git a/libs/api/user/notification.rs b/libs/api/user/notification.rs deleted file mode 100644 index 55e3c6c..0000000 --- a/libs/api/user/notification.rs +++ /dev/null @@ -1,89 +0,0 @@ -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::error::AppError; -use session::Session; - -use crate::{ApiResponse, error::ApiError}; - -#[derive(serde::Serialize, utoipa::ToSchema)] -pub struct VapidKeyResponse { - pub public_key: String, -} - -#[utoipa::path( - get, - path = "/api/users/me/notifications/push/vapid-key", - responses( - (status = 200, description = "Get VAPID public key for push subscription", body = ApiResponse), - (status = 503, description = "Push notifications not configured"), - ), - tag = "User" -)] -pub async fn get_vapid_public_key( - service: web::Data, -) -> Result { - let public_key = service.config.vapid_public_key(); - let public_key = match public_key { - Some(k) => k, - None => { - let err: AppError = AppError::InternalError; - return Err(err.into()); - } - }; - Ok(ApiResponse::ok(VapidKeyResponse { public_key }).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/users/me/notifications/push/subscription", - responses( - (status = 200, description = "Unsubscribe from push notifications", body = ApiResponse), - (status = 401, description = "Unauthorized"), - ), - tag = "User" -)] -pub async fn unsubscribe_push( - service: web::Data, - session: Session, -) -> Result { - service.user_unsubscribe_push(&session).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/notifications/preferences", - responses( - (status = 200, description = "Get notification preferences", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn get_notification_preferences( - service: web::Data, - session: Session, -) -> Result { - let resp = service.user_get_notification_preferences(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/users/me/notifications/preferences", - request_body = service::user::notification::NotificationPreferencesParams, - responses( - (status = 200, description = "Update notification preferences", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn update_notification_preferences( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .user_update_notification_preferences(&session, body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/preferences.rs b/libs/api/user/preferences.rs deleted file mode 100644 index 4114e80..0000000 --- a/libs/api/user/preferences.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/me/preferences", - responses( - (status = 200, description = "Get user preferences", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn get_preferences( - service: web::Data, - session: Session, -) -> Result { - let resp = service.user_get_preferences(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/users/me/preferences", - request_body = service::user::preferences::PreferencesParams, - responses( - (status = 200, description = "Update user preferences", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn update_preferences( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .user_update_preferences(&session, body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/profile.rs b/libs/api/user/profile.rs deleted file mode 100644 index a6bf953..0000000 --- a/libs/api/user/profile.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/me/profile", - responses( - (status = 200, description = "Get current user profile", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn get_my_profile( - service: web::Data, - session: Session, -) -> Result { - let resp = service.user_get_current_profile(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/{username}", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get user profile", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_profile_by_username( - service: web::Data, - path: web::Path, -) -> Result { - let username = path.into_inner(); - let resp = service.user_get_profile_by_username(username).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/users/me/profile", - request_body = service::user::profile::UpdateProfileParams, - responses( - (status = 200, description = "Update current user profile", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn update_my_profile( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .user_update_profile(&session, body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/projects.rs b/libs/api/user/projects.rs deleted file mode 100644 index 01c2bb2..0000000 --- a/libs/api/user/projects.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/{username}/projects", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get user projects", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_user_projects( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let username = path.into_inner(); - let resp = service - .get_user_projects(session, username, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/projects", - responses( - (status = 200, description = "Get current user projects", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn get_current_user_projects( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let resp = service - .get_current_user_projects(session, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/repository.rs b/libs/api/user/repository.rs deleted file mode 100644 index 354c8cf..0000000 --- a/libs/api/user/repository.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/{username}/repos", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get user repos", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_user_repos( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let username = path.into_inner(); - let resp = service - .get_user_repos(session, username, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/repos", - responses( - (status = 200, description = "Get current user repos", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn get_current_user_repos( - service: web::Data, - session: Session, - query: web::Query, -) -> Result { - let resp = service - .get_current_user_repos(session, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/ssh_key.rs b/libs/api/user/ssh_key.rs deleted file mode 100644 index 3ffaf6f..0000000 --- a/libs/api/user/ssh_key.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - post, - path = "/api/users/me/keys", - request_body = service::user::ssh_key::AddSshKeyParams, - responses( - (status = 200, description = "Add SSH key", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn add_ssh_key( - service: web::Data, - session: Session, - body: web::Json, -) -> Result { - let resp = service - .user_add_ssh_key(&session, body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/keys", - responses( - (status = 200, description = "List SSH keys", body = ApiResponse), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn list_ssh_keys( - service: web::Data, - session: Session, -) -> Result { - let resp = service.user_list_ssh_keys(&session).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/me/keys/{key_id}", - params(("key_id" = i64, Path)), - responses( - (status = 200, description = "Get SSH key", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_ssh_key( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let resp = service - .user_get_ssh_key(&session, path.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - patch, - path = "/api/users/me/keys/{key_id}", - params(("key_id" = i64, Path)), - request_body = service::user::ssh_key::UpdateSshKeyParams, - responses( - (status = 200, description = "Update SSH key", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn update_ssh_key( - service: web::Data, - session: Session, - path: web::Path, - body: web::Json, -) -> Result { - let resp = service - .user_update_ssh_key(&session, path.into_inner(), body.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/users/me/keys/{key_id}", - params(("key_id" = i64, Path)), - responses( - (status = 200, description = "Delete SSH key"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn delete_ssh_key( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - service - .user_delete_ssh_key(&session, path.into_inner()) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/{username}/keys", - params( - ("username" = String, Path, description = "Username"), - ), - responses( - (status = 200, description = "List user's SSH keys", body = ApiResponse), - (status = 404, description = "User not found"), - ), - tag = "User" -)] -pub async fn list_user_ssh_keys( - service: web::Data, - path: web::Path, -) -> Result { - let username = path.into_inner(); - let resp = service.user_list_ssh_keys_by_username(username).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/stars.rs b/libs/api/user/stars.rs deleted file mode 100644 index 4e24bce..0000000 --- a/libs/api/user/stars.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/{username}/stars", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get user stars", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_user_stars( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let username = path.into_inner(); - let resp = service.get_user_stars(session, username).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/subscribe.rs b/libs/api/user/subscribe.rs deleted file mode 100644 index ae52623..0000000 --- a/libs/api/user/subscribe.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - post, - path = "/api/users/{username}/follow", - params(("username" = String, Path)), - responses( - (status = 200, description = "Follow user"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn subscribe_target( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let target = path.into_inner(); - service.user_subscribe_target(session, target).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - delete, - path = "/api/users/{username}/follow", - params(("username" = String, Path)), - responses( - (status = 200, description = "Unfollow user"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn unsubscribe_target( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let target = path.into_inner(); - service.user_unsubscribe_target(session, target).await?; - Ok(ApiResponse::ok(serde_json::json!({ "success": true })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/{username}/follow", - params(("username" = String, Path)), - responses( - (status = 200, description = "Check if following user"), - (status = 401, description = "Unauthorized"), -), - tag = "User" -)] -pub async fn is_subscribed_to_target( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let target = path.into_inner(); - let resp = service - .user_is_subscribed_to_target(session, target) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "is_subscribed": resp })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/{username}/followers", - params(("username" = String, Path)), - responses( - (status = 200, description = "List followers", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_subscribers( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let target = path.into_inner(); - let resp = service.user_get_subscribers(session, target).await?; - Ok(ApiResponse::ok(resp).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/{username}/following/count", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get following count"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_subscription_count( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let username = path.into_inner(); - let resp = service - .user_get_subscription_count(session, username) - .await?; - Ok(ApiResponse::ok(serde_json::json!({ "count": resp })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/{username}/followers/count", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get follower count"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_subscriber_count( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let username = path.into_inner(); - let resp = service.user_get_subscriber_count(session, username).await?; - Ok(ApiResponse::ok(serde_json::json!({ "count": resp })).to_response()) -} - -#[utoipa::path( - get, - path = "/api/users/{username}/following", - params(("username" = String, Path)), - responses( - (status = 200, description = "List following users", body = ApiResponse>), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_following_list( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let username = path.into_inner(); - let resp = service.user_get_following_list(session, username).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/summary.rs b/libs/api/user/summary.rs deleted file mode 100644 index e1336fc..0000000 --- a/libs/api/user/summary.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/{username}/summary", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get user summary for dashboard", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), - ), - tag = "User" -)] -pub async fn get_user_summary( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let username = path.into_inner(); - let resp = service.user_get_summary(session, username).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/user_activity.rs b/libs/api/user/user_activity.rs deleted file mode 100644 index 6c7fc4d..0000000 --- a/libs/api/user/user_activity.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use service::user::user_activity::UserActivityQuery; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/{username}/activity", - params( - ("username" = String, Path), - ("page" = Option, Query), - ("per_page" = Option, Query), - ), - responses( - (status = 200, description = "Get user activity", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_user_activity( - service: web::Data, - session: Session, - path: web::Path, - query: web::Query, -) -> Result { - let username = path.into_inner(); - let resp = service - .get_user_activity(session, username, query.into_inner()) - .await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/api/user/user_info.rs b/libs/api/user/user_info.rs deleted file mode 100644 index 2f3d24e..0000000 --- a/libs/api/user/user_info.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{ApiResponse, error::ApiError}; -use actix_web::{HttpResponse, Result, web}; -use service::AppService; -use session::Session; - -#[utoipa::path( - get, - path = "/api/users/{username}/info", - params(("username" = String, Path)), - responses( - (status = 200, description = "Get user info", body = ApiResponse), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Not found"), -), - tag = "User" -)] -pub async fn get_user_info( - service: web::Data, - session: Session, - path: web::Path, -) -> Result { - let username = path.into_inner(); - let resp = service.user_info(session, username).await?; - Ok(ApiResponse::ok(resp).to_response()) -} diff --git a/libs/avatar/Cargo.toml b/libs/avatar/Cargo.toml deleted file mode 100644 index 1148e5b..0000000 --- a/libs/avatar/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "avatar" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true -[lib] -path = "lib.rs" -name = "avatar" -[dependencies] -config = { workspace = true } -anyhow = { workspace = true } -image = { workspace = true } -serde = { workspace = true, features = ["derive"] } -tracing = { workspace = true } -[lints] -workspace = true diff --git a/libs/avatar/lib.rs b/libs/avatar/lib.rs deleted file mode 100644 index f7a9ba5..0000000 --- a/libs/avatar/lib.rs +++ /dev/null @@ -1,96 +0,0 @@ -use config::AppConfig; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Clone, Debug)] -pub struct AppAvatar { - pub basic_path: PathBuf, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct AvatarLoad { - w: Option, - h: Option, -} - -impl AppAvatar { - pub async fn init(cfg: &AppConfig) -> anyhow::Result { - let path = cfg.avatar_path()?; - if std::fs::read_dir(&path).is_err() { - if let Err(e) = std::fs::create_dir_all(&path) { - tracing::warn!(path = %path, error = %e, "Avatar directory not available, avatars disabled"); - } - } - let basic_path = PathBuf::from(path); - Ok(Self { basic_path }) - } - pub async fn upload(&self, file: Vec, file_name: String, ext: &str) -> anyhow::Result<()> { - // Validate file size (max 5MB) - if file.len() > 5 * 1024 * 1024 { - anyhow::bail!("File size exceeds 5MB limit"); - } - - // Validate extension - let allowed_exts = ["png", "jpg", "jpeg", "gif", "webp"]; - if !allowed_exts.contains(&ext) { - anyhow::bail!("Invalid file extension: {}", ext); - } - - // Sanitize filename to prevent path traversal - let sanitized_name = file_name.replace(['/', '\\', '.', ':'], "_"); - if sanitized_name.is_empty() || sanitized_name.len() > 255 { - anyhow::bail!("Invalid filename"); - } - - let image = image::load_from_memory(&*file)?; - image.save(self.basic_path.join(format!("{}.{}", sanitized_name, ext)))?; - Ok(()) - } - pub async fn load(&self, file_name: String, load: AvatarLoad) -> anyhow::Result> { - // Sanitize filename to prevent path traversal - let sanitized_name = file_name.replace(['/', '\\', '.', ':'], "_"); - if sanitized_name.is_empty() || sanitized_name.len() > 255 { - anyhow::bail!("Invalid filename"); - } - - let path = self.basic_path.join(format!("{}.png", sanitized_name)); - - // Verify path is within basic_path - let canonical_path = path.canonicalize().unwrap_or(path.clone()); - if !canonical_path.starts_with(&self.basic_path) { - anyhow::bail!("Path traversal detected"); - } - - let image = image::open(canonical_path)?; - let (w, h) = ( - load.w.unwrap_or(image.width()), - load.h.unwrap_or(image.height()), - ); - let image = image.resize(w, h, image::imageops::FilterType::Nearest); - Ok(image.as_bytes().to_vec()) - } - pub async fn delete(&self, file_name: String, ext: &str) -> anyhow::Result<()> { - // Validate extension - let allowed_exts = ["png", "jpg", "jpeg", "gif", "webp"]; - if !allowed_exts.contains(&ext) { - anyhow::bail!("Invalid file extension: {}", ext); - } - - // Sanitize filename to prevent path traversal - let sanitized_name = file_name.replace(['/', '\\', '.', ':'], "_"); - if sanitized_name.is_empty() || sanitized_name.len() > 255 { - anyhow::bail!("Invalid filename"); - } - - let path = self.basic_path.join(format!("{}.{}", sanitized_name, ext)); - - // Verify path is within basic_path - let canonical_path = path.canonicalize().unwrap_or(path.clone()); - if !canonical_path.starts_with(&self.basic_path) { - anyhow::bail!("Path traversal detected"); - } - - std::fs::remove_file(canonical_path)?; - Ok(()) - } -} diff --git a/libs/config/Cargo.toml b/libs/config/Cargo.toml deleted file mode 100644 index 7a44b88..0000000 --- a/libs/config/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "config" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true -[lib] -path = "lib.rs" -name = "config" -[dependencies] -dotenvy = { workspace = true } -anyhow = { workspace = true } -serde = { workspace = true, features = ["derive"] } -uuid = { workspace = true, features = ["v4"] } -num_cpus = { workspace = true } -tracing = { workspace = true } -[lints] -workspace = true diff --git a/libs/config/ai.rs b/libs/config/ai.rs deleted file mode 100644 index e646bb7..0000000 --- a/libs/config/ai.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn ai_basic_url(&self) -> anyhow::Result { - if let Some(url) = self.env.get("APP_AI_BASIC_URL") { - return Ok(url.to_string()); - } - Err(anyhow::anyhow!("APP_AI_BASIC_URL not found")) - } - pub fn ai_api_key(&self) -> anyhow::Result { - if let Some(api_key) = self.env.get("APP_AI_API_KEY") { - return Ok(api_key.to_string()); - } - Err(anyhow::anyhow!("APP_AI_API_KEY not found")) - } -} diff --git a/libs/config/app.rs b/libs/config/app.rs deleted file mode 100644 index 231a299..0000000 --- a/libs/config/app.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn app_name(&self) -> anyhow::Result { - if let Some(name) = self.env.get("APP_NAME") { - return Ok(name.to_string()); - } - Ok(env!("CARGO_PKG_NAME").to_string()) - } - - pub fn app_version(&self) -> anyhow::Result { - if let Some(version) = self.env.get("APP_VERSION") { - return Ok(version.to_string()); - } - Ok(env!("CARGO_PKG_VERSION").to_string()) - } - pub fn app_description(&self) -> anyhow::Result { - if let Some(description) = self.env.get("APP_DESCRIPTION") { - return Ok(description.to_string()); - } - Ok(env!("CARGO_PKG_DESCRIPTION").to_string()) - } -} diff --git a/libs/config/avatar.rs b/libs/config/avatar.rs deleted file mode 100644 index ddd09fe..0000000 --- a/libs/config/avatar.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn avatar_path(&self) -> anyhow::Result { - if let Some(url) = self.env.get("APP_AVATAR_PATH") { - return Ok(url.to_string()); - } - Err(anyhow::anyhow!("APP_AVATAR_PATH not found")) - } - - pub fn repos_root(&self) -> anyhow::Result { - if let Some(root) = self.env.get("APP_REPOS_ROOT") { - return Ok(root.to_string()); - } - Ok("/data/repos".to_string()) - } -} diff --git a/libs/config/database.rs b/libs/config/database.rs deleted file mode 100644 index a33b515..0000000 --- a/libs/config/database.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn database_url(&self) -> anyhow::Result { - if let Some(url) = self.env.get("APP_DATABASE_URL") { - return Ok(url.to_string()); - } - Err(anyhow::anyhow!("APP_DATABASE_URL not found")) - } - pub fn database_max_connections(&self) -> anyhow::Result { - if let Some(max_connections) = self.env.get("APP_DATABASE_MAX_CONNECTIONS") { - return Ok(max_connections.parse::()?); - } - Ok(10) - } - pub fn database_min_connections(&self) -> anyhow::Result { - if let Some(min_connections) = self.env.get("APP_DATABASE_MIN_CONNECTIONS") { - return Ok(min_connections.parse::()?); - } - Ok(2) - } - pub fn database_idle_timeout(&self) -> anyhow::Result { - if let Some(idle_timeout) = self.env.get("APP_DATABASE_IDLE_TIMEOUT") { - return Ok(idle_timeout.parse::()?); - } - Ok(600) // seconds - } - pub fn database_max_lifetime(&self) -> anyhow::Result { - if let Some(max_lifetime) = self.env.get("APP_DATABASE_MAX_LIFETIME") { - return Ok(max_lifetime.parse::()?); - } - Ok(3600) // seconds - } - pub fn database_connection_timeout(&self) -> anyhow::Result { - if let Some(connection_timeout) = self.env.get("APP_DATABASE_CONNECTION_TIMEOUT") { - return Ok(connection_timeout.parse::()?); - } - Ok(8) // seconds - } - pub fn database_schema_search_path(&self) -> anyhow::Result { - if let Some(schema_search_path) = self.env.get("APP_DATABASE_SCHEMA_SEARCH_PATH") { - return Ok(schema_search_path.to_string()); - } - Ok("public".to_string()) - } - pub fn database_read_replicas(&self) -> anyhow::Result> { - if let Some(replicas) = self.env.get("APP_DATABASE_REPLICAS") { - if replicas.is_empty() { - return Ok(None); - } - return Ok(Some(replicas.to_string())); - } - Ok(None) - } - pub fn database_health_check_interval(&self) -> anyhow::Result { - if let Some(interval) = self.env.get("APP_DATABASE_HEALTH_CHECK_INTERVAL") { - return Ok(interval.parse::()?); - } - Ok(30) - } - pub fn database_retry_attempts(&self) -> anyhow::Result { - if let Some(attempts) = self.env.get("APP_DATABASE_RETRY_ATTEMPTS") { - return Ok(attempts.parse::()?); - } - Ok(3) - } - pub fn database_retry_delay(&self) -> anyhow::Result { - if let Some(delay) = self.env.get("APP_DATABASE_RETRY_DELAY") { - return Ok(delay.parse::()?); - } - Ok(5) - } -} diff --git a/libs/config/domain.rs b/libs/config/domain.rs deleted file mode 100644 index 702d272..0000000 --- a/libs/config/domain.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn main_domain(&self) -> anyhow::Result { - if let Some(domain_url) = self.env.get("APP_DOMAIN_URL") { - return Ok(domain_url.to_string()); - } - Ok("http://127.0.0.1".to_string()) - } - - pub fn static_domain(&self) -> anyhow::Result { - if let Some(static_domain) = self.env.get("APP_STATIC_DOMAIN") { - return Ok(static_domain.to_string()); - } - self.main_domain() - } - pub fn media_domain(&self) -> anyhow::Result { - if let Some(media_domain) = self.env.get("APP_MEDIA_DOMAIN") { - return Ok(media_domain.to_string()); - } - self.main_domain() - } - pub fn git_http_domain(&self) -> anyhow::Result { - if let Some(git_http_domain) = self.env.get("APP_GIT_HTTP_DOMAIN") { - return Ok(git_http_domain.to_string()); - } - self.main_domain() - } -} diff --git a/libs/config/embed.rs b/libs/config/embed.rs deleted file mode 100644 index 8a816b4..0000000 --- a/libs/config/embed.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn get_embed_model_base_url(&self) -> anyhow::Result { - if let Some(url) = self.env.get("APP_EMBED_MODEL_BASE_URL") { - return Ok(url.to_string()); - } - Err(anyhow::anyhow!("APP_EMBED_MODEL_BASE_URL not found")) - } - pub fn get_embed_model_dimensions(&self) -> anyhow::Result { - if let Some(dimensions) = self.env.get("APP_EMBED_MODEL_DIMENSIONS") { - return Ok(dimensions.parse::()?); - } - Err(anyhow::anyhow!("APP_EMBED_MODEL_DIMENSIONS not found")) - } - pub fn get_embed_model_api_key(&self) -> anyhow::Result { - if let Some(api_key) = self.env.get("APP_EMBED_MODEL_API_KEY") { - return Ok(api_key.to_string()); - } - Err(anyhow::anyhow!("APP_EMBED_MODEL_API_KEY not found")) - } - pub fn get_embed_model_name(&self) -> anyhow::Result { - if let Some(model_name) = self.env.get("APP_EMBED_MODEL_NAME") { - return Ok(model_name.to_string()); - } - Err(anyhow::anyhow!("APP_EMBED_MODEL_NAME not found")) - } - pub fn get_qdrant_url(&self) -> anyhow::Result { - if let Some(url) = self.env.get("APP_QDRANT_URL") { - return Ok(url.to_string()); - } - Err(anyhow::anyhow!("APP_QDRANT_URL not found")) - } - pub fn get_qdrant_api_key(&self) -> Option { - self.env.get("APP_QDRANT_API_KEY").map(|s| s.to_string()) - } -} diff --git a/libs/config/hook.rs b/libs/config/hook.rs deleted file mode 100644 index dba3539..0000000 --- a/libs/config/hook.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::AppConfig; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PoolConfig { - /// Intended concurrency (used by K8s operator/HPA, not the worker itself). - /// The worker is single-threaded by design; K8s replicas provide parallelism. - pub max_concurrent: usize, - pub cpu_threshold: f32, - /// Hash-tag-prefixed Redis key prefix for hook task queues. - /// Example: "{hook}" — full keys will be "{hook}:sync", "{hook}:sync:work", etc. - pub redis_list_prefix: String, - /// Redis channel for task logs (PubSub). - pub redis_log_channel: String, - /// BLMOVE blocking timeout in seconds (0 = infinite). - pub redis_block_timeout_secs: u64, - /// Max retry attempts before discarding a failed task. - pub redis_max_retries: usize, - pub worker_id: String, -} - -impl PoolConfig { - pub fn from_env(config: &AppConfig) -> Self { - let max_concurrent = config - .env - .get("HOOK_POOL_MAX_CONCURRENT") - .and_then(|v| v.parse().ok()) - .unwrap_or_else(num_cpus::get); - - let cpu_threshold = config - .env - .get("HOOK_POOL_CPU_THRESHOLD") - .and_then(|v| v.parse().ok()) - .unwrap_or(80.0); - - let redis_list_prefix = config - .env - .get("HOOK_POOL_REDIS_LIST_PREFIX") - .cloned() - .unwrap_or_else(|| "{hook}".to_string()); - - let redis_log_channel = config - .env - .get("HOOK_POOL_REDIS_LOG_CHANNEL") - .cloned() - .unwrap_or_else(|| "hook:logs".to_string()); - - let redis_block_timeout_secs = config - .env - .get("HOOK_POOL_REDIS_BLOCK_TIMEOUT") - .and_then(|v| v.parse().ok()) - .unwrap_or(5); - - let redis_max_retries = config - .env - .get("HOOK_POOL_REDIS_MAX_RETRIES") - .and_then(|v| v.parse().ok()) - .unwrap_or(3); - - let worker_id = config - .env - .get("HOOK_POOL_WORKER_ID") - .cloned() - .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - - Self { - max_concurrent, - cpu_threshold, - redis_list_prefix, - redis_log_channel, - redis_block_timeout_secs, - redis_max_retries, - worker_id, - } - } -} - -impl Default for PoolConfig { - fn default() -> Self { - Self { - max_concurrent: num_cpus::get(), - cpu_threshold: 80.0, - redis_list_prefix: "{hook}".to_string(), - redis_log_channel: "hook:logs".to_string(), - redis_block_timeout_secs: 5, - redis_max_retries: 3, - worker_id: uuid::Uuid::new_v4().to_string(), - } - } -} diff --git a/libs/config/lib.rs b/libs/config/lib.rs deleted file mode 100644 index 86549c5..0000000 --- a/libs/config/lib.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::collections::HashMap; -use std::sync::OnceLock; - -pub static GLOBAL_CONFIG: OnceLock = OnceLock::new(); - -#[derive(Clone, Debug)] -pub struct AppConfig { - pub env: HashMap, -} - -impl AppConfig { - const ENV_FILES: &'static [&'static str] = &[".env", ".env.local"]; - pub fn load() -> AppConfig { - let mut env = HashMap::new(); - for env_file in AppConfig::ENV_FILES { - if let Err(e) = dotenvy::from_path(env_file) { - tracing::debug!(file = %env_file, error = %e, "dotenv load skipped"); - } - if let Ok(env_file_content) = std::fs::read_to_string(env_file) { - for line in env_file_content.lines() { - if let Some((key, value)) = line.split_once('=') { - env.insert(key.to_string(), value.to_string()); - } - } - } - } - // Environment variables (e.g. K8s injected APP_DOMAIN_URL) take precedence over .env files - env = std::env::vars().chain(env).collect(); - let this = AppConfig { env }; - // Handle the race condition: if another thread already set the global, return it. - // This is safe because config is immutable after load. - if GLOBAL_CONFIG.get().is_some() { - GLOBAL_CONFIG.get().unwrap().clone() - } else { - let _ = GLOBAL_CONFIG.set(this); - GLOBAL_CONFIG - .get() - .expect("global config should be set after load") - .clone() - } - } -} - -pub mod ai; -pub mod app; -pub mod avatar; -pub mod database; -pub mod domain; -pub mod embed; -pub mod hook; -pub mod logs; -pub mod nats; -pub mod qdrant; -pub mod redis; -pub mod smtp; -pub mod ssh; -pub mod storage; diff --git a/libs/config/logs.rs b/libs/config/logs.rs deleted file mode 100644 index 60eaccf..0000000 --- a/libs/config/logs.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn log_level(&self) -> anyhow::Result { - if let Some(level) = self.env.get("APP_LOG_LEVEL") { - return Ok(level.to_string()); - } - Ok("info".to_string()) - } - - pub fn log_format(&self) -> anyhow::Result { - if let Some(format) = self.env.get("APP_LOG_FORMAT") { - return Ok(format.to_string()); - } - Ok("json".to_string()) - } - - pub fn log_file_enabled(&self) -> anyhow::Result { - if let Some(enabled) = self.env.get("APP_LOG_FILE_ENABLED") { - return Ok(enabled.parse::()?); - } - Ok(false) - } - - pub fn log_file_path(&self) -> anyhow::Result { - if let Some(path) = self.env.get("APP_LOG_FILE_PATH") { - return Ok(path.to_string()); - } - Ok("./logs".to_string()) - } - - pub fn log_file_rotation(&self) -> anyhow::Result { - if let Some(rotation) = self.env.get("APP_LOG_FILE_ROTATION") { - return Ok(rotation.to_string()); - } - Ok("daily".to_string()) - } - - pub fn log_file_max_files(&self) -> anyhow::Result { - if let Some(max_files) = self.env.get("APP_LOG_FILE_MAX_FILES") { - return Ok(max_files.parse::()?); - } - Ok(7) - } - - pub fn log_file_max_size(&self) -> anyhow::Result { - if let Some(max_size) = self.env.get("APP_LOG_FILE_MAX_SIZE") { - return Ok(max_size.parse::()?); - } - Ok(104857600) // 100MB - } - - pub fn otel_enabled(&self) -> anyhow::Result { - if let Some(enabled) = self.env.get("APP_OTEL_ENABLED") { - return Ok(enabled.parse::()?); - } - Ok(false) - } - - pub fn otel_endpoint(&self) -> anyhow::Result { - if let Some(endpoint) = self.env.get("APP_OTEL_ENDPOINT") { - return Ok(endpoint.to_string()); - } - Ok("http://localhost:5080/api/default/v1/traces".to_string()) - } - - pub fn otel_service_name(&self) -> anyhow::Result { - if let Some(service_name) = self.env.get("APP_OTEL_SERVICE_NAME") { - return Ok(service_name.to_string()); - } - Ok(env!("CARGO_PKG_NAME").to_string()) - } - - pub fn otel_service_version(&self) -> anyhow::Result { - if let Some(service_version) = self.env.get("APP_OTEL_SERVICE_VERSION") { - return Ok(service_version.to_string()); - } - Ok(env!("CARGO_PKG_VERSION").to_string()) - } - - pub fn otel_authorization(&self) -> anyhow::Result> { - if let Some(authorization) = self.env.get("APP_OTEL_AUTHORIZATION") { - return Ok(Some(authorization.to_string())); - } - Ok(None) - } - - pub fn otel_organization(&self) -> anyhow::Result> { - if let Some(organization) = self.env.get("APP_OTEL_ORGANIZATION") { - return Ok(Some(organization.to_string())); - } - Ok(None) - } -} diff --git a/libs/config/nats.rs b/libs/config/nats.rs deleted file mode 100644 index b8f04ee..0000000 --- a/libs/config/nats.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn nats_url(&self) -> Option { - self.env.get("NATS_URL").cloned() - } - - pub fn nats_token(&self) -> Option { - self.env.get("NATS_TOKEN").cloned() - } - - pub fn nats_stream_name(&self) -> String { - self.env - .get("NATS_STREAM_NAME") - .cloned() - .unwrap_or_else(|| "ROOM_EVENTS".to_string()) - } - - pub fn nats_max_deliver(&self) -> i64 { - self.env - .get("NATS_MAX_DELIVER") - .and_then(|v| v.parse().ok()) - .unwrap_or(3) - } - - pub fn nats_ack_wait_secs(&self) -> u64 { - self.env - .get("NATS_ACK_WAIT_SECS") - .and_then(|v| v.parse().ok()) - .unwrap_or(10) - } - - pub fn nats_max_age_secs(&self) -> u64 { - self.env - .get("NATS_MAX_AGE_SECS") - .and_then(|v| v.parse().ok()) - .unwrap_or(86_400) - } - - pub fn nats_buffer_size(&self) -> usize { - self.env - .get("NATS_BUFFER_SIZE") - .and_then(|v| v.parse().ok()) - .unwrap_or(256) - } - - pub fn nats_is_enabled(&self) -> bool { - self.nats_url().is_some() && self.nats_token().is_some() - } -} diff --git a/libs/config/qdrant.rs b/libs/config/qdrant.rs deleted file mode 100644 index a507f08..0000000 --- a/libs/config/qdrant.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn qdrant_url(&self) -> anyhow::Result { - if let Some(url) = self.env.get("APP_QDRANT_URL") { - return Ok(url.to_string()); - } - Err(anyhow::anyhow!("APP_QDRANT_URL not found")) - } - - pub fn qdrant_api_key(&self) -> anyhow::Result> { - if let Some(api_key) = self.env.get("APP_QDRANT_API_KEY") { - return Ok(Some(api_key.to_string())); - } - Ok(None) - } -} diff --git a/libs/config/redis.rs b/libs/config/redis.rs deleted file mode 100644 index a391630..0000000 --- a/libs/config/redis.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - /// Returns a single Redis URL (first from APP_REDIS_URLS or APP_REDIS_URL). - pub fn redis_url(&self) -> anyhow::Result { - let urls = self.redis_urls()?; - urls.into_iter() - .next() - .ok_or_else(|| anyhow::anyhow!("APP_REDIS_URLS or APP_REDIS_URL is empty")) - } - - pub fn redis_urls(&self) -> anyhow::Result> { - if let Some(urls) = self.env.get("APP_REDIS_URLS") { - return Ok(urls.split(',').map(|s| s.trim().to_string()).collect()); - } - if let Some(url) = self.env.get("APP_REDIS_URL") { - return Ok(vec![url.to_string()]); - } - Err(anyhow::anyhow!("APP_REDIS_URLS or APP_REDIS_URL not found")) - } - - pub fn redis_pool_size(&self) -> anyhow::Result { - if let Some(pool_size) = self.env.get("APP_REDIS_POOL_SIZE") { - return Ok(pool_size.parse::()?); - } - Ok(10) - } - - pub fn redis_connect_timeout(&self) -> anyhow::Result { - if let Some(timeout) = self.env.get("APP_REDIS_CONNECT_TIMEOUT") { - return Ok(timeout.parse::()?); - } - Ok(5) - } - - pub fn redis_acquire_timeout(&self) -> anyhow::Result { - if let Some(timeout) = self.env.get("APP_REDIS_ACQUIRE_TIMEOUT") { - return Ok(timeout.parse::()?); - } - Ok(5) - } -} diff --git a/libs/config/smtp.rs b/libs/config/smtp.rs deleted file mode 100644 index 77fab34..0000000 --- a/libs/config/smtp.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn smtp_host(&self) -> anyhow::Result { - if let Some(host) = self.env.get("APP_SMTP_HOST") { - return Ok(host.to_string()); - } - Err(anyhow::anyhow!("APP_SMTP_HOST not found")) - } - - pub fn smtp_port(&self) -> anyhow::Result { - if let Some(port) = self.env.get("APP_SMTP_PORT") { - return Ok(port.parse::()?); - } - Ok(587) - } - - pub fn smtp_username(&self) -> anyhow::Result { - if let Some(username) = self.env.get("APP_SMTP_USERNAME") { - return Ok(username.to_string()); - } - Err(anyhow::anyhow!("APP_SMTP_USERNAME not found")) - } - - pub fn smtp_password(&self) -> anyhow::Result { - if let Some(password) = self.env.get("APP_SMTP_PASSWORD") { - return Ok(password.to_string()); - } - Err(anyhow::anyhow!("APP_SMTP_PASSWORD not found")) - } - - pub fn smtp_from(&self) -> anyhow::Result { - if let Some(from) = self.env.get("APP_SMTP_FROM") { - return Ok(from.to_string()); - } - Err(anyhow::anyhow!("APP_SMTP_FROM not found")) - } - - pub fn smtp_tls(&self) -> anyhow::Result { - if let Some(tls) = self.env.get("APP_SMTP_TLS") { - return Ok(tls.parse::()?); - } - Ok(true) - } - - pub fn smtp_timeout(&self) -> anyhow::Result { - if let Some(timeout) = self.env.get("APP_SMTP_TIMEOUT") { - return Ok(timeout.parse::()?); - } - Ok(30) - } -} diff --git a/libs/config/ssh.rs b/libs/config/ssh.rs deleted file mode 100644 index a767c11..0000000 --- a/libs/config/ssh.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn ssh_domain(&self) -> anyhow::Result { - if let Some(ssh_domain) = self.env.get("APP_SSH_DOMAIN") { - return Ok(ssh_domain.to_string()); - } - let main_domain = self.main_domain()?; - if let Some(stripped) = main_domain.strip_prefix("https://") { - Ok(stripped.to_string()) - } else if let Some(stripped) = main_domain.strip_prefix("http://") { - Ok(stripped.to_string()) - } else { - Ok(main_domain) - } - } - - pub fn ssh_port(&self) -> anyhow::Result { - if let Some(ssh_port) = self.env.get("APP_SSH_PORT") { - return Ok(ssh_port.parse::()?); - } - Ok(8022) - } - - pub fn ssh_server_private_key(&self) -> anyhow::Result { - if let Some(private_key) = self.env.get("APP_SSH_SERVER_PRIVATE_KEY") { - return Ok(private_key.to_string()); - } - Ok("".to_string()) - } - - pub fn ssh_server_public_key(&self) -> anyhow::Result { - if let Some(public_key) = self.env.get("APP_SSH_SERVER_PUBLIC_KEY") { - return Ok(public_key.to_string()); - } - Ok("".to_string()) - } -} diff --git a/libs/config/storage.rs b/libs/config/storage.rs deleted file mode 100644 index 8f2584c..0000000 --- a/libs/config/storage.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::AppConfig; - -impl AppConfig { - pub fn storage_path(&self) -> String { - self.env - .get("STORAGE_PATH") - .cloned() - .unwrap_or_else(|| "/data/files".to_string()) - } - - pub fn storage_public_url(&self) -> String { - self.env - .get("STORAGE_PUBLIC_URL") - .cloned() - .unwrap_or_else(|| "/files".to_string()) - } - - pub fn storage_max_file_size(&self) -> usize { - self.env - .get("STORAGE_MAX_FILE_SIZE") - .and_then(|s| s.parse::().ok()) - .unwrap_or(10 * 1024 * 1024) // 10MB default - } - - pub fn vapid_public_key(&self) -> Option { - self.env.get("VAPID_PUBLIC_KEY").cloned() - } - - pub fn vapid_private_key(&self) -> Option { - self.env.get("VAPID_PRIVATE_KEY").cloned() - } - - pub fn vapid_sender_email(&self) -> String { - self.env - .get("VAPID_SENDER_EMAIL") - .cloned() - .unwrap_or_else(|| "mailto:admin@example.com".to_string()) - } -} diff --git a/libs/db/Cargo.toml b/libs/db/Cargo.toml deleted file mode 100644 index 51d4834..0000000 --- a/libs/db/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "db" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true -[lib] -path = "lib.rs" -name = "db" -[dependencies] -sea-orm = { workspace = true, features = ["sqlx-all", "runtime-tokio"] } -deadpool-redis = { workspace = true, features = ["rt_tokio_1", "cluster-async", "cluster"] } -config = { workspace = true } -anyhow = { workspace = true } -tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } -async-trait = { workspace = true } -serde_json = { workspace = true } -redis = { workspace = true } -uuid = { workspace = true } -chrono = { workspace = true } -[lints] -workspace = true diff --git a/libs/db/cache.rs b/libs/db/cache.rs deleted file mode 100644 index 2e849ad..0000000 --- a/libs/db/cache.rs +++ /dev/null @@ -1,178 +0,0 @@ -use config::AppConfig; -use deadpool_redis::cluster::{Connection, Manager, Pool}; -use uuid::Uuid; - -const CHAT_STREAM_KEY_PREFIX: &str = "chat:stream:"; -const CHAT_STREAM_TTL_SECS: u64 = 600; // 10 minutes - -#[derive(Clone)] -pub struct AppCache { - pool: Pool, - redis_url: String, -} - -impl AppCache { - pub async fn init(cfg: &AppConfig) -> anyhow::Result { - let urls = cfg.redis_urls()?; - let pool_size = cfg.redis_pool_size()?; - let conn = Manager::new(urls.clone(), true)?; - let pool = deadpool_redis::cluster::Pool::builder(conn) - .max_size(pool_size as usize) - .build()?; - let redis_url = urls - .first() - .cloned() - .unwrap_or_else(|| "redis://127.0.0.1:6379".to_string()); - - Ok(Self { pool, redis_url }) - } - pub async fn conn(&self) -> anyhow::Result { - Ok(self.pool.get().await?) - } - - pub fn redis_pool(&self) -> &Pool { - &self.pool - } - - pub fn redis_url(&self) -> &str { - &self.redis_url - } - - /// Set chat stream active state (conversation_id → {message_id, started_at}). - /// TTL 10 minutes — prevents stale entries. - pub async fn set_chat_stream_active(&self, conversation_id: Uuid, message_id: Uuid) -> bool { - if let Ok(mut conn) = self.conn().await { - let key = format!("{}{}", CHAT_STREAM_KEY_PREFIX, conversation_id); - let value = serde_json::json!({ - "message_id": message_id.to_string(), - "started_at": chrono::Utc::now().timestamp(), - }) - .to_string(); - let _: Result<(), _> = redis::cmd("SETEX") - .arg(&key) - .arg(CHAT_STREAM_TTL_SECS as i64) - .arg(&value) - .query_async(&mut conn) - .await; - return true; - } - false - } - - /// Get active chat stream state for a conversation. Returns (message_id, started_at_ts). - pub async fn get_chat_stream_active(&self, conversation_id: Uuid) -> Option<(Uuid, i64)> { - if let Ok(mut conn) = self.conn().await { - let key = format!("{}{}", CHAT_STREAM_KEY_PREFIX, conversation_id); - if let Ok(value) = redis::cmd("GET") - .arg(&key) - .query_async::(&mut conn) - .await - { - if let Ok(parsed) = serde_json::from_str::(&value) { - let msg_id = parsed - .get("message_id") - .and_then(|v| v.as_str()) - .and_then(|s| Uuid::parse_str(s).ok()); - let started_at = parsed.get("started_at").and_then(|v| v.as_i64()); - if let (Some(mid), Some(ts)) = (msg_id, started_at) { - return Some((mid, ts)); - } - } - } - } - None - } - - /// Clear chat stream active state (called when streaming finishes). - pub async fn clear_chat_stream_active(&self, conversation_id: Uuid) { - if let Ok(mut conn) = self.conn().await { - let key = format!("{}{}", CHAT_STREAM_KEY_PREFIX, conversation_id); - let _: Result<(), _> = redis::cmd("DEL").arg(&key).query_async(&mut conn).await; - } - } - - // ─── Chat Stream Cancellation ───────────────────────────────────────── - - /// Mark a chat stream as cancelled by the user. - /// TTL 60 seconds — prevents stale entries. - pub async fn set_chat_stream_cancelled(&self, conversation_id: Uuid) -> bool { - if let Ok(mut conn) = self.conn().await { - let key = format!("{}cancel:{}", CHAT_STREAM_KEY_PREFIX, conversation_id); - let _: Result<(), _> = redis::cmd("SETEX") - .arg(&key) - .arg(60_i64) - .arg("1") - .query_async(&mut conn) - .await; - return true; - } - false - } - - /// Check if a chat stream has been cancelled. - pub async fn is_chat_stream_cancelled(&self, conversation_id: Uuid) -> bool { - if let Ok(mut conn) = self.conn().await { - let key = format!("{}cancel:{}", CHAT_STREAM_KEY_PREFIX, conversation_id); - if let Ok(value) = redis::cmd("GET") - .arg(&key) - .query_async::>(&mut conn) - .await - { - return value.is_some(); - } - } - false - } - - /// Clear the cancel flag for a chat stream. - pub async fn clear_chat_stream_cancelled(&self, conversation_id: Uuid) { - if let Ok(mut conn) = self.conn().await { - let key = format!("{}cancel:{}", CHAT_STREAM_KEY_PREFIX, conversation_id); - let _: Result<(), _> = redis::cmd("DEL").arg(&key).query_async(&mut conn).await; - } - } - - pub async fn set_sub_agent_cancelled(&self, conversation_id: Uuid, children_id: &str) -> bool { - if let Ok(mut conn) = self.conn().await { - let key = format!( - "{}subagent:cancel:{}:{}", - CHAT_STREAM_KEY_PREFIX, conversation_id, children_id - ); - let _: Result<(), _> = redis::cmd("SETEX") - .arg(&key) - .arg(300_i64) - .arg("1") - .query_async(&mut conn) - .await; - return true; - } - false - } - - pub async fn is_sub_agent_cancelled(&self, conversation_id: Uuid, children_id: &str) -> bool { - if let Ok(mut conn) = self.conn().await { - let key = format!( - "{}subagent:cancel:{}:{}", - CHAT_STREAM_KEY_PREFIX, conversation_id, children_id - ); - if let Ok(value) = redis::cmd("GET") - .arg(&key) - .query_async::>(&mut conn) - .await - { - return value.is_some(); - } - } - false - } - - pub async fn clear_sub_agent_cancelled(&self, conversation_id: Uuid, children_id: &str) { - if let Ok(mut conn) = self.conn().await { - let key = format!( - "{}subagent:cancel:{}:{}", - CHAT_STREAM_KEY_PREFIX, conversation_id, children_id - ); - let _: Result<(), _> = redis::cmd("DEL").arg(&key).query_async(&mut conn).await; - } - } -} diff --git a/libs/db/database.rs b/libs/db/database.rs deleted file mode 100644 index 9af27ee..0000000 --- a/libs/db/database.rs +++ /dev/null @@ -1,191 +0,0 @@ -use config::AppConfig; -use sea_orm::prelude::async_trait::async_trait; -use sea_orm::{ - ConnectionTrait, Database, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, - ExecResult, QueryResult, Statement, TransactionTrait, -}; -use std::time::Duration; - -#[derive(Clone)] -pub struct AppDatabase { - db_write: DatabaseConnection, - db_read: Option, -} - -impl AppDatabase { - pub async fn init(cfg: &AppConfig) -> anyhow::Result { - let db_url = cfg.database_url()?; - let max_connections = cfg.database_max_connections()?; - let min_connections = cfg.database_min_connections()?; - let idle_timeout = cfg.database_idle_timeout()?; - let max_lifetime = cfg.database_max_lifetime()?; - let connection_timeout = cfg.database_connection_timeout()?; - let schema_search_path = cfg.database_schema_search_path()?; - let read_replica = cfg.database_read_replicas()?; - println!("[System]: Start Connect Database"); - let conn_cfg = sea_orm::ConnectOptions::new(db_url) - .max_connections(max_connections) - .min_connections(min_connections) - .idle_timeout(Duration::from_secs(idle_timeout)) - .max_lifetime(Duration::from_secs(max_lifetime)) - .connect_timeout(Duration::from_secs(connection_timeout)) - .set_schema_search_path(schema_search_path) - .sqlx_logging(false) - .to_owned(); - - let db_write = Database::connect(conn_cfg).await?; - println!("[System]: Start Read Database"); - let db_read = if let Some(ref replica_url) = read_replica { - let conn_cfg = sea_orm::ConnectOptions::new(replica_url.clone()) - .max_connections(max_connections) - .min_connections(min_connections) - .idle_timeout(Duration::from_secs(idle_timeout)) - .max_lifetime(Duration::from_secs(max_lifetime)) - .connect_timeout(Duration::from_secs(connection_timeout)) - .to_owned(); - Some(Database::connect(conn_cfg).await?) - } else { - None - }; - - Ok(Self { db_write, db_read }) - } - - pub fn writer(&self) -> &DatabaseConnection { - &self.db_write - } - - pub fn reader(&self) -> &DatabaseConnection { - match &self.db_read { - Some(conn) => conn, - None => &self.db_write, - } - } - - pub async fn begin(&self) -> Result { - let txn = self.db_write.begin().await?; - Ok(AppTransaction { inner: txn }) - } -} - -pub struct AppTransaction { - inner: DatabaseTransaction, -} - -impl AppTransaction { - pub async fn commit(self) -> Result<(), DbErr> { - self.inner.commit().await - } - pub async fn rollback(self) -> Result<(), DbErr> { - self.inner.rollback().await - } -} -#[async_trait] -impl ConnectionTrait for AppTransaction { - fn get_database_backend(&self) -> DbBackend { - self.inner.get_database_backend() - } - - async fn execute_raw(&self, stmt: Statement) -> Result { - self.inner.execute_raw(stmt).await - } - - async fn execute_unprepared(&self, sql: &str) -> Result { - self.inner.execute_unprepared(sql).await - } - - async fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { - self.inner.query_one_raw(stmt).await - } - - async fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { - self.inner.query_all_raw(stmt).await - } -} - -#[async_trait] -impl ConnectionTrait for AppDatabase { - fn get_database_backend(&self) -> DbBackend { - self.db_write.get_database_backend() - } - - async fn execute_raw(&self, stmt: Statement) -> Result { - if is_force_write(&stmt.sql) { - return self.db_write.execute_raw(stmt).await; - } - - if is_read_query(&stmt.sql) { - return self.reader().execute_raw(stmt).await; - } - - self.db_write.execute_raw(stmt).await - } - - async fn execute_unprepared(&self, sql: &str) -> Result { - if is_read_query(sql) { - self.reader().execute_unprepared(sql).await - } else { - self.db_write.execute_unprepared(sql).await - } - } - - async fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { - if is_force_write(&stmt.sql) { - return self.db_write.query_one_raw(stmt).await; - } - - if is_read_query(&stmt.sql) { - return self.reader().query_one_raw(stmt).await; - } - - self.db_write.query_one_raw(stmt).await - } - - async fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { - if is_force_write(&stmt.sql) { - return self.db_write.query_all_raw(stmt).await; - } - - if is_read_query(&stmt.sql) { - return self.reader().query_all_raw(stmt).await; - } - - self.db_write.query_all_raw(stmt).await - } -} - -fn is_force_write(sql: &str) -> bool { - sql.contains("/*+ write */") -} - -fn is_force_read(sql: &str) -> bool { - sql.contains("/*+ read */") -} - -fn is_read_query(sql: &str) -> bool { - if is_force_write(sql) { - return false; - } - if is_force_read(sql) { - return true; - } - let sql = strip_comments(sql).to_lowercase(); - if sql.contains("for update") || sql.contains("for share") { - return false; - } - - match sql.split_whitespace().next() { - Some("select") | Some("show") | Some("desc") | Some("describe") | Some("explain") => true, - _ => false, - } -} - -fn strip_comments(sql: &str) -> String { - sql.lines() - .filter(|l| { - let l = l.trim_start(); - !l.starts_with("--") && !l.starts_with("/*") - }) - .collect::>() - .join(" ") -} diff --git a/libs/db/lib.rs b/libs/db/lib.rs deleted file mode 100644 index d9c379b..0000000 --- a/libs/db/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod cache; -pub mod database; diff --git a/libs/email/Cargo.toml b/libs/email/Cargo.toml deleted file mode 100644 index 0e17114..0000000 --- a/libs/email/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "email" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true -[lib] -name = "email" -path = "lib.rs" -[dependencies] -config = { workspace = true } -lettre = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "rt", "sync", "macros"] } -serde = { workspace = true, features = ["derive"] } -anyhow = { workspace = true } -regex = { workspace = true } -tracing = { workspace = true } -metrics = { workspace = true } -[lints] -workspace = true diff --git a/libs/email/lib.rs b/libs/email/lib.rs deleted file mode 100644 index 58fdaf8..0000000 --- a/libs/email/lib.rs +++ /dev/null @@ -1,130 +0,0 @@ -use config::AppConfig; -use lettre::Transport; -use lettre::message::Mailbox; -use lettre::transport::smtp::{PoolConfig, SmtpTransport}; -use metrics::counter; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::sync::LazyLock; -use std::time::Duration; -use tokio::sync::mpsc; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct EmailMessage { - pub to: String, - pub subject: String, - pub body: String, -} - -pub static EMAIL_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()); - -#[derive(Clone)] -pub struct AppEmail { - sender: mpsc::Sender, -} - -impl AppEmail { - pub async fn init(cfg: &AppConfig) -> anyhow::Result { - let smtp_host = cfg.smtp_host()?; - let smtp_port = cfg.smtp_port()?; - let smtp_username = cfg.smtp_username()?; - let smtp_password = cfg.smtp_password()?; - let smtp_from = cfg.smtp_from()?; - let smtp_tls = cfg.smtp_tls()?; - let smtp_timeout = cfg.smtp_timeout()?; - - // Port 465 = SMTPS (implicit TLS via smtps://), others = STARTTLS via smtp:// - let url = if smtp_port == 465 && smtp_tls { - format!( - "smtps://{}:{}@{}:{}", - smtp_username, smtp_password, smtp_host, smtp_port - ) - } else { - let tls_mode = if smtp_tls { - "required" - } else { - "opportunistic" - }; - format!( - "smtp://{}:{}@{}:{}?tls={}", - smtp_username, smtp_password, smtp_host, smtp_port, tls_mode - ) - }; - - let mailer = SmtpTransport::from_url(&url) - .map_err(|e| anyhow::anyhow!("SMTP transport build error: {}", e))? - .timeout(Some(Duration::from_secs(smtp_timeout))) - .pool_config(PoolConfig::new().min_idle(0).max_size(10)) - .build(); - - let from: Mailbox = smtp_from.parse()?; - - let (tx, mut rx) = mpsc::channel::(100); - - tokio::spawn(async move { - while let Some(msg) = rx.recv().await { - let recipient: Mailbox = match msg.to.parse() { - Ok(addr) => addr, - Err(_) => { - counter!("email_validation_skipped_total").increment(1); - tracing::warn!(to = %msg.to, "Invalid recipient address"); - continue; - } - }; - - let email = match lettre::Message::builder() - .from(from.clone()) - .to(recipient) - .subject(msg.subject) - .body(msg.body) - { - Ok(e) => e, - Err(_) => { - counter!("email_build_errors_total").increment(1); - tracing::warn!(to = %msg.to, "Email build error"); - continue; - } - }; - let mut success = false; - for i in 0..3 { - counter!("email_send_attempts_total").increment(1); - let mailer = mailer.clone(); - let email = email.clone(); - let result = tokio::task::spawn_blocking(move || mailer.send(&email)).await; - - match result { - Ok(Ok(_)) => { - success = true; - break; - } - Ok(Err(e)) => { - if i == 2 { - counter!("email_send_failures_total").increment(1); - tracing::error!(to = %msg.to, error = %e, "Email send failed after retries"); - } - tokio::time::sleep(Duration::from_secs(1u64 << i)).await; - } - Err(e) => { - tracing::error!(to = %msg.to, error = %e, "Email spawn error"); - break; - } - } - } - - if success { - counter!("email_sent_total").increment(1); - } - } - }); - - Ok(Self { sender: tx }) - } - - pub async fn send(&self, msg: EmailMessage) -> anyhow::Result<()> { - self.sender - .send(msg) - .await - .map_err(|e| anyhow::anyhow!("queue send error: {}", e)) - } -} diff --git a/libs/fctool/Cargo.toml b/libs/fctool/Cargo.toml deleted file mode 100644 index a88254b..0000000 --- a/libs/fctool/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "fctool" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[lib] -path = "src/lib.rs" -name = "fctool" - -[dependencies] -agent = { workspace = true } -git = { workspace = true } -models = { workspace = true } -db = { workspace = true } -queue = { workspace = true } -sea-orm = { workspace = true, features = [] } -git2 = { workspace = true } -ammonia = "4.0" - -redis = { workspace = true, features = ["tokio-comp"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -base64 = { workspace = true } -chrono = { workspace = true, features = ["serde"] } -uuid = { workspace = true, features = ["serde", "v7"] } -reqwest = { workspace = true, features = ["json", "native-tls"] } -regex = { workspace = true } -csv = { workspace = true } -quick-xml = { workspace = true } -sqlparser = { workspace = true } -pulldown-cmark = { workspace = true } -tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } -tracing = { workspace = true } diff --git a/libs/fctool/src/chat_tools/mod.rs b/libs/fctool/src/chat_tools/mod.rs deleted file mode 100644 index d252d96..0000000 --- a/libs/fctool/src/chat_tools/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Chat tools for AI agent function calling. -//! -//! Tools for managing AI conversations: title generation, -//! sending messages, retracting messages. -//! -//! `register_all` registers globally-available tools (e.g. title generation). -//! `register_room_tools` registers room-only tools (send_message, retract_message) -//! that should only be available when the AI is @mentioned in a room. - -mod retract_message; -mod send_message; -mod title; - -use agent::{ToolHandler, ToolRegistry}; - -pub use retract_message::retract_message_exec; -pub use send_message::send_message_exec; -pub use title::generate_title_exec; - -/// Register globally-available chat tools (title generation only). -/// Room-specific tools (send_message, retract_message) are registered -/// separately via `register_room_tools` when the AI is @mentioned in a room. -pub fn register_all(registry: &mut ToolRegistry) { - registry.register( - title::tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(generate_title_exec(ctx, args))), - ); -} - -/// Register room-only tools. These should only be available when the AI is -/// @mentioned in a room, allowing the AI to send short messages and retract -/// its own messages instead of producing long-form output. -pub fn register_room_tools(registry: &mut ToolRegistry) { - registry.register( - send_message::tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(send_message_exec(ctx, args))), - ); - registry.register( - retract_message::tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(retract_message_exec(ctx, args))), - ); -} diff --git a/libs/fctool/src/chat_tools/retract_message.rs b/libs/fctool/src/chat_tools/retract_message.rs deleted file mode 100644 index 1b9aa81..0000000 --- a/libs/fctool/src/chat_tools/retract_message.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! `retract_message` tool: revokes a message previously sent by the AI agent. -//! -//! Only messages sent in the current turn (matching sender_id) can be retracted. - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use chrono::Utc; -use models::projects::project_members; -use models::rooms::room_message; -use queue::ProjectRoomEvent; -use sea_orm::*; -use std::collections::HashMap; -use uuid::Uuid; - -/// Retract (revoke) a message that was sent by the current agent in the current -/// conversation turn. Cannot retract messages sent by other users or in previous -/// turns. -pub async fn retract_message_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let message_id = args - .get("message_id") - .and_then(|v| v.as_str()) - .and_then(|s| Uuid::parse_str(s).ok()) - .ok_or_else(|| { - ToolError::ExecutionError("message_id is required and must be a valid UUID".into()) - })?; - - let db = ctx.db(); - let sender_id = ctx.sender_id(); - - // In room context, the AI model is the sender — use its ID for authorization. - let effective_sender_id = ctx.ai_model_id().or(sender_id); - - let model = room_message::Entity::find_by_id(message_id) - .one(db.reader()) - .await - .map_err(|e| ToolError::ExecutionError(format!("db error: {}", e)))? - .ok_or_else(|| ToolError::ExecutionError(format!("message not found: {}", message_id)))?; - - let room_id = model.room; - let project_id = ctx.project_id(); - - // Allow retraction if sender is the original author OR a room admin - let is_author = model.sender_id == effective_sender_id; - let is_admin = match effective_sender_id { - Some(uid) => is_room_admin(db, room_id, uid, project_id).await, - None => false, - }; - if !is_author && !is_admin { - return Err(ToolError::ExecutionError( - "can only retract your own messages or be a room admin".into(), - )); - } - - // Must not already be revoked - if model.revoked.is_some() { - return Err(ToolError::ExecutionError( - "message is already revoked".into(), - )); - } - - // Must be a message sent in the current turn — cannot retract across turns - if !ctx.is_sent_in_turn(message_id) { - return Err(ToolError::ExecutionError( - "can only retract messages sent in the current turn — \ - cross-turn retraction is not allowed" - .into(), - )); - } - - let now = Utc::now(); - let mut active: room_message::ActiveModel = model.into(); - active.revoked = Set(Some(now)); - active.revoked_by = Set(effective_sender_id); - active - .update(db.writer()) - .await - .map_err(|e| ToolError::ExecutionError(format!("failed to revoke message: {}", e)))?; - - // Publish retraction event for real-time delivery - if let Some(producer) = ctx.message_producer() { - let event = ProjectRoomEvent { - event_type: "message_revoked".to_string(), - project_id, - room_id: Some(room_id), - category_id: None, - message_id: Some(message_id), - seq: None, - timestamp: now, - }; - producer.publish_project_room_event(project_id, event).await; - } - - Ok(serde_json::json!({ - "message_id": message_id.to_string(), - "revoked": true, - "revoked_at": now.to_rfc3339(), - })) -} - -async fn is_room_admin( - db: &db::database::AppDatabase, - room_id: Uuid, - user_id: Uuid, - project_id: Uuid, -) -> bool { - use models::rooms::room; - let room_model = room::Entity::find_by_id(room_id) - .one(db.reader()) - .await - .ok() - .flatten(); - match room_model { - Some(r) if r.created_by == user_id => true, - Some(_) => { - let member = project_members::Entity::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(user_id)) - .one(db.reader()) - .await - .ok() - .flatten(); - match member { - Some(m) => matches!( - m.scope_role(), - Ok(models::projects::MemberRole::Admin | models::projects::MemberRole::Owner) - ), - None => false, - } - } - None => false, - } -} - -pub fn tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "message_id".into(), - ToolParam { - name: "message_id".into(), - param_type: "string".into(), - description: Some( - "The UUID of the message to retract. Must be a message sent by you \ - in the current conversation turn." - .into(), - ), - required: true, - properties: None, - items: None, - }, - ); - ToolDefinition::new("retract_message") - .description( - "Retract (revoke) a message you previously sent in the current turn. \ - Only works for messages sent by the AI agent — cannot retract user \ - messages or messages from previous turns. Use this to clean up mistakes \ - or remove messages that are no longer relevant.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["message_id".into()]), - }) -} diff --git a/libs/fctool/src/chat_tools/send_message.rs b/libs/fctool/src/chat_tools/send_message.rs deleted file mode 100644 index dc0347b..0000000 --- a/libs/fctool/src/chat_tools/send_message.rs +++ /dev/null @@ -1,229 +0,0 @@ -//! `send_message` tool: sends a brief message to a specified room. -//! -//! Supports mentions in the standard `@[type:id:label]` format: -//! - `@[user:uuid:username]` — mention a user -//! - `@[repo:uuid:name]` — mention a repository -//! - `@[skill:slug]` — mention a skill -//! - `@[ai:uuid:name]` — mention an AI model -//! - `@[issue:uuid:title]` — mention an issue - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use chrono::Utc; -use models::rooms::{MessageContentType, MessageSenderType, room_message}; -use queue::{ProjectRoomEvent, RoomMessageEnvelope}; -use sea_orm::*; -use std::collections::HashMap; -use uuid::Uuid; - -const MAX_CONTENT_LEN: usize = 600; - -/// Send a brief message to the specified room. -/// The message content may include mentions in `@[type:id:label]` format. -pub async fn send_message_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let room_id = args - .get("room_id") - .and_then(|v| v.as_str()) - .and_then(|s| Uuid::parse_str(s).ok()) - .unwrap_or_else(|| ctx.room_id()); - - let content = args - .get("content") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("content is required".into()))?; - - if content.trim().is_empty() { - return Err(ToolError::ExecutionError("content cannot be empty".into())); - } - if content.len() > MAX_CONTENT_LEN { - return Err(ToolError::ExecutionError(format!( - "content too long ({} chars, max {})", - content.len(), - MAX_CONTENT_LEN - ))); - } - - let content = ammonia::clean(content); - - let db = ctx.db(); - let cache = ctx.cache(); - let sender_id = ctx.sender_id(); - - // In room context, the AI model is the sender — not the user who @mentioned it. - let (effective_sender_id, display_name) = match ctx.ai_model_id() { - Some(model_id) => (Some(model_id), ctx.ai_model_name()), - None => (sender_id, None), - }; - - // Verify room exists - let room_model = models::rooms::room::Entity::find_by_id(room_id) - .one(db.reader()) - .await - .map_err(|e| ToolError::ExecutionError(format!("db error: {}", e)))? - .ok_or_else(|| ToolError::ExecutionError(format!("room not found: {}", room_id)))?; - - // Sequence: use Redis atomic INCR if available, fall back to DB MAX+1 - let seq = get_next_seq(room_id, db, cache) - .await - .map_err(|e| ToolError::ExecutionError(format!("seq error: {}", e)))?; - - let now = Utc::now(); - let message_id = Uuid::now_v7(); - - room_message::Entity::insert(room_message::ActiveModel { - id: Set(message_id), - seq: Set(seq), - room: Set(room_id), - sender_type: Set(MessageSenderType::Ai), - sender_id: Set(effective_sender_id), - model_id: Set(ctx.ai_model_id()), - thread: Set(None), - content: Set(content.clone()), - content_type: Set(MessageContentType::Text), - thinking_content: Set(None), - edited_at: Set(None), - send_at: Set(now), - revoked: Set(None), - revoked_by: Set(None), - in_reply_to: Set(None), - }) - .exec(db.writer()) - .await - .map_err(|e| ToolError::ExecutionError(format!("failed to insert message: {}", e)))?; - - // Register this message in the current turn so retract_message can validate it - ctx.register_sent_message(message_id); - - // Update room last_msg_at - let mut room_active: models::rooms::room::ActiveModel = room_model.into(); - room_active.last_msg_at = Set(now); - room_active - .update(db.writer()) - .await - .map_err(|e| ToolError::ExecutionError(format!("failed to update room: {}", e)))?; - - // Publish via message queue for real-time delivery to other clients - if let Some(producer) = ctx.message_producer() { - let envelope = RoomMessageEnvelope { - id: message_id, - dedup_key: Some(format!("{}:{}", room_id, message_id)), - room_id, - sender_type: "ai".to_string(), - sender_id: effective_sender_id, - model_id: ctx.ai_model_id(), - thread_id: None, - in_reply_to: None, - content: content.clone(), - content_type: "text".to_string(), - thinking_content: None, - send_at: now, - seq, - display_name, - }; - let _ = producer.publish(room_id, envelope).await; - - let project_event = ProjectRoomEvent { - event_type: "new_message".to_string(), - project_id: ctx.project_id(), - room_id: Some(room_id), - category_id: None, - message_id: Some(message_id), - seq: Some(seq), - timestamp: now, - }; - producer - .publish_project_room_event(ctx.project_id(), project_event) - .await; - } - - Ok(serde_json::json!({ - "message_id": message_id.to_string(), - "room_id": room_id.to_string(), - "seq": seq, - "content": content, - "sent_at": now.to_rfc3339(), - })) -} - -async fn get_next_seq( - room_id: Uuid, - db: &db::database::AppDatabase, - cache: &db::cache::AppCache, -) -> Result { - // Try Redis atomic INCR first - let seq_key = format!("room:seq:{}", room_id); - if let Ok(mut conn) = cache.conn().await { - let result: Result, _> = redis::cmd("INCR") - .arg(&seq_key) - .query_async(&mut conn) - .await; - if let Ok(Some(seq)) = result { - return Ok(seq); - } - } - - // Fallback: DB MAX+1 - let max_seq = room_message::Entity::find() - .filter(room_message::Column::Room.eq(room_id)) - .select_only() - .column_as(room_message::Column::Seq.max(), "max_seq") - .into_tuple::>>() - .one(db.reader()) - .await - .map_err(|e| ToolError::ExecutionError(format!("db max seq: {}", e)))? - .flatten() - .flatten() - .unwrap_or(0); - Ok(max_seq + 1) -} - -pub fn tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "content".into(), - ToolParam { - name: "content".into(), - param_type: "string".into(), - description: Some( - "Brief message content. Keep it concise — no long reports. \ - Use @[type:id:label] syntax to mention users, repos, skills, issues, etc. \ - Example: '@[user:550e8400-e29b-41d4-a716-446655440000:alice] see the repo @[repo:660e8400-e29b-41d4-a716-446655440001:backend] for details.'" - .into(), - ), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "room_id".into(), - ToolParam { - name: "room_id".into(), - param_type: "string".into(), - description: Some( - "The UUID of the room to send the message to. \ - If omitted, defaults to the current room context." - .into(), - ), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("send_message") - .description( - "Send a brief message to a room. Keep content concise and to the point. \ - Use @[type:id:label] syntax to mention project resources: \ - @[user:uuid:name] for users, @[repo:uuid:name] for repos, \ - @[skill:slug] for skills, @[issue:uuid:title] for issues, etc. \ - Do NOT use this for long reports or multi-paragraph explanations — \ - use it only for short notifications, status updates, or asking follow-up questions.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["content".into()]), - }) -} diff --git a/libs/fctool/src/chat_tools/title.rs b/libs/fctool/src/chat_tools/title.rs deleted file mode 100644 index d379d71..0000000 --- a/libs/fctool/src/chat_tools/title.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! `generate_title` tool: reads conversation history, generates a short title, saves it to the conversation. - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use chrono::Utc; -use models::ai::{AiMessage, ai_conversation, ai_message}; -use sea_orm::*; -use std::collections::HashMap; - -/// Generate a concise title for the current conversation based on its message history. -/// The title must be 5 words or fewer. Updates the conversation record. -pub async fn generate_title_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let conversation_id = args - .get("conversation_id") - .and_then(|v| v.as_str()) - .and_then(|s| uuid::Uuid::parse_str(s).ok()) - .ok_or_else(|| { - ToolError::ExecutionError("conversation_id is required and must be a valid UUID".into()) - })?; - - let db = ctx.db(); - - // Load conversation - let conv = ai_conversation::Entity::find_by_id(conversation_id) - .one(db.reader()) - .await - .map_err(|e| ToolError::ExecutionError(format!("db error: {}", e)))? - .ok_or_else(|| ToolError::ExecutionError("Conversation not found".into()))?; - - // Load recent user messages (last 3) as context - let recent_messages = AiMessage::find() - .filter(ai_message::Column::ConversationId.eq(conversation_id)) - .filter(ai_message::Column::Role.eq("user")) - .order_by_desc(ai_message::Column::CreatedAt) - .limit(3) - .all(db.reader()) - .await - .map_err(|e| ToolError::ExecutionError(format!("db error: {}", e)))?; - - if recent_messages.is_empty() { - return Err(ToolError::ExecutionError( - "No user messages found in conversation".into(), - )); - } - - // Build content summary from the most recent message - let content = recent_messages - .first() - .and_then(|m| m.content.as_array()) - .and_then(|arr| arr.first()) - .and_then(|v| v.get("content")) - .and_then(|c| c.as_str()) - .unwrap_or("") - .to_string(); - - // Generate a title using a simple keyword-extraction heuristic: - // Take first meaningful words from the content, up to 5 words. - let words: Vec<&str> = content - .split_whitespace() - .filter(|w| w.len() > 2 && !is_stop_word(w)) - .take(5) - .collect(); - - let title = if words.is_empty() { - "New Chat".to_string() - } else { - words.join(" ") - }; - - // Update conversation title - let mut active: ai_conversation::ActiveModel = conv.into(); - active.title = Set(Some(title.clone())); - active.updated_at = Set(Utc::now()); - active - .update(db.writer()) - .await - .map_err(|e| ToolError::ExecutionError(format!("failed to update title: {}", e)))?; - - Ok(serde_json::json!({ - "conversation_id": conversation_id.to_string(), - "title": title, - })) -} - -fn is_stop_word(w: &str) -> bool { - matches!( - w.to_lowercase().as_str(), - "the" - | "this" - | "that" - | "what" - | "which" - | "when" - | "where" - | "why" - | "how" - | "can" - | "could" - | "would" - | "should" - | "please" - | "help" - | "thanks" - | "thank" - | "you" - | "your" - | "have" - | "has" - | "had" - | "with" - | "for" - | "from" - | "into" - | "about" - | "also" - | "just" - | "now" - | "very" - | "really" - ) -} - -pub fn tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "conversation_id".into(), - ToolParam { - name: "conversation_id".into(), - param_type: "string".into(), - description: Some( - "The UUID of the conversation to generate a title for (required).".into(), - ), - required: true, - properties: None, - items: None, - }, - ); - ToolDefinition::new("chat_generate_title") - .description( - "Generate a concise title (5 words or fewer) for the current conversation \ - based on its message history, and save it to the conversation record. \ - Call this tool at the start of a new conversation if it has no title.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["conversation_id".into()]), - }) -} diff --git a/libs/fctool/src/file_tools/csv.rs b/libs/fctool/src/file_tools/csv.rs deleted file mode 100644 index 520aa0e..0000000 --- a/libs/fctool/src/file_tools/csv.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! read_csv — parse and query CSV files. - -use crate::file_tools::MAX_FILE_SIZE; -use crate::git_tools::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use csv::ReaderBuilder; -use std::collections::HashMap; - -async fn read_csv_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| "HEAD".to_string()); - let delimiter = p - .get("delimiter") - .and_then(|v| v.as_str()) - .and_then(|s| s.chars().next()) - .unwrap_or(','); - let has_header = p - .get("has_header") - .and_then(|v| v.as_bool()) - .unwrap_or(true); - let offset = p.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize; - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize; - let filter_col = p.get("filter_column").and_then(|v| v.as_str()); - let filter_val = p.get("filter_value").and_then(|v| v.as_str()); - let select_cols = p.get("columns").and_then(|v| v.as_array()).map(|a| { - a.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect::>() - }); - - let domain = ctx.open_repo(project_name, repo_name).await?; - - let commit_oid = if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - git::commit::types::CommitOid::new(&rev) - } else if let Ok(Some(oid)) = domain.ref_target(&rev) { - oid - } else { - domain - .commit_get_prefix(&rev) - .map_err(|e| e.to_string())? - .oid - }; - - let entry = domain - .tree_entry_by_path_from_commit(&commit_oid, path) - .map_err(|e| e.to_string())?; - let blob = domain.blob_get(&entry.oid).map_err(|e| e.to_string())?; - - if blob.is_binary { - return Err("file is binary, not a CSV".to_string()); - } - - let content = domain.blob_content(&entry.oid).map_err(|e| e.to_string())?; - let data = &content.content; - if data.len() > MAX_FILE_SIZE { - return Err(format!( - "file too large ({} bytes), max {} bytes", - data.len(), - MAX_FILE_SIZE - )); - } - - let text = String::from_utf8_lossy(data); - let mut reader = ReaderBuilder::new() - .delimiter(delimiter as u8) - .has_headers(has_header) - .from_reader(text.as_bytes()); - - let headers: Vec = if has_header { - reader - .headers() - .map_err(|e| e.to_string())? - .clone() - .into_iter() - .map(String::from) - .collect() - } else { - vec![] - }; - - let col_indices: Vec = if let Some(ref sel) = select_cols { - sel.iter() - .filter_map(|col| headers.iter().position(|h| h == col)) - .collect() - } else { - (0..headers.len()).collect() - }; - - let _col_set: std::collections::HashSet = col_indices.iter().cloned().collect(); - let filter_col_idx = filter_col.and_then(|c| headers.iter().position(|h| h == c)); - - let mut rows: Vec = Vec::new(); - let mut skipped = 0; - let mut total = 0; - - for result in reader.records() { - let record = result.map_err(|e| e.to_string())?; - - // Skip offset - if skipped < offset { - skipped += 1; - continue; - } - - total += 1; - - // Filter - if let (Some(fci), Some(fv)) = (filter_col_idx, filter_val) { - if record.get(fci) != Some(fv) { - continue; - } - } - - // Select columns - let obj = if has_header { - let mut map = serde_json::Map::new(); - for &idx in &col_indices { - let key = headers - .get(idx) - .cloned() - .unwrap_or_else(|| format!("col_{}", idx)); - let val = record.get(idx).unwrap_or("").to_string(); - map.insert(key, serde_json::Value::String(val)); - } - serde_json::Value::Object(map) - } else { - let arr: Vec = col_indices - .iter() - .map(|&idx| record.get(idx).unwrap_or("").to_string()) - .collect(); - serde_json::Value::Array(arr.into_iter().map(serde_json::Value::String).collect()) - }; - - rows.push(obj); - - if rows.len() >= limit { - break; - } - } - - Ok(serde_json::json!({ - "path": path, - "rev": rev, - "headers": if has_header { headers } else { vec![] }, - "selected_columns": select_cols, - "rows": rows, - "row_count": rows.len(), - "total_available": total + offset, - "filter": if let (Some(c), Some(v)) = (filter_col, filter_val) { - serde_json::json!({ "column": c, "value": v }) - } else { serde_json::Value::Null }, - })) -} - -pub fn register_csv_tools(registry: &mut ToolRegistry) { - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("File path within the repository".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Git revision (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "delimiter".into(), - ToolParam { - name: "delimiter".into(), - param_type: "string".into(), - description: Some("Field delimiter character (default: comma \",\")".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "has_header".into(), - ToolParam { - name: "has_header".into(), - param_type: "boolean".into(), - description: Some("If true, first row is column headers (default: true)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "columns".into(), - ToolParam { - name: "columns".into(), - param_type: "array".into(), - description: Some("List of column names to select".into()), - required: false, - properties: None, - items: Some(Box::new(ToolParam { - name: "".into(), - param_type: "string".into(), - description: None, - required: false, - properties: None, - items: None, - })), - }, - ), - ( - "filter_column".into(), - ToolParam { - name: "filter_column".into(), - param_type: "string".into(), - description: Some("Column name to filter by".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "filter_value".into(), - ToolParam { - name: "filter_value".into(), - param_type: "string".into(), - description: Some("Value to match in filter_column".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "offset".into(), - ToolParam { - name: "offset".into(), - param_type: "integer".into(), - description: Some("Number of rows to skip (default: 0)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "limit".into(), - ToolParam { - name: "limit".into(), - param_type: "integer".into(), - description: Some("Maximum rows to return (default: 100)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "path".into(), - ]), - }; - registry.register( - ToolDefinition::new("read_csv") - .description("Parse and query a CSV file. Supports header detection, column selection, filtering, pagination (offset/limit), and custom delimiters.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - read_csv_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/file_tools/grep.rs b/libs/fctool/src/file_tools/grep.rs deleted file mode 100644 index d0adc87..0000000 --- a/libs/fctool/src/file_tools/grep.rs +++ /dev/null @@ -1,438 +0,0 @@ -//! git_grep — search repository files for patterns. - -use crate::file_tools::MAX_FILE_SIZE; -use crate::git_tools::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use regex::RegexBuilder; -use std::collections::HashMap; - -/// Text file extensions to search (skip binary files). -const TEXT_EXTS: &[&str] = &[ - "rs", - "toml", - "yaml", - "yml", - "json", - "jsonc", - "js", - "jsx", - "ts", - "tsx", - "css", - "scss", - "less", - "html", - "htm", - "xml", - "svg", - "vue", - "svelte", - "py", - "rb", - "go", - "java", - "kt", - "swift", - "c", - "cpp", - "h", - "hpp", - "cs", - "php", - "pl", - "sh", - "bash", - "zsh", - "fish", - "ps1", - "bat", - "cmd", - "sql", - "md", - "markdown", - "rst", - "txt", - "log", - "ini", - "cfg", - "conf", - "dockerfile", - "makefile", - "cmake", - "gradle", - "properties", - "env", - "proto", - "graphql", - "vue", - "lock", -]; - -fn is_text_ext(path: &str) -> bool { - let lower = path.to_lowercase(); - TEXT_EXTS - .iter() - .any(|&e| lower.ends_with(&format!(".{}", e))) -} - -fn is_binary_content(data: &[u8]) -> bool { - data.iter().take(8192).any(|&b| b == 0) -} - -async fn git_grep_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| "HEAD".to_string()); - let pattern = p - .get("pattern") - .and_then(|v| v.as_str()) - .ok_or("missing pattern")?; - let glob = p.get("glob").and_then(|v| v.as_str()).map(String::from); - let is_regex = p.get("is_regex").and_then(|v| v.as_bool()).unwrap_or(true); - let context_lines = p.get("context_lines").and_then(|v| v.as_u64()).unwrap_or(0) as usize; - let max_results = p.get("max_results").and_then(|v| v.as_u64()).unwrap_or(100) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - - // Resolve revision to commit oid - let commit_oid = if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - git::commit::types::CommitOid::new(&rev) - } else if let Ok(Some(oid)) = domain.ref_target(&rev) { - oid - } else { - domain - .commit_get_prefix(&rev) - .map_err(|e| e.to_string())? - .oid - }; - - let regex = if is_regex { - RegexBuilder::new(pattern) - .case_insensitive(true) - .build() - .map_err(|e| format!("invalid regex '{}': {}", pattern, e))? - } else { - // Escape for literal search - RegexBuilder::new(®ex::escape(pattern)) - .case_insensitive(true) - .build() - .map_err(|e| e.to_string())? - }; - - // Recursive tree walk using git2 - let repo = domain.repo(); - let commit = repo - .find_commit(commit_oid.to_oid().map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - let tree = commit.tree().map_err(|e| e.to_string())?; - - let mut results: Vec = Vec::new(); - // Stack: (tree, current_path_prefix) - let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree, String::new())]; - - while let Some((current_tree, current_prefix)) = stack.pop() { - for entry in current_tree.iter() { - let name = entry.name().unwrap_or_default(); - if name.is_empty() { - continue; - } - let path: String = if current_prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", current_prefix, name) - }; - - if entry.kind() == Some(git2::ObjectType::Tree) { - if let Some(subtree) = entry.to_object(&repo).ok().and_then(|o| o.into_tree().ok()) - { - stack.push((subtree, path)); - } - continue; - } - - if entry.kind() != Some(git2::ObjectType::Blob) { - continue; - } - - // Glob filter - if let Some(ref g) = glob { - if !glob_match(&path, g) { - continue; - } - } else if !is_text_ext(&path) { - continue; - } - - // Read blob content - let blob = match entry.to_object(&repo).ok().and_then(|o| o.into_blob().ok()) { - Some(b) => b, - None => continue, - }; - - let size = blob.size(); - if size == 0 || size > MAX_FILE_SIZE { - continue; - } - - let data = blob.content(); - if is_binary_content(data) { - continue; - } - - let content = match String::from_utf8(data.to_vec()) { - Ok(s) => s, - Err(_) => continue, - }; - - // Search line by line - let lines: Vec<&str> = content.lines().collect(); - for (line_idx, line) in lines.iter().enumerate() { - if regex.is_match(line) { - let start = line_idx.saturating_sub(context_lines); - let end = (line_idx + context_lines + 1).min(lines.len()); - - let context: Vec = lines[start..end] - .iter() - .enumerate() - .map(|(i, l)| { - let line_num = start + i + 1; - let prefix = if start + i == line_idx { ">" } else { " " }; - format!("{}{}: {}", prefix, line_num, l) - }) - .collect(); - - results.push(serde_json::json!({ - "file": path, - "line_number": line_idx + 1, - "match": line, - "context": context.join("\n"), - })); - - if results.len() >= max_results { - return Ok(serde_json::json!({ - "query": pattern, - "rev": rev, - "total_matches": results.len(), - "truncated": true, - "results": results - })); - } - } - } - } - } - - Ok(serde_json::json!({ - "query": pattern, - "rev": rev, - "total_matches": results.len(), - "truncated": false, - "results": results - })) -} - -fn glob_match(path: &str, pattern: &str) -> bool { - let path_lower = path.to_lowercase(); - let pattern_lower = pattern.to_lowercase(); - let parts: Vec<&str> = pattern_lower.split('/').collect(); - let path_parts: Vec<&str> = path_lower.split('/').collect(); - - fn matches_part(path_part: &str, pattern_part: &str) -> bool { - if pattern_part.is_empty() || pattern_part == "*" { - return true; - } - if pattern_part == "**" { - return true; - } - if let Some(star) = pattern_part.find('*') { - let (prefix, suffix) = pattern_part.split_at(star); - let suffix = if suffix.starts_with('*') { - &suffix[1..] - } else { - suffix - }; - if !prefix.is_empty() && !path_part.starts_with(prefix) { - return false; - } - if !suffix.is_empty() && !path_part.ends_with(suffix) { - return false; - } - return true; - } - path_part == pattern_part - } - - if parts.len() == 1 { - // Simple glob pattern on filename only - let file_name = path_parts.last().unwrap_or(&""); - return matches_part(file_name, &parts[0]); - } - - // Multi-part glob - let mut pi = 0; - for part in &parts { - if *part == "**" { - // ** matches zero or more path segments - // If this is the last pattern part, consume all remaining path segments - if part == parts.last().unwrap() { - pi = path_parts.len(); - break; - } - // Try skipping segments until the next pattern part matches - let next_part = parts - .iter() - .skip_while(|p| **p == "**") - .next() - .unwrap_or(&"*"); - while pi < path_parts.len() && !matches_part(path_parts[pi], next_part) { - pi += 1; - } - continue; - } - if pi >= path_parts.len() || !matches_part(path_parts[pi], part) { - return false; - } - pi += 1; - } - // All pattern parts consumed — check that all path segments were matched too - pi == path_parts.len() -} - -pub fn register_grep_tools(registry: &mut ToolRegistry) { - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "pattern".into(), - ToolParam { - name: "pattern".into(), - param_type: "string".into(), - description: Some("Search pattern (regex or literal string)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some( - "Git revision to search in (branch, tag, commit). Default: HEAD".into(), - ), - required: false, - properties: None, - items: None, - }, - ), - ( - "glob".into(), - ToolParam { - name: "glob".into(), - param_type: "string".into(), - description: Some("File glob pattern to filter (e.g. *.rs, src/**/*.ts)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "is_regex".into(), - ToolParam { - name: "is_regex".into(), - param_type: "boolean".into(), - description: Some( - "If true, pattern is a regex. If false, literal string. Default: true".into(), - ), - required: false, - properties: None, - items: None, - }, - ), - ( - "context_lines".into(), - ToolParam { - name: "context_lines".into(), - param_type: "integer".into(), - description: Some( - "Number of surrounding lines to include for each match. Default: 0".into(), - ), - required: false, - properties: None, - items: None, - }, - ), - ( - "max_results".into(), - ToolParam { - name: "max_results".into(), - param_type: "integer".into(), - description: Some("Maximum number of matches to return. Default: 100".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "pattern".into(), - ]), - }; - - registry.register( - ToolDefinition::new("git_grep") - .description("Search for a text pattern across all files in a repository at a given revision. Supports regex, glob filtering, and line-level context. Skips binary files automatically.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - git_grep_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/file_tools/json.rs b/libs/fctool/src/file_tools/json.rs deleted file mode 100644 index c16c9e0..0000000 --- a/libs/fctool/src/file_tools/json.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! read_json — parse, validate, and query JSON / JSONC files. - -use crate::file_tools::MAX_FILE_SIZE; -use crate::git_tools::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use serde_json::Value as JsonValue; -use std::collections::HashMap; - -/// Remove comments from JSONC (lines starting with // or /* */) for parsing. -fn strip_jsonc_comments(input: &str) -> String { - let mut result = String::with_capacity(input.len()); - let mut chars = input.chars().peekable(); - let mut in_string = false; - let mut escaped = false; - - while let Some(c) = chars.next() { - if escaped { - result.push(c); - escaped = false; - continue; - } - - if c == '\\' && in_string { - result.push(c); - escaped = true; - continue; - } - - if c == '"' { - result.push(c); - in_string = !in_string; - continue; - } - - if !in_string { - if c == '/' { - if let Some(&next) = chars.peek() { - if next == '/' { - // Line comment — skip to end of line - chars.next(); - while let Some(nc) = chars.next() { - if nc == '\n' { - result.push(nc); - break; - } - } - continue; - } else if next == '*' { - // Block comment — skip until */ - chars.next(); - while let Some(nc) = chars.next() { - if nc == '*' { - if let Some(&'/') = chars.peek() { - chars.next(); - break; - } - } - } - continue; - } - } - } - } - - result.push(c); - } - - result -} - -fn infer_schema(value: &JsonValue, max_depth: usize) -> JsonValue { - if max_depth == 0 { - return serde_json::json!({ "type": "MAX_DEPTH" }); - } - - match value { - JsonValue::Null => serde_json::json!({ "type": "null" }), - JsonValue::Bool(_) => serde_json::json!({ "type": "boolean" }), - JsonValue::Number(_) => serde_json::json!({ "type": "number" }), - JsonValue::String(_) => serde_json::json!({ "type": "string" }), - JsonValue::Array(arr) => { - if arr.is_empty() { - serde_json::json!({ "type": "array", "items": null }) - } else { - serde_json::json!({ - "type": "array", - "length": arr.len(), - "items": infer_schema(&arr[0], max_depth - 1) - }) - } - } - JsonValue::Object(obj) => { - let mut schema = serde_json::Map::new(); - schema.insert("type".into(), serde_json::Value::String("object".into())); - let mut properties = serde_json::Map::new(); - for (k, v) in obj { - properties.insert(k.clone(), infer_schema(v, max_depth - 1)); - } - schema.insert("properties".into(), serde_json::Value::Object(properties)); - schema.insert("keyCount".into(), serde_json::json!(obj.len())); - serde_json::Value::Object(schema) - } - } -} - -async fn read_json_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| "HEAD".to_string()); - let query = p.get("query").and_then(|v| v.as_str()).map(String::from); - let max_depth = p.get("schema_depth").and_then(|v| v.as_u64()).unwrap_or(4) as usize; - let pretty = p.get("pretty").and_then(|v| v.as_bool()).unwrap_or(false); - - let domain = ctx.open_repo(project_name, repo_name).await?; - - let commit_oid = if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - git::commit::types::CommitOid::new(&rev) - } else if let Ok(Some(oid)) = domain.ref_target(&rev) { - oid - } else { - domain - .commit_get_prefix(&rev) - .map_err(|e| e.to_string())? - .oid - }; - - let entry = domain - .tree_entry_by_path_from_commit(&commit_oid, path) - .map_err(|e| e.to_string())?; - let content = domain.blob_content(&entry.oid).map_err(|e| e.to_string())?; - - let data = &content.content; - if data.len() > MAX_FILE_SIZE { - return Err(format!( - "file too large ({} bytes), max {} bytes", - data.len(), - MAX_FILE_SIZE - )); - } - - let text = String::from_utf8_lossy(data); - // Only treat as JSONC if the extension indicates it, or if we can - // confirm a comment-like pattern outside of a string context. - let is_jsonc = path.ends_with(".jsonc"); - - let json_text = if is_jsonc { - strip_jsonc_comments(&text) - } else { - text.to_string() - }; - - let parsed: JsonValue = serde_json::from_str(&json_text) - .map_err(|e| format!("JSON parse error at {}: {}", e.line(), e))?; - - // Apply JSONPath-like query - let result = if let Some(ref q) = query { - query_json(&parsed, q)? - } else { - parsed - }; - - let schema = infer_schema(&result, max_depth); - - let display = if pretty { - serde_json::to_string_pretty(&result).unwrap_or_default() - } else { - serde_json::to_string(&result).unwrap_or_default() - }; - - Ok(serde_json::json!({ - "path": path, - "rev": rev, - "format": if is_jsonc { "jsonc" } else { "json" }, - "size_bytes": data.len(), - "schema": schema, - "data": if display.chars().count() > 5000 { - let truncated: String = display.chars().take(5000).collect(); - format!("{}... (truncated, {} chars total)", truncated, display.chars().count()) - } else { display }, - })) -} - -/// Simple JSONPath-like query support. -/// Supports: $.key, $[0], $.key.nested, $.arr[0].field -/// Bracket notation ["key.with.dots"] allows accessing keys containing dots. -fn query_json(value: &JsonValue, query: &str) -> Result { - let query = query.trim(); - let query = if query.starts_with("$.") { - &query[2..] - } else if query.starts_with('$') && query.len() > 1 { - &query[1..] - } else { - query - }; - - let mut current = value.clone(); - - // Parse into access segments: Key("name"), Index(0), BracketKey("key.with.dots") - enum Segment { - Key(String), - Index(usize), - BracketKey(String), - } - let mut segments: Vec = Vec::new(); - let mut i = 0; - let q_chars: Vec = query.chars().collect(); - while i < q_chars.len() { - if q_chars[i] == '[' { - // Find matching ] - let mut j = i + 1; - let mut bracket_content = String::new(); - while j < q_chars.len() && q_chars[j] != ']' { - bracket_content.push(q_chars[j]); - j += 1; - } - if j < q_chars.len() && q_chars[j] == ']' { - let content = bracket_content.trim(); - // Check if it's a quoted string key or a numeric index - if content.starts_with('"') && content.ends_with('"') { - let key = content[1..content.len() - 1].to_string(); - segments.push(Segment::BracketKey(key)); - } else if content.starts_with("'") && content.ends_with("'") { - let key = content[1..content.len() - 1].to_string(); - segments.push(Segment::BracketKey(key)); - } else if let Ok(idx) = content.parse::() { - segments.push(Segment::Index(idx)); - } else { - return Err(format!("Invalid bracket notation: [{}]", content)); - } - i = j + 1; - // Skip dot after bracket if present - if i < q_chars.len() && q_chars[i] == '.' { - i += 1; - } - } else { - return Err("Unmatched [ in query".into()); - } - } else { - // Read key until . or [ - let mut key = String::new(); - while i < q_chars.len() && q_chars[i] != '.' && q_chars[i] != '[' { - key.push(q_chars[i]); - i += 1; - } - if !key.is_empty() { - // Check if key contains a numeric-only segment (array index shorthand) - segments.push(Segment::Key(key)); - } - if i < q_chars.len() && q_chars[i] == '.' { - i += 1; - } - } - } - - for seg in &segments { - match seg { - Segment::Key(key) | Segment::BracketKey(key) => { - if let JsonValue::Object(obj) = ¤t { - current = obj.get(key).cloned().unwrap_or(JsonValue::Null); - } else { - return Err(format!("cannot access property '{}' on non-object", key)); - } - } - Segment::Index(idx) => { - if let JsonValue::Array(arr) = ¤t { - current = arr.get(*idx).cloned().unwrap_or(JsonValue::Null); - } else { - return Err(format!("index {} on non-array", idx)); - } - } - } - } - - Ok(current) -} - -pub fn register_json_tools(registry: &mut ToolRegistry) { - let p = HashMap::from([ - ("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }), - ("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }), - ("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path to the JSON or JSONC file".into()), required: true, properties: None, items: None }), - ("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Git revision (default: HEAD)".into()), required: false, properties: None, items: None }), - ("query".into(), ToolParam { name: "query".into(), param_type: "string".into(), description: Some("JSONPath-like query (e.g. $.config.items[0].name) to extract a subset of the document".into()), required: false, properties: None, items: None }), - ("schema_depth".into(), ToolParam { name: "schema_depth".into(), param_type: "integer".into(), description: Some("How deep to infer the JSON schema (default: 4)".into()), required: false, properties: None, items: None }), - ("pretty".into(), ToolParam { name: "pretty".into(), param_type: "boolean".into(), description: Some("Pretty-print the output (default: false)".into()), required: false, properties: None, items: None }), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "path".into(), - ]), - }; - registry.register( - ToolDefinition::new("read_json") - .description("Parse, validate, and query JSON and JSONC files. Supports JSONPath-like queries ($.key, $.arr[0]), schema inference, and pretty-printing. Automatically detects JSONC (with // comments).") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - read_json_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/file_tools/markdown.rs b/libs/fctool/src/file_tools/markdown.rs deleted file mode 100644 index 7f908de..0000000 --- a/libs/fctool/src/file_tools/markdown.rs +++ /dev/null @@ -1,288 +0,0 @@ -//! read_markdown — parse and analyze Markdown files. - -use crate::file_tools::MAX_FILE_SIZE; -use crate::git_tools::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Parser, Tag, TagEnd}; -use std::collections::HashMap; - -async fn read_markdown_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| "HEAD".to_string()); - let include_code = p - .get("include_code") - .and_then(|v| v.as_bool()) - .unwrap_or(true); - let sections_only = p - .get("sections_only") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let domain = ctx.open_repo(project_name, repo_name).await?; - - let commit_oid = if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - git::commit::types::CommitOid::new(&rev) - } else if let Ok(Some(oid)) = domain.ref_target(&rev) { - oid - } else { - domain - .commit_get_prefix(&rev) - .map_err(|e| e.to_string())? - .oid - }; - - let entry = domain - .tree_entry_by_path_from_commit(&commit_oid, path) - .map_err(|e| e.to_string())?; - let content = domain.blob_content(&entry.oid).map_err(|e| e.to_string())?; - - let data = &content.content; - if data.len() > MAX_FILE_SIZE { - return Err(format!( - "file too large ({} bytes), max {} bytes", - data.len(), - MAX_FILE_SIZE - )); - } - - let text = String::from_utf8_lossy(data); - let parser = Parser::new(&text); - - let mut sections: Vec = Vec::new(); - let mut code_blocks: Vec = Vec::new(); - let mut links: Vec = Vec::new(); - let mut images: Vec = Vec::new(); - - let mut current_heading_level: Option = None; - let mut current_heading_text = String::new(); - let mut in_code_block = false; - let mut code_block_lang = String::new(); - let mut code_block_content = String::new(); - - let mut toc: Vec = Vec::new(); - - for event in parser { - match event { - Event::Start(Tag::Heading { level, .. }) => { - current_heading_level = Some(match level { - HeadingLevel::H1 => 1, - HeadingLevel::H2 => 2, - HeadingLevel::H3 => 3, - HeadingLevel::H4 => 4, - HeadingLevel::H5 => 5, - HeadingLevel::H6 => 6, - }); - current_heading_text.clear(); - } - Event::End(TagEnd::Heading(level)) => { - let lvl = match level { - HeadingLevel::H1 => 1, - HeadingLevel::H2 => 2, - HeadingLevel::H3 => 3, - HeadingLevel::H4 => 4, - HeadingLevel::H5 => 5, - HeadingLevel::H6 => 6, - }; - let heading = current_heading_text.trim().to_string(); - if !heading.is_empty() { - let section = serde_json::json!({ - "level": lvl, - "title": heading, - }); - toc.push(section.clone()); - if !sections_only { - sections.push(serde_json::json!({ - "level": lvl, - "title": heading, - "content": "", - })); - } - } - current_heading_level = None; - } - Event::Text(text) => { - if in_code_block { - code_block_content.push_str(&text); - code_block_content.push('\n'); - } else if let Some(_) = current_heading_level { - current_heading_text.push_str(&text); - current_heading_text.push(' '); - } - } - Event::Code(code) => { - code_blocks.push(serde_json::json!({ - "language": "", - "code": code.as_ref(), - })); - } - Event::Start(Tag::CodeBlock(kind)) => { - in_code_block = true; - code_block_content.clear(); - code_block_lang = match kind { - CodeBlockKind::Fenced(info) => info.as_ref().to_string(), - CodeBlockKind::Indented => String::new(), - }; - } - Event::End(TagEnd::CodeBlock) => { - in_code_block = false; - if include_code { - code_blocks.push(serde_json::json!({ - "language": code_block_lang, - "code": code_block_content.trim().to_string(), - })); - } - code_block_lang.clear(); - } - Event::Start(Tag::Link { dest_url, .. }) => { - links.push(serde_json::json!({ "url": dest_url.to_string() })); - } - Event::Start(Tag::Image { dest_url, .. }) => { - images.push(serde_json::json!({ "url": dest_url.to_string() })); - } - _ => {} - } - } - - // Build outline (h1/h2/h3 only) - let outline: Vec = toc - .iter() - .filter(|s| { - let lvl = s.get("level").and_then(|v| v.as_u64()).unwrap_or(0) as u32; - lvl <= 3 - }) - .cloned() - .collect(); - - Ok(serde_json::json!({ - "path": path, - "rev": rev, - "stats": { - "chars": text.chars().count(), - "words": text.split_whitespace().count(), - "lines": text.lines().count(), - "headings": toc.len(), - "code_blocks": code_blocks.len(), - "links": links.len(), - "images": images.len(), - }, - "outline": outline, - "headings": toc, - "code_blocks": if include_code { code_blocks } else { vec![] }, - "links": links, - "images": images, - })) -} - -pub fn register_markdown_tools(registry: &mut ToolRegistry) { - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("File path to the Markdown file".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Git revision (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "sections_only".into(), - ToolParam { - name: "sections_only".into(), - param_type: "boolean".into(), - description: Some( - "If true, return only section headings (outline). Default: false".into(), - ), - required: false, - properties: None, - items: None, - }, - ), - ( - "include_code".into(), - ToolParam { - name: "include_code".into(), - param_type: "boolean".into(), - description: Some("Include code blocks in result. Default: true".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "path".into(), - ]), - }; - registry.register( - ToolDefinition::new("read_markdown") - .description("Parse and analyze a Markdown file. Returns document statistics, heading outline, code blocks with languages, links, and images.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - read_markdown_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/file_tools/mod.rs b/libs/fctool/src/file_tools/mod.rs deleted file mode 100644 index 398a251..0000000 --- a/libs/fctool/src/file_tools/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! File reading and search tools for AI agents. -//! -//! Tools for reading structured files (CSV, Excel, Word, PDF, PPT, Markdown, -//! SQL, JSON) and searching across repository files (git_grep). -//! -//! All tools operate on repository blobs (read via git context) or standalone -//! content, returning structured JSON suitable for AI consumption. - -pub mod csv; -pub mod grep; -pub mod json; -pub mod markdown; -pub mod sql; - -use agent::ToolRegistry; - -/// Maximum number of bytes to read from any single file (prevents huge blobs). -const MAX_FILE_SIZE: usize = 2 * 1024 * 1024; // 2MB - -/// Registers all file tools into a ToolRegistry. -pub fn register_all(registry: &mut ToolRegistry) { - grep::register_grep_tools(registry); - csv::register_csv_tools(registry); - markdown::register_markdown_tools(registry); - sql::register_sql_tools(registry); - json::register_json_tools(registry); -} diff --git a/libs/fctool/src/file_tools/sql.rs b/libs/fctool/src/file_tools/sql.rs deleted file mode 100644 index 4addec0..0000000 --- a/libs/fctool/src/file_tools/sql.rs +++ /dev/null @@ -1,227 +0,0 @@ -//! read_sql — parse and analyze SQL files. - -use crate::file_tools::MAX_FILE_SIZE; -use crate::git_tools::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use sqlparser::ast::{ColumnDef, Statement}; -use sqlparser::dialect::{GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect}; -use sqlparser::parser::Parser; -use std::collections::HashMap; - -async fn read_sql_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| "HEAD".to_string()); - let dialect = p - .get("dialect") - .and_then(|v| v.as_str()) - .unwrap_or("generic"); - - let domain = ctx.open_repo(project_name, repo_name).await?; - - let commit_oid = if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - git::commit::types::CommitOid::new(&rev) - } else if let Ok(Some(oid)) = domain.ref_target(&rev) { - oid - } else { - domain - .commit_get_prefix(&rev) - .map_err(|e| e.to_string())? - .oid - }; - - let entry = domain - .tree_entry_by_path_from_commit(&commit_oid, path) - .map_err(|e| e.to_string())?; - let content = domain.blob_content(&entry.oid).map_err(|e| e.to_string())?; - - let data = &content.content; - if data.len() > MAX_FILE_SIZE { - return Err(format!( - "file too large ({} bytes), max {} bytes", - data.len(), - MAX_FILE_SIZE - )); - } - - let text = String::from_utf8_lossy(data); - - let parser_dialect: Box = match dialect { - "mysql" => Box::new(MySqlDialect {}), - "postgresql" | "postgres" => Box::new(PostgreSqlDialect {}), - "sqlite" => Box::new(SQLiteDialect {}), - _ => Box::new(GenericDialect {}), - }; - - let statements = Parser::parse_sql(parser_dialect.as_ref(), &text) - .map_err(|e| format!("SQL parse error: {}", e))?; - - let mut tables: Vec = Vec::new(); - let mut views: Vec = Vec::new(); - let mut functions: Vec = Vec::new(); - let mut indexes: Vec = Vec::new(); - let mut statement_kinds: std::collections::HashMap = - std::collections::HashMap::new(); - - for statement in &statements { - let kind = format!("{:?}", statement) - .split('{') - .next() - .unwrap_or("unknown") - .to_string(); - *statement_kinds.entry(kind).or_insert(0) += 1; - - match statement { - Statement::CreateTable(stmt) => { - let name = stmt.name.to_string(); - let columns: Vec = stmt.columns.iter().map(format_column_def).collect(); - tables.push(serde_json::json!({ - "name": name, - "columns": columns, - "if_not_exists": stmt.if_not_exists, - })); - } - Statement::CreateView { name, query, .. } => { - views.push(serde_json::json!({ - "name": name.to_string(), - "query": query.to_string(), - })); - } - Statement::CreateIndex(stmt) => { - indexes.push(serde_json::json!({ - "name": stmt.name.as_ref().map(|n| n.to_string()).unwrap_or_default(), - "table": stmt.table_name.to_string(), - "columns": stmt.columns.iter().map(|c| c.to_string()).collect::>(), - })); - } - Statement::CreateFunction(stmt) => { - functions.push(serde_json::json!({ - "name": stmt.name.to_string(), - "args": stmt.args.iter().flat_map(|args| args.iter().filter_map(|a| a.name.as_ref().map(|n| n.to_string()))).collect::>(), - "return_type": stmt.return_type.as_ref().map(|r| r.to_string()).unwrap_or_default(), - })); - } - _ => {} - } - } - - Ok(serde_json::json!({ - "path": path, - "rev": rev, - "dialect": dialect, - "statement_count": statements.len(), - "statement_kinds": statement_kinds, - "tables": tables, - "views": views, - "functions": functions, - "indexes": indexes, - })) -} - -fn format_column_def(col: &ColumnDef) -> String { - let name = col.name.to_string(); - let data_type = col.data_type.to_string(); - format!("{} {}", name, data_type) -} - -pub fn register_sql_tools(registry: &mut ToolRegistry) { - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("File path to the SQL file".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Git revision (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "dialect".into(), - ToolParam { - name: "dialect".into(), - param_type: "string".into(), - description: Some( - "SQL dialect: generic, mysql, postgresql, sqlite. Default: generic".into(), - ), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "path".into(), - ]), - }; - registry.register( - ToolDefinition::new("read_sql") - .description("Parse and analyze a SQL file. Extracts CREATE TABLE statements (with columns and types), CREATE VIEW, CREATE INDEX, CREATE FUNCTION, and counts all statement types.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - read_sql_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/blob.rs b/libs/fctool/src/git_tools/blob.rs deleted file mode 100644 index 3f3e6b1..0000000 --- a/libs/fctool/src/git_tools/blob.rs +++ /dev/null @@ -1,405 +0,0 @@ -//! Git blob tools — raw object-level operations on blob OIDs. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use base64::Engine; -use std::collections::HashMap; - -async fn git_blob_info_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let blob_oid = git::commit::types::CommitOid::new(oid); - let info = domain.blob_get(&blob_oid).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "oid": info.oid.to_string(), - "size": info.size, - "is_binary": info.is_binary, - })) -} - -async fn git_blob_exists_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let blob_oid = git::commit::types::CommitOid::new(oid); - let exists = domain.blob_exists(&blob_oid); - - Ok(serde_json::json!({ "oid": blob_oid.to_string(), "exists": exists })) -} - -async fn git_blob_content_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?; - let max_size = p - .get("max_size") - .and_then(|v| v.as_u64()) - .unwrap_or(1_048_576) as usize; // 1MB default - - let domain = ctx.open_repo(project_name, repo_name).await?; - let blob_oid = git::commit::types::CommitOid::new(oid); - let blob = domain.blob_content(&blob_oid).map_err(|e| e.to_string())?; - - if blob.size > max_size { - return Err(format!( - "blob too large ({} bytes), max {} bytes. Use a smaller max_size or retrieve the raw OID.", - blob.size, max_size - )); - } - - let (content, is_binary) = if blob.is_binary { - ( - base64::engine::general_purpose::STANDARD.encode(&blob.content), - true, - ) - } else { - (String::from_utf8_lossy(&blob.content).to_string(), false) - }; - - Ok(serde_json::json!({ - "oid": blob.oid.to_string(), - "size": blob.size, - "is_binary": is_binary, - "content": content, - })) -} - -async fn git_blob_create_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let content = p - .get("content") - .and_then(|v| v.as_str()) - .ok_or("missing content")?; - let encoding = p - .get("encoding") - .and_then(|v| v.as_str()) - .unwrap_or("utf-8"); - - let data = match encoding { - "base64" => base64::engine::general_purpose::STANDARD - .decode(content) - .map_err(|e| format!("invalid base64: {}", e))?, - "utf-8" => content.as_bytes().to_vec(), - other => { - return Err(format!( - "unsupported encoding '{}'. Use 'utf-8' or 'base64'.", - other - )); - } - }; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let oid = domain.blob_create(&data).map_err(|e| e.to_string())?; - let info = domain.blob_get(&oid).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "oid": info.oid.to_string(), - "size": info.size, - "is_binary": info.is_binary, - })) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - // git_blob_info - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "oid".into(), - ToolParam { - name: "oid".into(), - param_type: "string".into(), - description: Some("Blob OID (full 40-char hex or short prefix)".into()), - required: true, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "oid".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_blob_info") - .description("Get metadata about a git blob by its OID. Returns size and whether the blob is binary.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - git_blob_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_blob_exists - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "oid".into(), - ToolParam { - name: "oid".into(), - param_type: "string".into(), - description: Some("Blob OID to check".into()), - required: true, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "oid".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_blob_exists") - .description("Check whether a git blob exists in the repository by its OID.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - git_blob_exists_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_blob_content - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "oid".into(), - ToolParam { - name: "oid".into(), - param_type: "string".into(), - description: Some("Blob OID to retrieve content for".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "max_size".into(), - ToolParam { - name: "max_size".into(), - param_type: "integer".into(), - description: Some("Maximum blob size in bytes (default: 1MB)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "oid".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_blob_content") - .description("Retrieve the raw content of a git blob by its OID. Binary content is base64-encoded. Limits to 1MB by default.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - git_blob_content_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_blob_create - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "content".into(), - ToolParam { - name: "content".into(), - param_type: "string".into(), - description: Some("Blob content (utf-8 string or base64-encoded bytes)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "encoding".into(), - ToolParam { - name: "encoding".into(), - param_type: "string".into(), - description: Some("Encoding of content: 'utf-8' (default) or 'base64'".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "content".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_blob_create") - .description("Create a new git blob in the repository. Writes the raw content to the object database and returns the new blob OID. Supports both utf-8 text and base64-encoded binary content.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = GitToolCtx::new(ctx); - Box::pin(async move { - git_blob_create_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/branch.rs b/libs/fctool/src/git_tools/branch.rs deleted file mode 100644 index 2ea4bb9..0000000 --- a/libs/fctool/src/git_tools/branch.rs +++ /dev/null @@ -1,402 +0,0 @@ -//! Git branch tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use std::collections::HashMap; - -async fn git_branch_list_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let remote_only = p - .get("remote_only") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let branches = domain.branch_list(remote_only).map_err(|e| e.to_string())?; - - let result: Vec<_> = branches.iter().map(|b| { - let oid = b.oid.to_string(); - serde_json::json!({ - "name": b.name, "oid": oid.clone(), "short_oid": oid.get(..7).unwrap_or(&oid).to_string(), - "is_head": b.is_head, "is_remote": b.is_remote, "is_current": b.is_current, - "upstream": b.upstream.clone() - }) - }).collect(); - - Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) -} - -async fn git_branch_info_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let name = p - .get("name") - .and_then(|v| v.as_str()) - .ok_or("missing name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let info = domain.branch_get(name).map_err(|e| e.to_string())?; - - let ahead_behind = if let Some(ref upstream) = info.upstream { - match domain.branch_ahead_behind(name, upstream) { - Ok((ahead, behind)) => Some(serde_json::json!({ "ahead": ahead, "behind": behind })), - Err(e) => Some(serde_json::json!({ "error": e.to_string() })), - } - } else { - None - }; - - Ok(serde_json::json!({ - "branch": { "name": info.name, "oid": info.oid.to_string(), "is_head": info.is_head, - "is_remote": info.is_remote, "is_current": info.is_current, "upstream": info.upstream }, - "ahead_behind": ahead_behind - })) -} - -async fn git_branches_merged_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let branch = p - .get("branch") - .and_then(|v| v.as_str()) - .ok_or("missing branch")?; - let into = p - .get("into") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "main".to_string()); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let is_merged = domain - .branch_is_merged(branch, &into) - .map_err(|e| e.to_string())?; - - // Resolve branch names to commit OIDs before calling merge_base - let branch_oid = domain - .branch_target(branch) - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("branch '{}' not found or has no target", branch))?; - let into_oid = domain - .branch_target(&into) - .map_err(|e| e.to_string())? - .ok_or_else(|| format!("branch '{}' not found or has no target", into))?; - let merge_base = domain - .merge_base(&branch_oid, &into_oid) - .map(|oid| oid.to_string()) - .ok(); - - Ok( - serde_json::json!({ "branch": branch, "into": into, "is_merged": is_merged, "merge_base": merge_base }), - ) -} - -async fn git_branch_diff_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let local = p - .get("local") - .and_then(|v| v.as_str()) - .ok_or("missing local")?; - let remote = p - .get("remote") - .and_then(|v| v.as_str()) - .unwrap_or(local) - .to_string(); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let diff = domain - .branch_diff(local, &remote) - .map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ "ahead": diff.ahead, "behind": diff.behind, "diverged": diff.diverged })) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - let mut p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ]); - - // git_branch_list - p.insert( - "remote_only".into(), - ToolParam { - name: "remote_only".into(), - param_type: "boolean".into(), - description: Some("If true, list only remote-tracking branches".into()), - required: false, - properties: None, - items: None, - }, - ); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(std::mem::take(&mut p)), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }; - p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ]); - registry.register( - ToolDefinition::new("git_branch_list") - .description("List all local or remote branches with their current HEAD commit.") - .parameters(schema), - ToolHandler::new(move |ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_branch_list_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_branch_info - p.insert( - "name".into(), - ToolParam { - name: "name".into(), - param_type: "string".into(), - description: Some("Branch name (e.g. main, feature/my-branch)".into()), - required: true, - properties: None, - items: None, - }, - ); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(std::mem::take(&mut p)), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "name".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_branch_info").description("Get detailed information about a specific branch including ahead/behind status vs upstream.").parameters(schema), - ToolHandler::new(move |ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_branch_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_branches_merged - p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "branch".into(), - ToolParam { - name: "branch".into(), - param_type: "string".into(), - description: Some("Branch to check (source)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "into".into(), - ToolParam { - name: "into".into(), - param_type: "string".into(), - description: Some("Branch to check against (target, default: main)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "branch".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_branches_merged").description("Check whether a branch has been merged into another branch, and find the merge base.").parameters(schema), - ToolHandler::new(move |ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_branches_merged_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_branch_diff - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "local".into(), - ToolParam { - name: "local".into(), - param_type: "string".into(), - description: Some("Local branch name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "remote".into(), - ToolParam { - name: "remote".into(), - param_type: "string".into(), - description: Some("Remote branch name (defaults to local)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "local".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_branch_diff").description("Compare a local branch against its remote counterpart to see how many commits ahead/behind they are.").parameters(schema), - ToolHandler::new(move |ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_branch_diff_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/commit.rs b/libs/fctool/src/git_tools/commit.rs deleted file mode 100644 index 0106fb2..0000000 --- a/libs/fctool/src/git_tools/commit.rs +++ /dev/null @@ -1,621 +0,0 @@ -//! Git commit-related tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use chrono::TimeZone; -use std::collections::HashMap; - -// --- Execution functions for each tool --- - -async fn git_log_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()); - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; - let skip = p.get("skip").and_then(|v| v.as_u64()).unwrap_or(0) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let commits = domain - .commit_log(rev.as_deref(), skip, limit) - .map_err(|e| e.to_string())?; - - // Flatten to simple JSON - let result: Vec<_> = commits - .iter() - .map(|c| { - use chrono::TimeZone; - let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60); - let time_str = chrono::Utc - .timestamp_opt(ts, 0) - .single() - .map(|dt| dt.to_rfc3339()) - .unwrap_or_else(|| format!("{}", c.author.time_secs)); - - let oid = c.oid.to_string(); - let short_oid = oid.get(..7).unwrap_or(&oid).to_string(); - - serde_json::json!({ - "oid": oid, - "short_oid": short_oid, - "message": c.message, - "summary": c.summary, - "author_name": c.author.name, - "author_email": c.author.email, - "author_time": time_str, - "committer_name": c.committer.name, - "committer_email": c.committer.email, - "parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::>(), - "tree_oid": c.tree_id.to_string() - }) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) -} - -/// Resolve a rev string to commit metadata using the full rev-parse machinery -/// (branch names, tags, HEAD, hex prefixes, etc.). -fn resolve_commit( - domain: &git::GitDomain, - rev: &str, -) -> Result { - let oid = domain.resolve_rev(rev).map_err(|e| e.to_string())?; - domain.commit_get(&oid).map_err(|e| e.to_string()) -} - -async fn git_show_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let rev = p.get("rev").and_then(|v| v.as_str()).ok_or("missing rev")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let meta = resolve_commit(&domain, rev).map_err(|e| e.to_string())?; - - let refs = domain.commit_refs(&meta.oid).map_err(|e| e.to_string())?; - - use chrono::TimeZone; - let ts = meta.author.time_secs - (meta.author.offset_minutes as i64 * 60); - let author_time = chrono::Utc - .timestamp_opt(ts, 0) - .single() - .map(|dt| dt.to_rfc3339()) - .unwrap_or_else(|| format!("{}", meta.author.time_secs)); - - let oid = meta.oid.to_string(); - let short_oid = oid.get(..7).unwrap_or(&oid).to_string(); - - Ok(serde_json::json!({ - "commit": { - "oid": oid, "short_oid": short_oid, "message": meta.message, "summary": meta.summary, - "author_name": meta.author.name, "author_email": meta.author.email, "author_time": author_time, - "committer_name": meta.committer.name, "committer_email": meta.committer.email, - "parent_oids": meta.parent_ids.iter().map(|p| p.to_string()).collect::>(), - "tree_oid": meta.tree_id.to_string() - }, - "refs": refs.into_iter().map(|r| serde_json::json!({ "name": r.name, "is_tag": r.is_tag })).collect::>() - })) -} - -async fn git_search_commits_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let query = p - .get("query") - .and_then(|v| v.as_str()) - .ok_or("missing query")?; - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - // Fetch extra commits to have enough candidates after filtering - let walk_limit = limit.saturating_mul(2).max(100); - let commits = domain - .commit_log(Some("HEAD"), 0, walk_limit) - .map_err(|e| e.to_string())?; - let q = query.to_lowercase(); - - let result: Vec<_> = commits - .iter() - .filter(|c| c.message.to_lowercase().contains(&q)) - .take(limit) - .map(|c| flatten_commit(c)) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) -} - -fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value { - use chrono::TimeZone; - let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60); - let author_time = chrono::Utc - .timestamp_opt(ts, 0) - .single() - .map(|dt| dt.to_rfc3339()) - .unwrap_or_else(|| format!("{}", c.author.time_secs)); - let oid = c.oid.to_string(); - serde_json::json!({ - "oid": oid.clone(), - "short_oid": oid.get(..7).unwrap_or(&oid).to_string(), - "message": c.message, "summary": c.summary, - "author_name": c.author.name, "author_email": c.author.email, "author_time": author_time, - "committer_name": c.committer.name, "committer_email": c.committer.email, - "parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::>(), - "tree_oid": c.tree_id.to_string() - }) -} - -async fn git_commit_info_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let rev = p.get("rev").and_then(|v| v.as_str()).ok_or("missing rev")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let meta = resolve_commit(&domain, rev).map_err(|e| e.to_string())?; - - Ok(flatten_commit(&meta)) -} - -async fn git_graph_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()); - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let commits = domain - .commit_log(rev.as_deref(), 0, limit) - .map_err(|e| e.to_string())?; - - let mut col_map: std::collections::HashMap = std::collections::HashMap::new(); - let lines: Vec<_> = commits - .iter() - .map(|m| { - let lane_index = *col_map.get(m.oid.as_str()).unwrap_or(&0); - let oid = m.oid.to_string(); - let refs = match domain.commit_refs(&m.oid) { - Ok(refs) => refs - .iter() - .map(|r| { - if r.is_tag { - format!("tag: {}", r.name.trim_start_matches("refs/tags/")) - } else if r.is_remote { - r.name.trim_start_matches("refs/remotes/").to_string() - } else { - r.name.trim_start_matches("refs/heads/").to_string() - } - }) - .collect::>() - .join(", "), - Err(_) => String::new(), - }; - for (i, p) in m.parent_ids.iter().enumerate() { - if i == 0 { - col_map.insert(p.to_string(), lane_index); - } else { - col_map.remove(p.as_str()); - } - } - let ts = m.author.time_secs - (m.author.offset_minutes as i64 * 60); - let author_time = chrono::Utc - .timestamp_opt(ts, 0) - .single() - .map(|dt| dt.to_rfc3339()) - .unwrap_or_else(|| format!("{}", m.author.time_secs)); - - serde_json::json!({ - "oid": oid.clone(), - "short_oid": oid.get(..7).unwrap_or(&oid).to_string(), - "refs": refs, - "short_message": m.summary, - "lane_index": lane_index, - "author_name": m.author.name, - "author_email": m.author.email, - "author_time": author_time, - "parent_oids": m.parent_ids.iter().map(|p| p.to_string()).collect::>() - }) - }) - .collect(); - - Ok(serde_json::to_value(lines).map_err(|e| e.to_string())?) -} - -async fn git_reflog_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let ref_name = p - .get("ref_name") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let entries = domain - .reflog_entries(ref_name.as_deref()) - .map_err(|e| e.to_string())?; - - let result: Vec<_> = entries - .iter() - .take(limit) - .map(|e| { - // Convert to UTC by subtracting the timezone offset, consistent - // with all other timestamp conversions in this module. - let ts = e.time_secs - (e.offset_minutes as i64 * 60); - let time_str = chrono::Utc - .timestamp_opt(ts, 0) - .single() - .map(|dt| dt.to_rfc3339()) - .unwrap_or_else(|| format!("{}", e.time_secs)); - serde_json::json!({ - "oid_new": e.oid_new.to_string(), "oid_old": e.oid_old.to_string(), - "committer_name": e.committer_name, "committer_email": e.committer_email, - "time": time_str, "message": e.message, "ref_name": e.ref_name - }) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) -} - -/// Common required params used across all git tools. -fn common_params() -> HashMap { - HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ]) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - // git_log - let mut p = common_params(); - p.insert( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Revision/range specifier (branch name, commit hash, etc.)".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "limit".into(), - ToolParam { - name: "limit".into(), - param_type: "integer".into(), - description: Some("Maximum number of commits to return".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "skip".into(), - ToolParam { - name: "skip".into(), - param_type: "integer".into(), - description: Some("Number of commits to skip".into()), - required: false, - properties: None, - items: None, - }, - ); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }; - registry.register( - ToolDefinition::new("git_log") - .description("List commits in a repository, optionally filtered by revision range.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_log_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_show - let mut p = common_params(); - p.insert( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Revision to show (commit hash, branch, tag)".into()), - required: true, - properties: None, - items: None, - }, - ); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "rev".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_show") - .description( - "Show detailed commit information including message, author, refs, and diff stats.", - ) - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_show_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_search_commits - let mut p = common_params(); - p.insert( - "query".into(), - ToolParam { - name: "query".into(), - param_type: "string".into(), - description: Some("Keyword to search in commit messages".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "limit".into(), - ToolParam { - name: "limit".into(), - param_type: "integer".into(), - description: Some("Maximum results to return".into()), - required: false, - properties: None, - items: None, - }, - ); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "query".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_search_commits") - .description("Search commit messages for a keyword and return matching commits.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_search_commits_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_commit_info - let mut p = common_params(); - p.insert( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some( - "Revision to look up (full or short commit hash, branch, tag)".into(), - ), - required: true, - properties: None, - items: None, - }, - ); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "rev".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_commit_info") - .description( - "Get detailed metadata for a specific commit (author, committer, parents, tree).", - ) - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_commit_info_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_graph - let mut p = common_params(); - p.insert( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Starting revision (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "limit".into(), - ToolParam { - name: "limit".into(), - param_type: "integer".into(), - description: Some("Maximum number of commits (default: 20)".into()), - required: false, - properties: None, - items: None, - }, - ); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }; - registry.register( - ToolDefinition::new("git_graph") - .description("Show an ASCII commit graph with branch lanes and refs.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_graph_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_reflog - let mut p = common_params(); - p.insert( - "ref_name".into(), - ToolParam { - name: "ref_name".into(), - param_type: "string".into(), - description: Some( - "Reference name (e.g. refs/heads/main). Defaults to all refs.".into(), - ), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "limit".into(), - ToolParam { - name: "limit".into(), - param_type: "integer".into(), - description: Some("Maximum number of entries (default: 50)".into()), - required: false, - properties: None, - items: None, - }, - ); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }; - registry.register( - ToolDefinition::new("git_reflog") - .description( - "Show the reference log (reflog) recording when branch tips and refs were updated.", - ) - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_reflog_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/ctx.rs b/libs/fctool/src/git_tools/ctx.rs deleted file mode 100644 index c446f64..0000000 --- a/libs/fctool/src/git_tools/ctx.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Context wrapper for git tool handlers. -//! -//! Provides `GitToolCtx` which wraps `ToolContext` and adds git-domain operations. - -use agent::ToolContext; -use git::GitDomain; -use models::projects::project; -use models::repos::repo; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; - -/// Wrapper around `ToolContext` providing git-domain operations for tool handlers. -#[derive(Clone)] -pub struct GitToolCtx { - pub ctx: ToolContext, -} - -impl GitToolCtx { - pub fn new(ctx: ToolContext) -> Self { - Self { ctx } - } - - /// Opens a git repository by project name and repo name. - pub async fn open_repo( - &self, - project_name: &str, - repo_name: &str, - ) -> Result { - let db = self.ctx.db(); - resolve_project_and_repo(db, project_name, repo_name) - .await - .and_then(|(_, path)| GitDomain::open(&path).map_err(|e| e.to_string())) - } -} - -/// Free helper to resolve project_id + storage_path from names. Used by registry. -async fn resolve_project_and_repo( - db: &db::database::AppDatabase, - project_name: &str, - repo_name: &str, -) -> Result<(uuid::Uuid, String), String> { - let project = project::Entity::find() - .filter(project::Column::Name.eq(project_name)) - .one(db) - .await - .map_err(|e| format!("DB error looking up project '{}': {}", project_name, e))? - .ok_or_else(|| format!("project '{}' not found", project_name))?; - - let repo_model = repo::Entity::find() - .filter(repo::Column::Project.eq(project.id)) - .filter(repo::Column::RepoName.eq(repo_name)) - .one(db) - .await - .map_err(|e| { - format!( - "DB error looking up repo '{}/{}': {}", - project_name, repo_name, e - ) - })? - .ok_or_else(|| format!("repo '{}/{}' not found", project_name, repo_name))?; - - Ok((project.id, repo_model.storage_path)) -} diff --git a/libs/fctool/src/git_tools/diff.rs b/libs/fctool/src/git_tools/diff.rs deleted file mode 100644 index f29afc6..0000000 --- a/libs/fctool/src/git_tools/diff.rs +++ /dev/null @@ -1,490 +0,0 @@ -//! Git diff and blame tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use std::collections::HashMap; - -async fn git_diff_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let base = p - .get("base") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let head = p - .get("head") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let paths = p.get("paths").and_then(|v| v.as_array()).map(|a| { - a.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect::>() - }); - - let domain = ctx.open_repo(project_name, repo_name).await?; - - let resolve = |rev: &str| -> Result { - if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - Ok(git::commit::types::CommitOid::new(rev)) - } else if let Ok(Some(oid)) = domain.ref_target(rev) { - Ok(oid) - } else { - domain - .commit_get_prefix(rev) - .map_err(|e| e.to_string()) - .map(|m| m.oid) - } - }; - - let base_oid = match &base { - Some(b) => Some(resolve(b)?), - None => None, - }; - let head_oid = match &head { - Some(h) => Some(resolve(h)?), - None => None, - }; - - let opts = paths - .map(|ps| { - let mut o = git::diff::types::DiffOptions::new(); - for p in ps { - o = o.pathspec(&p); - } - Some(o) - }) - .flatten(); - - let result = match (&base_oid, &head_oid) { - (None, None) => { - // Check if repo has any commits before attempting to diff - if domain.repo().head().is_err() { - return Err("No commits found in repository".into()); - } - let head_oid = domain - .ref_target("HEAD") - .map_err(|e| e.to_string())? - .ok_or_else(|| "HEAD reference not found".to_string())?; - // Bare repos have no working tree — use tree-to-tree diff instead - if domain.repo().is_bare() { - domain - .diff_tree_to_tree(None, Some(&head_oid), opts) - .map_err(|e| e.to_string())? - } else { - domain - .diff_commit_to_workdir(&head_oid, opts) - .map_err(|e| e.to_string())? - } - } - (Some(base), None) => { - if domain.repo().is_bare() { - domain - .diff_tree_to_tree(Some(base), None, opts) - .map_err(|e| e.to_string())? - } else { - domain - .diff_commit_to_workdir(base, opts) - .map_err(|e| e.to_string())? - } - } - (Some(base), Some(head_oid_val)) => domain - .diff_tree_to_tree(Some(base), Some(head_oid_val), opts) - .map_err(|e| e.to_string())?, - (None, Some(_)) => { - return Err("base revision required when head is specified".into()); - } - }; - - use git::diff::types::DiffDeltaStatus; - let files: Vec<_> = result.deltas.iter().map(|d| { - let (path, is_binary) = if d.status == DiffDeltaStatus::Deleted { - (d.old_file.path.clone(), d.old_file.is_binary) - } else { - (d.new_file.path.clone(), d.new_file.is_binary) - }; - serde_json::json!({ "path": path, "status": format!("{:?}", d.status), "is_binary": is_binary }) - }).collect(); - - Ok(serde_json::json!({ - "stats": { "files_changed": result.stats.files_changed, "insertions": result.stats.insertions, "deletions": result.stats.deletions }, - "files": files - })) -} - -async fn git_diff_stats_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let base = p - .get("base") - .and_then(|v| v.as_str()) - .ok_or("missing base")?; - let head = p - .get("head") - .and_then(|v| v.as_str()) - .ok_or("missing head")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - - let resolve = |rev: &str| -> Result { - if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - Ok(git::commit::types::CommitOid::new(rev)) - } else if let Ok(Some(oid)) = domain.ref_target(rev) { - Ok(oid) - } else { - domain - .commit_get_prefix(rev) - .map_err(|e| e.to_string()) - .map(|m| m.oid) - } - }; - let b = resolve(base).map_err(|e| e.to_string())?; - let h = resolve(head).map_err(|e| e.to_string())?; - let stats = domain.diff_stats(&b, &h).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "files_changed": stats.files_changed, - "insertions": stats.insertions, - "deletions": stats.deletions - })) -} - -async fn git_blame_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "HEAD".to_string()); - let from_line = p - .get("from_line") - .and_then(|v| v.as_u64().map(|n| n as u32)); - let to_line = p.get("to_line").and_then(|v| v.as_u64().map(|n| n as u32)); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let oid = if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - Ok(git::commit::types::CommitOid::new(&rev)) - } else if let Ok(Some(oid)) = domain.ref_target(&rev) { - Ok(oid) - } else { - domain - .commit_get_prefix(&rev) - .map_err(|e| e.to_string()) - .map(|m| m.oid) - }?; - - use git::blame::ops::BlameOptions; - let mut bopts = BlameOptions::new(); - if let Some(fl) = from_line { - bopts = bopts.min_line(fl as usize); - } - if let Some(tl) = to_line { - bopts = bopts.max_line(tl as usize); - } - - let hunks = domain - .blame_file(&oid, path, Some(bopts)) - .map_err(|e| e.to_string())?; - - let result: Vec<_> = hunks - .iter() - .map(|h| { - let oid = h.commit_oid.to_string(); - serde_json::json!({ - "commit_oid": oid.clone(), - "short_oid": oid.get(..7).unwrap_or(&oid).to_string(), - "final_start_line": h.final_start_line, - "final_lines": h.final_lines, - "orig_start_line": h.orig_start_line, - "orig_path": h.orig_path, - "boundary": h.boundary - }) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - // git_diff - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "base".into(), - ToolParam { - name: "base".into(), - param_type: "string".into(), - description: Some( - "Base revision (commit hash or branch). Defaults to HEAD.".into(), - ), - required: false, - properties: None, - items: None, - }, - ), - ( - "head".into(), - ToolParam { - name: "head".into(), - param_type: "string".into(), - description: Some( - "Head revision to diff against base. Requires base to be set.".into(), - ), - required: false, - properties: None, - items: None, - }, - ), - ( - "paths".into(), - ToolParam { - name: "paths".into(), - param_type: "array".into(), - description: Some("Filter diff to specific file paths".into()), - required: false, - properties: None, - items: Some(Box::new(ToolParam { - name: "".into(), - param_type: "string".into(), - description: None, - required: false, - properties: None, - items: None, - })), - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }; - registry.register( - ToolDefinition::new("git_diff").description("Show file changes between two commits, or between a commit and the working directory.").parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_diff_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_diff_stats - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "base".into(), - ToolParam { - name: "base".into(), - param_type: "string".into(), - description: Some("Base revision".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "head".into(), - ToolParam { - name: "head".into(), - param_type: "string".into(), - description: Some("Head revision".into()), - required: true, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "base".into(), - "head".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_diff_stats").description("Get aggregated diff statistics (files changed, insertions, deletions) between two revisions.").parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_diff_stats_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_blame - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("File path to blame".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Revision to blame from (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "from_line".into(), - ToolParam { - name: "from_line".into(), - param_type: "integer".into(), - description: Some("Start line number for blame range".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "to_line".into(), - ToolParam { - name: "to_line".into(), - param_type: "integer".into(), - description: Some("End line number for blame range".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "path".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_blame") - .description( - "Show what revision and author last modified each line of a file (git blame).", - ) - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_blame_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/kb.rs b/libs/fctool/src/git_tools/kb.rs deleted file mode 100644 index 50c9f85..0000000 --- a/libs/fctool/src/git_tools/kb.rs +++ /dev/null @@ -1,445 +0,0 @@ -//! Knowledge-base (documentation) repository tools for AI. -//! -//! Provides tools for AI to quickly index, read, and search -//! through documentation / knowledge-base repositories. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use std::collections::HashMap; - -// ── Helpers ──────────────────────────────────────────────────────────────────── - -/// Extract frontmatter (--- ... ---) from markdown content. -fn extract_frontmatter(raw: &str) -> (Option<&str>, &str) { - let trimmed = raw.trim_start(); - if !trimmed.starts_with("---") { - return (None, trimmed); - } - if let Some(end) = trimmed[3..].find("---") { - let fm = &trimmed[3..end + 3]; - let rest = trimmed[3 + end + 3..].trim_start(); - (Some(fm), rest) - } else { - (None, trimmed) - } -} - -/// Extract all headings (lines starting with #) from markdown body. -fn extract_headings(body: &str) -> Vec { - body.lines() - .filter_map(|line| { - let trimmed = line.trim(); - if trimmed.starts_with("# ") { - Some(serde_json::json!({ "level": 1, "text": trimmed[2..].trim() })) - } else if trimmed.starts_with("## ") { - Some(serde_json::json!({ "level": 2, "text": trimmed[3..].trim() })) - } else if trimmed.starts_with("### ") { - Some(serde_json::json!({ "level": 3, "text": trimmed[4..].trim() })) - } else if trimmed.starts_with("#### ") { - Some(serde_json::json!({ "level": 4, "text": trimmed[5..].trim() })) - } else { - None - } - }) - .collect() -} - -/// Resolve HEAD to a tree for traversal. -fn head_tree(domain: &git::GitDomain) -> Result, String> { - let repo = domain.repo(); - let head = repo.head().map_err(|e| format!("no HEAD: {e}"))?; - head.peel_to_tree().map_err(|e| format!("no tree: {e}")) -} - -// ── Tool executors ───────────────────────────────────────────────────────────── - -/// Tool: repo_doc_index — list all markdown docs with frontmatter -async fn repo_doc_index_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let tree = head_tree(&domain)?; - - let mut docs = Vec::new(); - let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree, String::new())]; - - while let Some((current_tree, prefix)) = stack.pop() { - for entry in current_tree.iter() { - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - let entry_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - match entry.kind() { - Some(git2::ObjectType::Tree) => { - if !name.starts_with('.') - && !matches!( - name, - "node_modules" | "target" | ".git" | ".github" | ".next" | "dist" - ) - { - if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) { - stack.push((subtree, entry_path)); - } - } - } - Some(git2::ObjectType::Blob) => { - if name.ends_with(".md") - || name.ends_with(".mdx") - || name.ends_with(".markdown") - { - if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) { - let raw = String::from_utf8_lossy(blob.content()); - let (fm_raw, body) = extract_frontmatter(&raw); - let metadata: serde_json::Value = fm_raw - .and_then(|fm| serde_json::from_str(fm).ok()) - .unwrap_or_default(); - - let title = metadata - .get("title") - .and_then(|v| v.as_str()) - .map(String::from) - .or_else(|| { - // Fall back to first # heading - body.lines() - .find(|l| l.trim().starts_with("# ")) - .map(|l| l.trim()[2..].trim().to_string()) - }); - - let description = metadata - .get("description") - .and_then(|v| v.as_str()) - .map(String::from) - .or_else(|| { - // Fall back to first non-heading non-empty line - body.lines() - .find(|l| { - let t = l.trim(); - !t.is_empty() && !t.starts_with('#') - }) - .map(|l| l.trim().chars().take(200).collect::()) - }); - - let tags: Vec = metadata - .get("tags") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default(); - - let headings = extract_headings(body); - - docs.push(serde_json::json!({ - "path": entry_path, - "title": title, - "description": description, - "tags": tags, - "headings": headings, - "size": raw.len(), - })); - } - } - } - _ => {} - } - } - } - - // Sort by path for consistent ordering - docs.sort_by(|a, b| { - a["path"] - .as_str() - .unwrap_or("") - .cmp(b["path"].as_str().unwrap_or("")) - }); - - Ok(serde_json::json!({ - "total": docs.len(), - "docs": docs - })) -} - -/// Tool: repo_doc_read — read a specific document with structure -async fn repo_doc_read_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let tree = head_tree(&domain)?; - - // Navigate to the file using git2 path lookup - let entry = tree - .get_path(std::path::Path::new(path)) - .map_err(|e| format!("file '{}' not found: {e}", path))?; - let blob = entry - .to_object(repo) - .and_then(|o| o.peel_to_blob()) - .map_err(|e| format!("not a blob: {e}"))?; - - let raw = String::from_utf8_lossy(blob.content()); - let (fm_raw, body) = extract_frontmatter(&raw); - let metadata: serde_json::Value = fm_raw - .and_then(|fm| serde_json::from_str(fm).ok()) - .unwrap_or_default(); - - let title = metadata - .get("title") - .and_then(|v| v.as_str()) - .map(String::from) - .or_else(|| { - body.lines() - .find(|l| l.trim().starts_with("# ")) - .map(|l| l.trim()[2..].trim().to_string()) - }); - - let headings = extract_headings(body); - - Ok(serde_json::json!({ - "path": path, - "title": title, - "metadata": metadata, - "headings": headings, - "content": body.to_string(), - "size": raw.len(), - })) -} - -/// Tool: repo_doc_search — search through docs content -async fn repo_doc_search_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let keyword = p - .get("keyword") - .and_then(|v| v.as_str()) - .ok_or("missing keyword")?; - let context_lines = p.get("context_lines").and_then(|v| v.as_u64()).unwrap_or(2) as usize; - - let keyword_lower = keyword.to_lowercase(); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let tree = head_tree(&domain)?; - - let mut matches: Vec = Vec::new(); - let mut matched_files = 0u64; - let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree, String::new())]; - - while let Some((current_tree, prefix)) = stack.pop() { - for entry in current_tree.iter() { - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - let entry_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - match entry.kind() { - Some(git2::ObjectType::Tree) => { - if !name.starts_with('.') - && !matches!( - name, - "node_modules" | "target" | ".git" | ".github" | ".next" | "dist" - ) - { - if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) { - stack.push((subtree, entry_path)); - } - } - } - Some(git2::ObjectType::Blob) => { - if name.ends_with(".md") - || name.ends_with(".mdx") - || name.ends_with(".markdown") - || name.ends_with(".txt") - { - if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) { - let content = String::from_utf8_lossy(blob.content()); - let lines: Vec<&str> = content.lines().collect(); - let mut file_hits: Vec = Vec::new(); - let mut hit_lines = Vec::new(); - - for (i, line) in lines.iter().enumerate() { - if line.to_lowercase().contains(&keyword_lower) { - hit_lines.push(i); - } - } - - if !hit_lines.is_empty() { - matched_files += 1; - // Merge overlapping context windows - let mut windows: Vec<(usize, usize)> = Vec::new(); - for &line_idx in &hit_lines { - let start = line_idx.saturating_sub(context_lines); - let end = (line_idx + context_lines + 1).min(lines.len()); - if let Some(last) = windows.last_mut() { - if start <= last.1 { - last.1 = end; - continue; - } - } - windows.push((start, end)); - } - - for (start, end) in windows { - let snippet: Vec = - lines[start..end].iter().map(|l| l.to_string()).collect(); - file_hits.push(serde_json::json!({ - "line_start": start + 1, - "line_end": end, - "snippet": snippet.join("\n"), - })); - } - - matches.push(serde_json::json!({ - "path": entry_path, - "hit_count": hit_lines.len(), - "snippets": file_hits, - })); - } - } - } - } - _ => {} - } - } - } - - Ok(serde_json::json!({ - "keyword": keyword, - "matched_files": matched_files, - "total_hits": matches.iter().map(|m| m["hit_count"].as_u64().unwrap_or(0)).sum::(), - "matches": matches, - })) -} - -// ── Registration ─────────────────────────────────────────────────────────────── - -macro_rules! param { - ($name:expr, $type:expr, $desc:expr, $required:expr) => { - ( - $name.into(), - ToolParam { - name: $name.into(), - param_type: $type.into(), - description: Some($desc.into()), - required: $required, - properties: None, - items: None, - }, - ) - }; -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - // repo_doc_index - registry.register( - ToolDefinition::new("repo_doc_index") - .description("Index all documentation files in a knowledge-base repository. Lists every .md/.mdx file with its title, description, tags, and heading structure. Use this first to understand what documents are available.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_doc_index_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_doc_read - registry.register( - ToolDefinition::new("repo_doc_read") - .description("Read a specific document from a knowledge-base repository. Returns the full markdown content plus extracted frontmatter metadata and heading structure. Use this after repo_doc_index to read the documents you need.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - param!("path", "string", "Document file path within the repository", true), - ])), - required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_doc_read_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_doc_search - registry.register( - ToolDefinition::new("repo_doc_search") - .description("Search through all documentation files in a knowledge-base repository for a keyword. Returns matching file paths, hit counts, and context snippets. Use this to find which documents discuss a specific topic.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - param!("keyword", "string", "Search keyword (case-insensitive)", true), - param!("context_lines", "integer", "Number of context lines around each match (default: 2)", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into(), "keyword".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_doc_search_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/lfs.rs b/libs/fctool/src/git_tools/lfs.rs deleted file mode 100644 index b5161a6..0000000 --- a/libs/fctool/src/git_tools/lfs.rs +++ /dev/null @@ -1,262 +0,0 @@ -//! Git LFS query tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use git::lfs::types::LfsOid; -use std::collections::HashMap; - -async fn git_lfs_summary_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p = parse_args(args)?; - let project_name = required_str(&p, "project_name")?; - let repo_name = required_str(&p, "repo_name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let objects = domain.lfs_object_list().map_err(|e| e.to_string())?; - let cache_size = domain.lfs_cache_size().map_err(|e| e.to_string())?; - let attributes = domain.lfs_gitattributes_list().map_err(|e| e.to_string())?; - let config = domain.lfs_config().map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "object_count": objects.len(), - "cache_size_bytes": cache_size, - "objects": objects.into_iter().map(|oid| oid.to_string()).collect::>(), - "gitattributes": attributes, - "config": { - "endpoint": config.endpoint, - "has_access_token": config.access_token.as_ref().is_some_and(|s| !s.is_empty()), - }, - })) -} - -async fn git_lfs_scan_tree_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p = parse_args(args)?; - let project_name = required_str(&p, "project_name")?; - let repo_name = required_str(&p, "repo_name")?; - let rev = p.get("rev").and_then(|v| v.as_str()).unwrap_or("HEAD"); - let recursive = p.get("recursive").and_then(|v| v.as_bool()).unwrap_or(true); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let commit_oid = resolve_rev(&domain, rev)?; - let commit = domain.commit_get(&commit_oid).map_err(|e| e.to_string())?; - let entries = domain - .lfs_scan_tree(&commit.tree_id, recursive) - .map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "rev": rev, - "commit_oid": commit_oid.to_string(), - "count": entries.len(), - "entries": entries.into_iter().map(|entry| { - serde_json::json!({ - "path": entry.path, - "oid": entry.pointer.oid.to_string(), - "size": entry.pointer.size, - "cached": domain.lfs_object_cached(&entry.pointer.oid), - "extra": entry.pointer.extra, - }) - }).collect::>(), - })) -} - -async fn git_lfs_pointer_info_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p = parse_args(args)?; - let project_name = required_str(&p, "project_name")?; - let repo_name = required_str(&p, "repo_name")?; - let rev = p.get("rev").and_then(|v| v.as_str()).unwrap_or("HEAD"); - let blob_oid = p.get("blob_oid").and_then(|v| v.as_str()); - let path = p.get("path").and_then(|v| v.as_str()); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let blob_oid = if let Some(blob_oid) = blob_oid { - git::commit::types::CommitOid::new(blob_oid) - } else if let Some(path) = path { - let commit_oid = resolve_rev(&domain, rev)?; - domain - .tree_entry_by_path_from_commit(&commit_oid, path) - .map_err(|e| e.to_string())? - .oid - } else { - return Err("either blob_oid or path is required".into()); - }; - - let pointer = domain - .lfs_pointer_from_blob(&blob_oid) - .map_err(|e| e.to_string())?; - - Ok(match pointer { - Some(pointer) => serde_json::json!({ - "is_lfs_pointer": true, - "blob_oid": blob_oid.to_string(), - "pointer": { - "version": pointer.version, - "oid": pointer.oid.to_string(), - "size": pointer.size, - "cached": domain.lfs_object_cached(&pointer.oid), - "object_path": domain.lfs_object_path(&pointer.oid).ok().map(|p| p.to_string_lossy().to_string()), - "extra": pointer.extra, - } - }), - None => serde_json::json!({ - "is_lfs_pointer": false, - "blob_oid": blob_oid.to_string(), - "pointer": null, - }), - }) -} - -async fn git_lfs_object_info_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p = parse_args(args)?; - let project_name = required_str(&p, "project_name")?; - let repo_name = required_str(&p, "repo_name")?; - let oid = LfsOid::new(required_str(&p, "oid")?); - - let domain = ctx.open_repo(project_name, repo_name).await?; - if !oid.is_valid() { - return Err(format!("invalid LFS oid: {}", oid)); - } - let path = domain.lfs_object_path(&oid).map_err(|e| e.to_string())?; - let cached = domain.lfs_object_cached(&oid); - let size = if cached { - std::fs::metadata(&path).ok().map(|m| m.len()) - } else { - None - }; - - Ok(serde_json::json!({ - "oid": oid.to_string(), - "cached": cached, - "path": path.to_string_lossy().to_string(), - "size_bytes": size, - })) -} - -fn resolve_rev( - domain: &git::GitDomain, - rev: &str, -) -> Result { - if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - return Ok(git::commit::types::CommitOid::new(rev)); - } - if let Ok(Some(oid)) = domain.ref_target(rev) { - return Ok(oid); - } - domain - .commit_get_prefix(rev) - .map(|m| m.oid) - .map_err(|e| e.to_string()) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - registry.register( - ToolDefinition::new("git_lfs_summary") - .description("Summarize Git LFS state for a repository: local LFS objects, cache size, .gitattributes LFS patterns, and sanitized LFS config.") - .parameters(base_schema(vec![])), - handler(git_lfs_summary_exec), - ); - - registry.register( - ToolDefinition::new("git_lfs_scan_tree") - .description("Scan a revision tree for Git LFS pointer files. Returns path, LFS object OID, declared size, and local cache status.") - .parameters(base_schema(vec![ - param("rev", "string", "Revision to scan. Default HEAD.", false), - param("recursive", "boolean", "Scan recursively. Default true.", false), - ])), - handler(git_lfs_scan_tree_exec), - ); - - registry.register( - ToolDefinition::new("git_lfs_pointer_info") - .description("Inspect whether a blob or file path is a Git LFS pointer. Provide either blob_oid or path; path is resolved at rev, default HEAD.") - .parameters(base_schema(vec![ - param("blob_oid", "string", "Blob object ID to inspect.", false), - param("path", "string", "File path to inspect at rev.", false), - param("rev", "string", "Revision used when path is provided. Default HEAD.", false), - ])), - handler(git_lfs_pointer_info_exec), - ); - - registry.register( - ToolDefinition::new("git_lfs_object_info") - .description("Inspect one local Git LFS object by SHA-256 OID and report cache path and size if present.") - .parameters(base_schema(vec![param( - "oid", - "string", - "64-character Git LFS object SHA-256 OID.", - true, - )])), - handler(git_lfs_object_info_exec), - ); -} - -fn handler(f: F) -> ToolHandler -where - F: Fn(GitToolCtx, serde_json::Value) -> Fut + Send + Sync + 'static, - Fut: std::future::Future> + Send + 'static, -{ - ToolHandler::new(move |ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - let fut = f(gctx, args); - Box::pin(async move { fut.await.map_err(agent::ToolError::ExecutionError) }) - }) -} - -fn base_schema(extra: Vec<(String, ToolParam)>) -> ToolSchema { - let mut properties = HashMap::from([ - param("project_name", "string", "Project name (slug)", true), - param("repo_name", "string", "Repository name", true), - ]); - let mut required = vec!["project_name".into(), "repo_name".into()]; - required.extend( - extra - .iter() - .filter(|(_, param)| param.required) - .map(|(name, _)| name.clone()), - ); - properties.extend(extra); - ToolSchema { - schema_type: "object".into(), - properties: Some(properties), - required: Some(required), - } -} - -fn parse_args( - args: serde_json::Value, -) -> Result, String> { - serde_json::from_value(args).map_err(|e| e.to_string()) -} - -fn required_str<'a>( - p: &'a serde_json::Map, - name: &str, -) -> Result<&'a str, String> { - p.get(name) - .and_then(|v| v.as_str()) - .ok_or_else(|| format!("missing {}", name)) -} - -fn param(name: &str, param_type: &str, description: &str, required: bool) -> (String, ToolParam) { - ( - name.into(), - ToolParam { - name: name.into(), - param_type: param_type.into(), - description: Some(description.into()), - required, - properties: None, - items: None, - }, - ) -} diff --git a/libs/fctool/src/git_tools/merge.rs b/libs/fctool/src/git_tools/merge.rs deleted file mode 100644 index c387a05..0000000 --- a/libs/fctool/src/git_tools/merge.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Git merge analysis tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use std::collections::HashMap; - -async fn git_merge_analysis_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = required_str(&p, "project_name")?; - let repo_name = required_str(&p, "repo_name")?; - let target = required_str(&p, "target")?; - let base_ref = p.get("base_ref").and_then(|v| v.as_str()).unwrap_or("HEAD"); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let target_oid = resolve_rev(&domain, target)?; - let (analysis, preference) = if base_ref == "HEAD" { - domain - .merge_analysis(&target_oid) - .map_err(|e| e.to_string())? - } else { - domain - .merge_analysis_for_ref(base_ref, &target_oid) - .map_err(|e| e.to_string())? - }; - - let base_oid = domain.ref_target(base_ref).ok().flatten(); - let merge_base = base_oid - .as_ref() - .and_then(|base| domain.merge_base(base, &target_oid).ok()) - .map(|oid| oid.to_string()); - - Ok(serde_json::json!({ - "base_ref": base_ref, - "base_oid": base_oid.map(|oid| oid.to_string()), - "target": target, - "target_oid": target_oid.to_string(), - "merge_base": merge_base, - "analysis": analysis, - "preference": preference, - })) -} - -fn resolve_rev( - domain: &git::GitDomain, - rev: &str, -) -> Result { - if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) { - return Ok(git::commit::types::CommitOid::new(rev)); - } - if let Ok(Some(oid)) = domain.ref_target(rev) { - return Ok(oid); - } - domain - .commit_get_prefix(rev) - .map(|m| m.oid) - .map_err(|e| e.to_string()) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - registry.register( - ToolDefinition::new("git_merge_analysis") - .description("Analyze whether a target commit/ref can be merged into a base ref. Returns up-to-date, fast-forward, normal merge, unborn, merge preference, and merge-base information. This is read-only.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param("project_name", "string", "Project name (slug)", true), - param("repo_name", "string", "Repository name", true), - param("target", "string", "Target revision/ref to merge, e.g. refs/heads/feature, feature, or a commit SHA.", true), - param("base_ref", "string", "Base ref to merge into. Default HEAD.", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into(), "target".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_merge_analysis_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} - -fn required_str<'a>( - p: &'a serde_json::Map, - name: &str, -) -> Result<&'a str, String> { - p.get(name) - .and_then(|v| v.as_str()) - .ok_or_else(|| format!("missing {}", name)) -} - -fn param(name: &str, param_type: &str, description: &str, required: bool) -> (String, ToolParam) { - ( - name.into(), - ToolParam { - name: name.into(), - param_type: param_type.into(), - description: Some(description.into()), - required, - properties: None, - items: None, - }, - ) -} diff --git a/libs/fctool/src/git_tools/mod.rs b/libs/fctool/src/git_tools/mod.rs deleted file mode 100644 index b213f47..0000000 --- a/libs/fctool/src/git_tools/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! Git tools for AI agent function calling. -//! -//! Each module defines async exec functions + a `register_git_tools()` call. -//! All tools take `project_name` + `repo_name` as required params. - -pub mod blob; -pub mod branch; -pub mod commit; -pub mod ctx; -pub mod diff; -pub mod kb; -pub mod lfs; -pub mod merge; -pub mod reference; -pub mod repo_analysis; -pub mod repo_util; -pub mod status; -pub mod tag; -pub mod tree; -pub mod types; - -/// Batch-register all git tools into a ToolRegistry. -pub fn register_all(registry: &mut agent::ToolRegistry) { - commit::register_git_tools(registry); - status::register_git_tools(registry); - branch::register_git_tools(registry); - reference::register_git_tools(registry); - merge::register_git_tools(registry); - diff::register_git_tools(registry); - blob::register_git_tools(registry); - tree::register_git_tools(registry); - tag::register_git_tools(registry); - lfs::register_git_tools(registry); - repo_analysis::register_git_tools(registry); - kb::register_git_tools(registry); - repo_util::register_git_tools(registry); -} diff --git a/libs/fctool/src/git_tools/reference.rs b/libs/fctool/src/git_tools/reference.rs deleted file mode 100644 index d98e448..0000000 --- a/libs/fctool/src/git_tools/reference.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Git reference query tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use std::collections::HashMap; - -async fn git_ref_list_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p = parse_args(args)?; - let project_name = required_str(&p, "project_name")?; - let repo_name = required_str(&p, "repo_name")?; - let pattern = p.get("pattern").and_then(|v| v.as_str()); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let refs = domain.ref_list(pattern).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "count": refs.len(), - "refs": refs.into_iter().map(ref_json).collect::>(), - })) -} - -async fn git_ref_info_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p = parse_args(args)?; - let project_name = required_str(&p, "project_name")?; - let repo_name = required_str(&p, "repo_name")?; - let name = required_str(&p, "name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let info = domain.ref_get(name).map_err(|e| e.to_string())?; - - Ok(ref_json(info)) -} - -fn ref_json(info: git::reference::types::RefInfo) -> serde_json::Value { - serde_json::json!({ - "name": info.name, - "oid": info.oid.map(|oid| oid.to_string()), - "target": info.target.map(|oid| oid.to_string()), - "is_symbolic": info.is_symbolic, - "is_branch": info.is_branch, - "is_remote": info.is_remote, - "is_tag": info.is_tag, - "is_note": info.is_note, - }) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - registry.register( - ToolDefinition::new("git_ref_list") - .description("List Git references such as branches, remote-tracking refs, tags, and notes. Optional pattern supports exact names plus refs/heads/* or refs/tags/** style prefix matching.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param("project_name", "string", "Project name (slug)", true), - param("repo_name", "string", "Repository name", true), - param("pattern", "string", "Optional ref pattern, e.g. refs/heads/*, refs/tags/**, or refs/heads/main.", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_ref_list_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - registry.register( - ToolDefinition::new("git_ref_info") - .description( - "Get one Git reference by full name, including peeled commit OID and target OID.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param("project_name", "string", "Project name (slug)", true), - param("repo_name", "string", "Repository name", true), - param( - "name", - "string", - "Full ref name, e.g. refs/heads/main or refs/tags/v1.0.0.", - true, - ), - ])), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "name".into(), - ]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_ref_info_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} - -fn parse_args( - args: serde_json::Value, -) -> Result, String> { - serde_json::from_value(args).map_err(|e| e.to_string()) -} - -fn required_str<'a>( - p: &'a serde_json::Map, - name: &str, -) -> Result<&'a str, String> { - p.get(name) - .and_then(|v| v.as_str()) - .ok_or_else(|| format!("missing {}", name)) -} - -fn param(name: &str, param_type: &str, description: &str, required: bool) -> (String, ToolParam) { - ( - name.into(), - ToolParam { - name: name.into(), - param_type: param_type.into(), - description: Some(description.into()), - required, - properties: None, - items: None, - }, - ) -} diff --git a/libs/fctool/src/git_tools/repo_analysis.rs b/libs/fctool/src/git_tools/repo_analysis.rs deleted file mode 100644 index 917fc3f..0000000 --- a/libs/fctool/src/git_tools/repo_analysis.rs +++ /dev/null @@ -1,1054 +0,0 @@ -//! Repository analysis tools for AI. -//! -//! Provides function-calling tools that let AI quickly understand -//! repository structure, languages, dependencies, and overview. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use std::collections::HashMap; - -// ── Helpers ──────────────────────────────────────────────────────────────────── - -/// Recognised dependency manifest file names and their parser labels. -const DEPENDENCY_MANIFESTS: &[(&str, &str)] = &[ - ("Cargo.toml", "rust"), - ("package.json", "node"), - ("go.mod", "go"), - ("go.sum", "go"), - ("Gemfile", "ruby"), - ("requirements.txt", "python"), - ("Pipfile", "python"), - ("pyproject.toml", "python"), - ("pom.xml", "java"), - ("build.gradle", "java"), - ("build.gradle.kts", "java"), - ("composer.json", "php"), - ("CMakeLists.txt", "cmake"), - ("Makefile", "make"), -]; - -const TEST_FILE_MARKERS: &[&str] = &[ - "_test.", ".test.", ".spec.", "_spec.", "test.", "tests.", "test_", ".feature", -]; - -const TEST_DIR_MARKERS: &[&str] = &[ - "test", - "tests", - "__tests__", - "spec", - "specs", - "e2e", - "integration", - "unit", - "cypress", - "playwright", -]; - -/// Language detection by file extension (lowercase). -fn ext_to_language(ext: &str) -> Option<&'static str> { - match ext { - "rs" => Some("Rust"), - "go" => Some("Go"), - "py" => Some("Python"), - "js" => Some("JavaScript"), - "jsx" => Some("JSX"), - "ts" => Some("TypeScript"), - "tsx" => Some("TSX"), - "java" => Some("Java"), - "kt" | "kts" => Some("Kotlin"), - "rb" => Some("Ruby"), - "php" => Some("PHP"), - "c" => Some("C"), - "h" => Some("C/C++ Header"), - "cpp" | "cc" | "cxx" => Some("C++"), - "hpp" | "hh" => Some("C++ Header"), - "cs" => Some("C#"), - "swift" => Some("Swift"), - "scala" => Some("Scala"), - "zig" => Some("Zig"), - "sh" | "bash" | "zsh" => Some("Shell"), - "ps1" => Some("PowerShell"), - "sql" => Some("SQL"), - "html" | "htm" => Some("HTML"), - "css" | "scss" | "sass" | "less" => Some("CSS"), - "json" => Some("JSON"), - "yaml" | "yml" => Some("YAML"), - "toml" => Some("TOML"), - "md" => Some("Markdown"), - "dockerfile" | "containerfile" => Some("Dockerfile"), - "proto" => Some("Protobuf"), - "vue" => Some("Vue"), - "svelte" => Some("Svelte"), - "lua" => Some("Lua"), - "dart" => Some("Dart"), - "r" | "R" => Some("R"), - "clj" | "cljs" | "cljc" => Some("Clojure"), - "ex" | "exs" => Some("Elixir"), - "erl" => Some("Erlang"), - "hs" => Some("Haskell"), - _ => None, - } -} - -/// Directories that should be ignored in file-tree scans. -fn is_ignored_dir(name: &str) -> bool { - matches!( - name, - ".git" - | "node_modules" - | "target" - | "dist" - | "build" - | ".next" - | ".nuxt" - | ".output" - | ".cache" - | "__pycache__" - | ".tox" - | "vendor" - | ".bundle" - | ".gradle" - | "bin" - | "obj" - | ".svn" - | ".hg" - | ".idea" - | ".vscode" - | "coverage" - | ".terraform" - | ".serverless" - | "deps" - | "_build" - | "elm-stuff" - | ".stack-work" - | ".pytest_cache" - ) -} - -/// Recursively collect file extensions and counts from a git tree. -/// Skips ignored directories and binary-looking files. -fn collect_languages( - repo: &git2::Repository, - tree: &git2::Tree, - prefix: &str, - stats: &mut HashMap, - max_files: u64, -) { - let mut count = 0u64; - let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree.clone(), prefix.to_string())]; - while let Some((current_tree, path)) = stack.pop() { - for entry in current_tree.iter() { - if max_files > 0 && count >= max_files { - return; - } - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - let entry_path = if path.is_empty() { - name.to_string() - } else { - format!("{}/{}", path, name) - }; - match entry.kind() { - Some(git2::ObjectType::Tree) => { - if !is_ignored_dir(name) && !name.starts_with('.') { - if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) { - stack.push((subtree, entry_path)); - } - } - } - Some(git2::ObjectType::Blob) => { - count += 1; - if let Some(ext) = name.rsplit('.').next() { - let ext = ext.to_lowercase(); - if let Some(lang) = ext_to_language(&ext) { - let entry = stats.entry(lang.to_string()).or_insert_with(|| (ext, 0)); - entry.1 += 1; - } - } - } - _ => {} - } - } - } -} - -/// Collect a recursive file tree (path + kind) up to a given depth and file limit. -fn collect_file_tree( - repo: &git2::Repository, - tree: &git2::Tree, - prefix: &str, - depth: usize, - max_depth: usize, - max_files: u64, - files: &mut Vec, -) { - if depth > max_depth { - return; - } - for entry in tree.iter() { - if max_files > 0 && files.len() as u64 >= max_files { - return; - } - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - let entry_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - match entry.kind() { - Some(git2::ObjectType::Tree) => { - if !is_ignored_dir(name) && !name.starts_with('.') { - files.push(serde_json::json!({ - "path": entry_path, - "kind": "dir" - })); - if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) { - collect_file_tree( - repo, - &subtree, - &entry_path, - depth + 1, - max_depth, - max_files, - files, - ); - } - } - } - Some(git2::ObjectType::Blob) => { - files.push(serde_json::json!({ - "path": entry_path, - "kind": "file" - })); - } - _ => {} - } - } -} - -/// Detect config/manifest files in the root tree and return their names. -fn detect_config_files(tree: &git2::Tree) -> Vec { - let mut configs = Vec::new(); - let known_configs = [ - "Cargo.toml", - "package.json", - "go.mod", - "Gemfile", - "README.md", - "Dockerfile", - "docker-compose.yml", - "docker-compose.yaml", - ".github/workflows", - ".gitignore", - ".gitattributes", - "Makefile", - "CMakeLists.txt", - "composer.json", - "pyproject.toml", - "requirements.txt", - "Pipfile", - "pom.xml", - "build.gradle", - "build.gradle.kts", - "settings.gradle", - "settings.gradle.kts", - "tsconfig.json", - ".eslintrc.js", - ".eslintrc.json", - "prettier.config.js", - "prettierrc", - "webpack.config.js", - "vite.config.ts", - "vite.config.js", - "next.config.js", - "nuxt.config.ts", - "svelte.config.js", - "rust-toolchain", - "rust-toolchain.toml", - "clippy.toml", - ".rustfmt.toml", - "rustfmt.toml", - "renovate.json", - ".renovaterc", - ".mergify.yml", - "docker-bake.hcl", - ".dockerignore", - "Cargo.lock", - "yarn.lock", - "package-lock.json", - "pnpm-lock.yaml", - "Gemfile.lock", - "Cargo.lock", - ]; - for entry in tree.iter() { - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - if known_configs.contains(&name) || name.starts_with('.') && !name.starts_with(".git") { - configs.push(name.to_string()); - } - } - configs.sort(); - configs.dedup(); - configs -} - -/// Parse a dependency manifest file content and return a structured summary. -fn parse_dependencies(content: &str, manifest_name: &str) -> serde_json::Value { - match manifest_name { - "Cargo.toml" => { - // Simple TOML-ish parsing for [dependencies] section - let mut deps = Vec::new(); - let mut in_deps = false; - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("[dependencies]") { - in_deps = true; - continue; - } - if trimmed.starts_with('[') { - in_deps = false; - continue; - } - if in_deps { - if let Some(eq_pos) = trimmed.find('=') { - let name = trimmed[..eq_pos].trim().to_string(); - let version = trimmed[eq_pos + 1..] - .trim() - .trim_matches('"') - .trim_matches('\'') - .to_string(); - if !name.is_empty() && !name.starts_with('#') { - deps.push(serde_json::json!({ "name": name, "version": version })); - } - } else if !trimmed.is_empty() && !trimmed.starts_with('#') { - // bare dependency name (path/git dep without explicit version) - deps.push(serde_json::json!({ "name": trimmed, "version": null })); - } - } - } - serde_json::json!({ "manifest": "Cargo.toml", "ecosystem": "rust", "dependencies": deps }) - } - "package.json" => { - let mut deps = Vec::new(); - if let Ok(parsed) = serde_json::from_str::(content) { - for section in &["dependencies", "devDependencies", "peerDependencies"] { - if let Some(map) = parsed.get(*section).and_then(|v| v.as_object()) { - for (name, version) in map { - deps.push(serde_json::json!({ - "name": name, - "version": version.as_str().unwrap_or("*"), - "scope": section - })); - } - } - } - } - serde_json::json!({ "manifest": "package.json", "ecosystem": "node", "dependencies": deps }) - } - "go.mod" => { - let mut deps = Vec::new(); - let mut in_require = false; - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("require (") || trimmed == "require (" { - in_require = true; - continue; - } - if trimmed == ")" { - in_require = false; - continue; - } - if in_require { - let parts: Vec<&str> = trimmed.split_whitespace().collect(); - if parts.len() >= 2 { - deps.push(serde_json::json!({ "name": parts[0], "version": parts[1] })); - } - } - } - serde_json::json!({ "manifest": "go.mod", "ecosystem": "go", "dependencies": deps }) - } - "Gemfile" => { - let mut deps = Vec::new(); - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("gem ") { - let rest = trimmed.trim_start_matches("gem "); - let name = rest - .split(',') - .next() - .unwrap_or(rest) - .trim() - .trim_matches('"') - .trim_matches('\''); - let version = rest - .split(',') - .nth(1) - .map(|v| v.trim().trim_matches('"').trim_matches('\'')); - deps.push(serde_json::json!({ "name": name, "version": version })); - } - } - serde_json::json!({ "manifest": "Gemfile", "ecosystem": "ruby", "dependencies": deps }) - } - "requirements.txt" => { - let mut deps = Vec::new(); - for line in content.lines() { - let trimmed = line.trim(); - if !trimmed.is_empty() - && !trimmed.starts_with('#') - && !trimmed.starts_with("-r") - && !trimmed.starts_with("--") - { - if let Some(eq_eq) = trimmed.find("==") { - let name = trimmed[..eq_eq].trim().to_string(); - let version = trimmed[eq_eq + 2..].trim().to_string(); - deps.push(serde_json::json!({ "name": name, "version": version })); - } else { - deps.push(serde_json::json!({ "name": trimmed, "version": null })); - } - } - } - serde_json::json!({ "manifest": "requirements.txt", "ecosystem": "python", "dependencies": deps }) - } - _ => { - serde_json::json!({ "manifest": manifest_name, "ecosystem": "unknown", "dependencies": [] }) - } - } -} - -// ── Tool executors ───────────────────────────────────────────────────────────── - -/// Resolve HEAD to a tree for traversal. -fn head_tree(domain: &git::GitDomain) -> Result, String> { - let repo = domain.repo(); - let head = repo.head().map_err(|e| format!("no HEAD: {e}"))?; - head.peel_to_tree().map_err(|e| format!("no tree: {e}")) -} - -/// Resolve HEAD to a commit OID. -fn head_oid(domain: &git::GitDomain) -> Result { - let repo = domain.repo(); - let head = repo.head().map_err(|e| format!("no HEAD: {e}"))?; - head.target() - .map(|o| o.to_string()) - .ok_or_else(|| "HEAD has no target".to_string()) -} - -/// Tool: repo_overview — quick project overview -async fn repo_overview_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let tree = head_tree(&domain)?; - - // Default branch - let default_branch = repo - .head() - .ok() - .and_then(|h| h.shorthand().map(|s| s.to_string())) - .unwrap_or_else(|| "unknown".to_string()); - - // Config files in root - let config_files = detect_config_files(&tree); - - // Language stats (up to 5000 files) - let mut lang_stats: HashMap = HashMap::new(); - collect_languages(repo, &tree, "", &mut lang_stats, 5000); - let mut languages: Vec = lang_stats - .into_iter() - .map(|(lang, (_ext, count))| serde_json::json!({ "language": lang, "file_count": count })) - .collect(); - languages.sort_by(|a, b| { - b["file_count"] - .as_u64() - .unwrap_or(0) - .cmp(&a["file_count"].as_u64().unwrap_or(0)) - }); - - // Top-level file tree - let mut root_files: Vec = Vec::new(); - collect_file_tree(repo, &tree, "", 0, 1, 100, &mut root_files); - - // Recent commits (last 10) - let head_oid = head_oid(&domain)?; - let recent_commits = domain - .commit_log(Some(&head_oid), 0, 10) - .map_err(|e| e.to_string())?; - let commits: Vec = recent_commits - .iter() - .map(|c| { - serde_json::json!({ - "oid": c.oid.to_string(), - "summary": c.summary, - "author": c.author.name, - "time": c.author.time_secs, - }) - }) - .collect(); - - // Total commit count - let total_commits = domain.commit_total(Some(&head_oid)).unwrap_or(0); - - Ok(serde_json::json!({ - "default_branch": default_branch, - "head_oid": head_oid, - "total_commits": total_commits, - "config_files": config_files, - "languages": languages, - "top_level_entries": root_files, - "recent_commits": commits, - })) -} - -/// Tool: repo_file_tree — recursive file tree with depth/ignore -async fn repo_file_tree_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let max_depth = p.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(3) as usize; - let max_files = p.get("max_files").and_then(|v| v.as_u64()).unwrap_or(200); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let tree = head_tree(&domain)?; - - let mut files = Vec::new(); - collect_file_tree(repo, &tree, "", 0, max_depth, max_files, &mut files); - - Ok(serde_json::json!({ - "total": files.len(), - "entries": files - })) -} - -/// Tool: repo_languages — detailed language breakdown -async fn repo_languages_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let tree = head_tree(&domain)?; - - let mut lang_stats: HashMap = HashMap::new(); - collect_languages(repo, &tree, "", &mut lang_stats, 100_000); - - let mut languages: Vec = lang_stats - .into_iter() - .map(|(lang, (_ext, count))| serde_json::json!({ "language": lang, "file_count": count })) - .collect(); - languages.sort_by(|a, b| { - b["file_count"] - .as_u64() - .unwrap_or(0) - .cmp(&a["file_count"].as_u64().unwrap_or(0)) - }); - - Ok(serde_json::json!({ - "total_languages": languages.len(), - "languages": languages - })) -} - -/// Tool: repo_dependencies — parse dependency manifests -async fn repo_dependencies_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let tree = head_tree(&domain)?; - - // Walk the tree looking for dependency manifests at any depth - let mut manifests: Vec = Vec::new(); - let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree, String::new())]; - let repo = domain.repo(); - - while let Some((current_tree, prefix)) = stack.pop() { - for entry in current_tree.iter() { - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - let entry_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - match entry.kind() { - Some(git2::ObjectType::Tree) => { - if !is_ignored_dir(name) && !name.starts_with('.') { - if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) { - stack.push((subtree, entry_path)); - } - } - } - Some(git2::ObjectType::Blob) => { - if DEPENDENCY_MANIFESTS.iter().any(|(fname, _)| *fname == name) { - if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) { - let content = String::from_utf8_lossy(blob.content()); - let manifest_type = DEPENDENCY_MANIFESTS - .iter() - .find(|(fname, _)| *fname == name) - .map(|(_, eco)| eco) - .unwrap_or(&"unknown"); - - let parsed = parse_dependencies(&content, name); - manifests.push(serde_json::json!({ - "path": entry_path, - "ecosystem": manifest_type, - "details": parsed - })); - } - } - } - _ => {} - } - } - } - - Ok(serde_json::json!({ - "manifest_count": manifests.len(), - "manifests": manifests - })) -} - -// ── Registration ─────────────────────────────────────────────────────────────── - -/// Tool: repo_test_discovery - discover likely tests and test commands. -async fn repo_test_discovery_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let max_files = p.get("max_files").and_then(|v| v.as_u64()).unwrap_or(200) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let tree = head_tree(&domain)?; - let repo = domain.repo(); - let mut test_files = Vec::new(); - let mut manifests = Vec::new(); - let mut frameworks: HashMap = HashMap::new(); - let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree, String::new())]; - - while let Some((current_tree, prefix)) = stack.pop() { - for entry in current_tree.iter() { - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - let entry_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - match entry.kind() { - Some(git2::ObjectType::Tree) => { - if !is_ignored_dir(name) { - if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) { - stack.push((subtree, entry_path)); - } - } - } - Some(git2::ObjectType::Blob) => { - let lower_path = entry_path.to_lowercase(); - let lower_name = name.to_lowercase(); - if is_test_file(&lower_path, &lower_name) && test_files.len() < max_files { - test_files.push(serde_json::json!({ - "path": entry_path, - "kind": test_kind(&lower_path), - })); - } - if is_test_manifest(name) { - if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) { - let content = String::from_utf8_lossy(blob.content()); - for framework in detect_test_frameworks(name, &content) { - *frameworks.entry(framework).or_insert(0) += 1; - } - manifests.push(serde_json::json!({ "path": entry_path, "name": name })); - } - } - } - _ => {} - } - } - } - - let mut framework_list: Vec<_> = frameworks - .into_iter() - .map(|(name, evidence_count)| { - serde_json::json!({ "name": name, "evidence_count": evidence_count }) - }) - .collect(); - framework_list.sort_by(|a, b| { - b["evidence_count"] - .as_u64() - .unwrap_or(0) - .cmp(&a["evidence_count"].as_u64().unwrap_or(0)) - }); - - let commands = infer_test_commands(&framework_list, &manifests); - - Ok(serde_json::json!({ - "test_file_count_returned": test_files.len(), - "test_files": test_files, - "test_manifests": manifests, - "frameworks": framework_list, - "suggested_commands": commands, - })) -} - -fn is_test_file(lower_path: &str, lower_name: &str) -> bool { - TEST_FILE_MARKERS - .iter() - .any(|marker| lower_name.contains(marker)) - || lower_path - .split('/') - .any(|part| TEST_DIR_MARKERS.iter().any(|marker| part == *marker)) -} - -fn test_kind(lower_path: &str) -> &'static str { - if lower_path.contains("/e2e/") - || lower_path.contains("/cypress/") - || lower_path.contains("/playwright/") - { - "e2e" - } else if lower_path.contains("/integration/") { - "integration" - } else if lower_path.contains("/unit/") { - "unit" - } else { - "test" - } -} - -fn is_test_manifest(name: &str) -> bool { - matches!( - name, - "Cargo.toml" - | "package.json" - | "go.mod" - | "pyproject.toml" - | "requirements.txt" - | "pom.xml" - | "build.gradle" - | "build.gradle.kts" - | "pytest.ini" - | "tox.ini" - | "vitest.config.ts" - | "vitest.config.js" - | "jest.config.js" - | "jest.config.ts" - | "playwright.config.ts" - | "playwright.config.js" - | "cypress.config.ts" - | "cypress.config.js" - ) -} - -fn detect_test_frameworks(name: &str, content: &str) -> Vec { - let lower = content.to_lowercase(); - let mut found = Vec::new(); - let mut add = |framework: &str| { - if !found.iter().any(|v| v == framework) { - found.push(framework.to_string()); - } - }; - - match name { - "Cargo.toml" => { - if lower.contains("[dev-dependencies]") - || lower.contains("tokio-test") - || lower.contains("rstest") - { - add("cargo test"); - } - } - "package.json" => { - if lower.contains("\"test\"") { - add("npm test"); - } - if lower.contains("vitest") { - add("vitest"); - } - if lower.contains("jest") { - add("jest"); - } - if lower.contains("playwright") { - add("playwright"); - } - if lower.contains("cypress") { - add("cypress"); - } - } - "go.mod" => add("go test"), - "pyproject.toml" | "requirements.txt" | "pytest.ini" | "tox.ini" => { - if lower.contains("pytest") { - add("pytest"); - } - if lower.contains("tox") { - add("tox"); - } - } - "pom.xml" => add("maven test"), - "build.gradle" | "build.gradle.kts" => add("gradle test"), - _ => { - if name.starts_with("vitest.config") { - add("vitest"); - } else if name.starts_with("jest.config") { - add("jest"); - } else if name.starts_with("playwright.config") { - add("playwright"); - } else if name.starts_with("cypress.config") { - add("cypress"); - } - } - } - - found -} - -fn infer_test_commands( - frameworks: &[serde_json::Value], - manifests: &[serde_json::Value], -) -> Vec { - let names: Vec = frameworks - .iter() - .filter_map(|v| v.get("name").and_then(|n| n.as_str()).map(str::to_string)) - .collect(); - let manifest_names: Vec = manifests - .iter() - .filter_map(|v| v.get("name").and_then(|n| n.as_str()).map(str::to_string)) - .collect(); - let mut commands = Vec::new(); - let mut add = |command: &str, reason: &str| { - if !commands - .iter() - .any(|v: &serde_json::Value| v.get("command").and_then(|c| c.as_str()) == Some(command)) - { - commands.push(serde_json::json!({ "command": command, "reason": reason })); - } - }; - - if names.iter().any(|n| n == "cargo test") || manifest_names.iter().any(|n| n == "Cargo.toml") { - add("cargo test", "Rust Cargo manifest detected"); - } - if names.iter().any(|n| n == "npm test") || manifest_names.iter().any(|n| n == "package.json") { - add("npm test", "Node package.json detected"); - } - if names.iter().any(|n| n == "vitest") { - add("npx vitest run", "Vitest detected"); - } - if names.iter().any(|n| n == "jest") { - add("npx jest", "Jest detected"); - } - if names.iter().any(|n| n == "playwright") { - add("npx playwright test", "Playwright detected"); - } - if names.iter().any(|n| n == "cypress") { - add("npx cypress run", "Cypress detected"); - } - if names.iter().any(|n| n == "go test") || manifest_names.iter().any(|n| n == "go.mod") { - add("go test ./...", "Go module detected"); - } - if names.iter().any(|n| n == "pytest") { - add("pytest", "Pytest detected"); - } - if names.iter().any(|n| n == "tox") { - add("tox", "Tox detected"); - } - if names.iter().any(|n| n == "maven test") || manifest_names.iter().any(|n| n == "pom.xml") { - add("mvn test", "Maven project detected"); - } - if names.iter().any(|n| n == "gradle test") - || manifest_names - .iter() - .any(|n| n == "build.gradle" || n == "build.gradle.kts") - { - add("./gradlew test", "Gradle project detected"); - } - - commands -} - -macro_rules! param { - ($name:expr, $type:expr, $desc:expr, $required:expr) => { - ( - $name.into(), - ToolParam { - name: $name.into(), - param_type: $type.into(), - description: Some($desc.into()), - required: $required, - properties: None, - items: None, - }, - ) - }; -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - // repo_overview - registry.register( - ToolDefinition::new("repo_overview") - .description("Get a quick overview of a repository: default branch, detected config files, language breakdown by file count, top-level directory entries, and recent commits. Ideal for first contact with a repo.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_overview_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_file_tree - registry.register( - ToolDefinition::new("repo_file_tree") - .description("List files and directories in a repository recursively with configurable depth. Ignores common generated/artifact directories (node_modules, target, .git, etc.). Useful for understanding project layout.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - param!("max_depth", "integer", "Maximum directory depth to traverse (default: 3)", false), - param!("max_files", "integer", "Maximum number of entries to return (default: 200)", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_file_tree_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_languages - registry.register( - ToolDefinition::new("repo_languages") - .description("Get a detailed breakdown of programming languages used in a repository, sorted by file count. Scans all files in the repo (up to 100K files) and maps extensions to language names.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_languages_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_dependencies - registry.register( - ToolDefinition::new("repo_dependencies") - .description("Discover and parse dependency manifests (Cargo.toml, package.json, go.mod, Gemfile, requirements.txt, etc.) in a repository. Returns structured dependency lists per manifest.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_dependencies_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_test_discovery - registry.register( - ToolDefinition::new("repo_test_discovery") - .description("Discover likely test files, test frameworks, and suggested test commands from repository manifests and file layout. Useful before changing code or planning validation.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - param!("max_files", "integer", "Maximum test file entries to return (default: 200)", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_test_discovery_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/repo_util.rs b/libs/fctool/src/git_tools/repo_util.rs deleted file mode 100644 index f4b8b9d..0000000 --- a/libs/fctool/src/git_tools/repo_util.rs +++ /dev/null @@ -1,620 +0,0 @@ -//! General-purpose repository utility tools for AI. -//! -//! Code search, README reading, commit history, contributors, and diff summaries. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use std::collections::HashMap; - -// ── Helpers ──────────────────────────────────────────────────────────────────── - -fn head_tree(domain: &git::GitDomain) -> Result, String> { - let repo = domain.repo(); - let head = repo.head().map_err(|e| format!("no HEAD: {e}"))?; - head.peel_to_tree().map_err(|e| format!("no tree: {e}")) -} - -fn head_oid(domain: &git::GitDomain) -> Result { - let repo = domain.repo(); - let head = repo.head().map_err(|e| format!("no HEAD: {e}"))?; - head.target() - .map(|o| o.to_string()) - .ok_or_else(|| "HEAD has no target".to_string()) -} - -fn is_ignored_dir(name: &str) -> bool { - matches!( - name, - ".git" - | "node_modules" - | "target" - | "dist" - | "build" - | ".next" - | ".nuxt" - | ".output" - | ".cache" - | "__pycache__" - | ".tox" - | "vendor" - | ".bundle" - | ".gradle" - | "bin" - | "obj" - | ".svn" - | ".hg" - | ".idea" - | ".vscode" - | "coverage" - | ".terraform" - | ".serverless" - | "deps" - | "_build" - | "elm-stuff" - | ".stack-work" - | ".pytest_cache" - ) -} - -fn is_binary_ext(name: &str) -> bool { - match name.rsplit('.').next().unwrap_or("") { - "png" | "jpg" | "jpeg" | "gif" | "webp" | "ico" | "svg" | "bmp" | "mp3" | "mp4" | "wav" - | "avi" | "mov" | "mkv" | "webm" | "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" - | "exe" | "dll" | "so" | "dylib" | "o" | "a" | "lib" | "woff" | "woff2" | "ttf" | "otf" - | "eot" | "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "sqlite" | "db" - | "bin" | "dat" | "pyc" | "class" | "wasm" | "node" => true, - _ => false, - } -} - -/// Resolve a rev string to a commit OID. -fn resolve_commit_oid( - domain: &git::GitDomain, - rev: &str, -) -> Result { - domain.resolve_rev(rev).map_err(|e| e.to_string()) -} - -// ── Tool executors ───────────────────────────────────────────────────────────── - -/// Tool: repo_search — search code content across the repo -async fn repo_search_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let keyword = p - .get("keyword") - .and_then(|v| v.as_str()) - .ok_or("missing keyword")?; - let context_lines = p.get("context_lines").and_then(|v| v.as_u64()).unwrap_or(2) as usize; - let max_results = p.get("max_results").and_then(|v| v.as_u64()).unwrap_or(50) as usize; - - let keyword_lower = keyword.to_lowercase(); - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let tree = head_tree(&domain)?; - - let mut matches: Vec = Vec::new(); - let mut matched_files = 0usize; - let mut total_hits = 0usize; - let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree, String::new())]; - - 'outer: while let Some((current_tree, prefix)) = stack.pop() { - for entry in current_tree.iter() { - if matches.len() >= max_results { - break 'outer; - } - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - let entry_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - match entry.kind() { - Some(git2::ObjectType::Tree) => { - if !is_ignored_dir(name) && !name.starts_with('.') { - if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) { - stack.push((subtree, entry_path)); - } - } - } - Some(git2::ObjectType::Blob) => { - if is_binary_ext(name) { - continue; - } - if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) { - if blob.is_binary() || blob.size() > 1024 * 1024 { - // Skip binary or files larger than 1MB to prevent OOM - continue; - } - - let content_bytes = blob.content(); - let keyword_bytes = keyword_lower.as_bytes(); - - // Check if file contains the keyword before doing expensive line splitting - let mut has_keyword = false; - for window in content_bytes.windows(keyword_bytes.len()) { - if window.eq_ignore_ascii_case(keyword_bytes) { - has_keyword = true; - break; - } - } - - if !has_keyword { - continue; - } - - let content = String::from_utf8_lossy(content_bytes); - let lines: Vec<&str> = content.lines().collect(); - let mut hit_lines = Vec::new(); - - for (i, line) in lines.iter().enumerate() { - if line.to_lowercase().contains(&keyword_lower) { - hit_lines.push(i); - } - } - - if !hit_lines.is_empty() { - matched_files += 1; - total_hits += hit_lines.len(); - - // Merge overlapping context windows - let mut windows: Vec<(usize, usize)> = Vec::new(); - for &line_idx in &hit_lines { - let start = line_idx.saturating_sub(context_lines); - let end = (line_idx + context_lines + 1).min(lines.len()); - if let Some(last) = windows.last_mut() { - if start <= last.1 { - last.1 = end; - continue; - } - } - windows.push((start, end)); - } - - let snippets: Vec = windows - .iter() - .map(|(start, end)| { - let snippet: Vec = - lines[*start..*end].iter().map(|l| l.to_string()).collect(); - serde_json::json!({ - "line_start": start + 1, - "line_end": end, - "snippet": snippet.join("\n"), - }) - }) - .collect(); - - matches.push(serde_json::json!({ - "path": entry_path, - "hit_count": hit_lines.len(), - "snippets": snippets, - })); - } - } - } - _ => {} - } - } - } - - Ok(serde_json::json!({ - "keyword": keyword, - "matched_files": matched_files, - "total_hits": total_hits, - "results": matches, - })) -} - -/// Tool: repo_readme — get README content -async fn repo_readme_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let tree = head_tree(&domain)?; - - // Try common README filenames - let candidates = [ - "README.md", - "README.MD", - "README.markdown", - "README.rst", - "README.txt", - "README", - ]; - let mut found = None; - - for candidate in &candidates { - if let Ok(entry) = tree.get_path(std::path::Path::new(candidate)) { - if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) { - let content = String::from_utf8_lossy(blob.content()).to_string(); - found = Some((candidate.to_string(), content)); - break; - } - } - } - - match found { - Some((filename, content)) => Ok(serde_json::json!({ - "filename": filename, - "content": content, - "size": content.len(), - })), - None => Ok(serde_json::json!({ - "filename": null, - "content": null, - "error": "No README file found in repository root", - })), - } -} - -/// Tool: repo_commit_log — filtered commit history -async fn repo_commit_log_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let author = p.get("author").and_then(|v| v.as_str()); - let keyword = p.get("keyword").and_then(|v| v.as_str()); - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let head_oid = head_oid(&domain)?; - - // Fetch extra to allow for filtering - let fetch_limit = if author.is_some() || keyword.is_some() { - limit.saturating_mul(5).max(200) - } else { - limit - }; - - let commits = domain - .commit_log(Some(&head_oid), 0, fetch_limit) - .map_err(|e| e.to_string())?; - - let keyword_lower = keyword.map(|k| k.to_lowercase()); - let author_lower = author.map(|a| a.to_lowercase()); - - let result: Vec = commits - .iter() - .filter(|c| { - if let Some(ref al) = author_lower { - if !c.author.name.to_lowercase().contains(al) { - return false; - } - } - if let Some(ref kl) = keyword_lower { - if !c.message.to_lowercase().contains(kl) { - return false; - } - } - true - }) - .take(limit) - .map(|c| { - let oid = c.oid.to_string(); - serde_json::json!({ - "oid": oid, - "short_oid": oid.get(..7).unwrap_or(&oid), - "summary": c.summary, - "author": c.author.name, - "author_email": c.author.email, - "time": c.author.time_secs, - }) - }) - .collect(); - - Ok(serde_json::json!({ - "total": result.len(), - "commits": result, - })) -} - -/// Tool: repo_contributors — contributor statistics -async fn repo_contributors_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(0) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let head_oid = head_oid(&domain)?; - - // Walk all commits (up to 10000) - let commits = domain - .commit_log(Some(&head_oid), 0, 10000) - .map_err(|e| e.to_string())?; - - // Aggregate by author email (more reliable than name) - let mut authors: HashMap = HashMap::new(); - for c in &commits { - let key = c.author.email.clone(); - let entry = authors.entry(key).or_insert_with(|| { - serde_json::json!({ - "name": c.author.name, - "email": c.author.email, - "commit_count": 0u64, - "first_commit_time": c.author.time_secs, - "last_commit_time": c.author.time_secs, - }) - }); - entry["commit_count"] = serde_json::json!(entry["commit_count"].as_u64().unwrap_or(0) + 1); - let t = c.author.time_secs; - if t < entry["first_commit_time"].as_i64().unwrap_or(i64::MAX) { - entry["first_commit_time"] = serde_json::json!(t); - } - if t > entry["last_commit_time"].as_i64().unwrap_or(0) { - entry["last_commit_time"] = serde_json::json!(t); - } - } - - let mut contributors: Vec = authors.into_values().collect(); - contributors.sort_by(|a, b| { - b["commit_count"] - .as_u64() - .unwrap_or(0) - .cmp(&a["commit_count"].as_u64().unwrap_or(0)) - }); - - if limit > 0 { - contributors.truncate(limit); - } - - Ok(serde_json::json!({ - "total_contributors": contributors.len(), - "contributors": contributors, - })) -} - -/// Tool: repo_diff_summary — change summary between two revisions -async fn repo_diff_summary_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let from_rev = p - .get("from_rev") - .and_then(|v| v.as_str()) - .ok_or("missing from_rev")?; - let to_rev = p.get("to_rev").and_then(|v| v.as_str()).unwrap_or("HEAD"); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - - let from_oid = resolve_commit_oid(&domain, from_rev)?; - let to_oid = resolve_commit_oid(&domain, to_rev)?; - - let from_commit = repo - .find_commit(from_oid.to_oid().map_err(|e| e.to_string())?) - .map_err(|e| format!("from_rev not found: {e}"))?; - let to_commit = repo - .find_commit(to_oid.to_oid().map_err(|e| e.to_string())?) - .map_err(|e| format!("to_rev not found: {e}"))?; - - let from_tree = from_commit.tree().map_err(|e| e.to_string())?; - let to_tree = to_commit.tree().map_err(|e| e.to_string())?; - - let diff = repo - .diff_tree_to_tree(Some(&from_tree), Some(&to_tree), None) - .map_err(|e| e.to_string())?; - - let stats = diff.stats().map_err(|e| e.to_string())?; - let files_changed = stats.files_changed(); - let insertions = stats.insertions(); - let deletions = stats.deletions(); - - // Collect per-file stats - let mut files: Vec = Vec::new(); - for i in 0..diff.deltas().len() { - let delta = diff.deltas().nth(i); - if let Some(d) = delta { - let old_path = d.old_file().path().map(|p| p.to_string_lossy().to_string()); - let new_path = d.new_file().path().map(|p| p.to_string_lossy().to_string()); - let status = match d.status() { - git2::Delta::Added => "added", - git2::Delta::Deleted => "deleted", - git2::Delta::Modified => "modified", - git2::Delta::Renamed => "renamed", - git2::Delta::Copied => "copied", - _ => "other", - }; - files.push(serde_json::json!({ - "old_path": old_path, - "new_path": new_path, - "status": status, - })); - } - } - - Ok(serde_json::json!({ - "from": from_rev, - "to": to_rev, - "files_changed": files_changed, - "insertions": insertions, - "deletions": deletions, - "files": files, - })) -} - -// ── Registration ─────────────────────────────────────────────────────────────── - -macro_rules! param { - ($name:expr, $type:expr, $desc:expr, $required:expr) => { - ( - $name.into(), - ToolParam { - name: $name.into(), - param_type: $type.into(), - description: Some($desc.into()), - required: $required, - properties: None, - items: None, - }, - ) - }; -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - // repo_search - registry.register( - ToolDefinition::new("repo_search") - .description("Search all files in a repository for a keyword (case-insensitive). Returns matching file paths, hit counts, and context snippets. Skips binary files and generated directories. Use this to find where a function, variable, or concept is defined or used.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - param!("keyword", "string", "Search keyword (case-insensitive)", true), - param!("context_lines", "integer", "Number of context lines around each match (default: 2)", false), - param!("max_results", "integer", "Maximum number of matching files to return (default: 50)", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into(), "keyword".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_search_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_readme - registry.register( - ToolDefinition::new("repo_readme") - .description("Read the README file from a repository. Automatically finds README.md, README.markdown, README.rst, README.txt, or README. Returns the full content. Use this as the first step to understand what a project is about.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_readme_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_commit_log - registry.register( - ToolDefinition::new("repo_commit_log") - .description("Get commit history with optional filters. Filter by author name (partial match), keyword in commit message, or limit the number of results. Use this to understand recent activity, find who made specific changes, or trace feature development.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - param!("author", "string", "Filter by author name (partial match, case-insensitive)", false), - param!("keyword", "string", "Filter by keyword in commit message (case-insensitive)", false), - param!("limit", "integer", "Maximum number of commits to return (default: 20)", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_commit_log_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_contributors - registry.register( - ToolDefinition::new("repo_contributors") - .description("List repository contributors sorted by commit count. Shows each contributor's name, email, commit count, and first/last commit timestamps. Use this to understand who is involved in a project and their contribution levels.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - param!("limit", "integer", "Maximum number of contributors to return (0 = all, default: 0)", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_contributors_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // repo_diff_summary - registry.register( - ToolDefinition::new("repo_diff_summary") - .description("Get a summary of changes between two revisions (commits, branches, or tags). Shows files changed, insertions, deletions, and per-file status (added/modified/deleted/renamed). Use this to understand what changed between versions.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param!("project_name", "string", "Project name (slug)", true), - param!("repo_name", "string", "Repository name", true), - param!("from_rev", "string", "Source revision (commit SHA, branch name, or tag)", true), - param!("to_rev", "string", "Target revision (default: HEAD)", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into(), "from_rev".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - repo_diff_summary_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/status.rs b/libs/fctool/src/git_tools/status.rs deleted file mode 100644 index 0329c8e..0000000 --- a/libs/fctool/src/git_tools/status.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Git status tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use std::collections::HashMap; - -async fn git_status_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let include_ignored = p - .get("include_ignored") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let repo = domain.repo(); - let is_bare = repo.is_bare(); - let head = repo - .head() - .ok() - .and_then(|h| h.shorthand().map(str::to_string)); - - if is_bare { - return Ok(serde_json::json!({ - "is_bare": true, - "head": head, - "is_dirty": false, - "files": [], - "summary": { - "total": 0, - "index": 0, - "worktree": 0, - "untracked": 0, - "ignored": 0, - "conflicted": 0, - } - })); - } - - let mut opts = git2::StatusOptions::new(); - opts.include_untracked(true) - .renames_head_to_index(true) - .renames_index_to_workdir(true) - .recurse_untracked_dirs(true); - if include_ignored { - opts.include_ignored(true); - } - - let statuses = repo - .statuses(Some(&mut opts)) - .map_err(|e| format!("git status failed: {}", e))?; - - let mut files = Vec::new(); - let mut index = 0usize; - let mut worktree = 0usize; - let mut untracked = 0usize; - let mut ignored = 0usize; - let mut conflicted = 0usize; - - for entry in statuses.iter() { - let status = entry.status(); - let path = entry - .head_to_index() - .and_then(|d| d.new_file().path()) - .or_else(|| entry.index_to_workdir().and_then(|d| d.new_file().path())) - .or_else(|| entry.path().map(std::path::Path::new)) - .map(|p| p.to_string_lossy().replace('\\', "/")) - .unwrap_or_default(); - - let index_status = index_status_label(status); - let worktree_status = worktree_status_label(status); - let is_untracked = status.contains(git2::Status::WT_NEW); - let is_ignored = status.contains(git2::Status::IGNORED); - let is_conflicted = status.is_conflicted(); - - if index_status.is_some() { - index += 1; - } - if worktree_status.is_some() { - worktree += 1; - } - if is_untracked { - untracked += 1; - } - if is_ignored { - ignored += 1; - } - if is_conflicted { - conflicted += 1; - } - - files.push(serde_json::json!({ - "path": path, - "index_status": index_status, - "worktree_status": worktree_status, - "is_untracked": is_untracked, - "is_ignored": is_ignored, - "is_conflicted": is_conflicted, - })); - } - - Ok(serde_json::json!({ - "is_bare": false, - "head": head, - "is_dirty": !files.is_empty(), - "summary": { - "total": files.len(), - "index": index, - "worktree": worktree, - "untracked": untracked, - "ignored": ignored, - "conflicted": conflicted, - }, - "files": files, - })) -} - -fn index_status_label(status: git2::Status) -> Option<&'static str> { - if status.contains(git2::Status::INDEX_NEW) { - Some("added") - } else if status.contains(git2::Status::INDEX_MODIFIED) { - Some("modified") - } else if status.contains(git2::Status::INDEX_DELETED) { - Some("deleted") - } else if status.contains(git2::Status::INDEX_RENAMED) { - Some("renamed") - } else if status.contains(git2::Status::INDEX_TYPECHANGE) { - Some("typechange") - } else { - None - } -} - -fn worktree_status_label(status: git2::Status) -> Option<&'static str> { - if status.contains(git2::Status::WT_NEW) { - Some("untracked") - } else if status.contains(git2::Status::WT_MODIFIED) { - Some("modified") - } else if status.contains(git2::Status::WT_DELETED) { - Some("deleted") - } else if status.contains(git2::Status::WT_RENAMED) { - Some("renamed") - } else if status.contains(git2::Status::WT_TYPECHANGE) { - Some("typechange") - } else if status.contains(git2::Status::IGNORED) { - Some("ignored") - } else { - None - } -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - registry.register( - ToolDefinition::new("git_status") - .description("Show repository working tree status: staged, unstaged, untracked, ignored, and conflicted files. Bare repositories return an empty clean status.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(HashMap::from([ - param("project_name", "string", "Project name (slug)", true), - param("repo_name", "string", "Repository name", true), - param("include_ignored", "boolean", "Include ignored files. Default false.", false), - ])), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_status_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} - -fn param(name: &str, param_type: &str, description: &str, required: bool) -> (String, ToolParam) { - ( - name.into(), - ToolParam { - name: name.into(), - param_type: param_type.into(), - description: Some(description.into()), - required, - properties: None, - items: None, - }, - ) -} diff --git a/libs/fctool/src/git_tools/tag.rs b/libs/fctool/src/git_tools/tag.rs deleted file mode 100644 index 8c17222..0000000 --- a/libs/fctool/src/git_tools/tag.rs +++ /dev/null @@ -1,329 +0,0 @@ -//! Git tag tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; -use std::collections::HashMap; - -async fn git_tag_list_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let pattern = p - .get("pattern") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let all_tags = domain.tag_list().map_err(|e| e.to_string())?; - - let result: Vec<_> = match pattern { - Some(ref pat) => { - let pat_lower = pat.to_lowercase(); - // Convert glob pattern (only * wildcards) to regex for proper matching. - // "*" matches any sequence of characters. - let regex_pat = pat_lower - .split('*') - .map(|s| regex::escape(s)) - .collect::>() - .join(".*"); - let re = regex::Regex::new(&format!("^{}$", regex_pat)).ok(); - all_tags - .iter() - .filter(|t| { - let n = t.name.to_lowercase(); - re.as_ref().map(|r| r.is_match(&n)).unwrap_or(false) - }) - .map(|t| tag_to_json(t)) - .collect() - } - None => all_tags.into_iter().map(|t| tag_to_json(&t)).collect(), - }; - - Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) -} - -fn tag_to_json(t: &git::tags::types::TagInfo) -> serde_json::Value { - serde_json::json!({ - "name": t.name, - "oid": t.oid.to_string(), - "target": t.target.to_string(), - "is_annotated": t.is_annotated, - "message": t.message.clone(), - "tagger_name": t.tagger.clone(), - "tagger_email": t.tagger_email.clone() - }) -} - -async fn git_tag_info_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let name = p - .get("name") - .and_then(|v| v.as_str()) - .ok_or("missing name")?; - - let domain = ctx.open_repo(project_name, repo_name).await?; - let info = domain.tag_get(name).map_err(|e| e.to_string())?; - - Ok(tag_to_json(&info)) -} - -async fn git_tag_search_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let query = p - .get("query") - .and_then(|v| v.as_str()) - .ok_or("missing query")?; - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(5) as usize; - - // Resolve project_id from project_name for project isolation - let db = ctx.ctx.db(); - let project = models::projects::project::Entity::find() - .filter(models::projects::project::Column::Name.eq(project_name)) - .one(db) - .await - .map_err(|e| format!("DB error looking up project '{}': {}", project_name, e))? - .ok_or_else(|| format!("project '{}' not found", project_name))?; - let project_id = project.id.to_string(); - - // Get embed_service from context - let embed = ctx.ctx.embed_service().ok_or_else(|| { - "EmbedService not available — Qdrant vector search is disabled".to_string() - })?; - - let results = embed - .search_tags(query, &project_id, limit) - .await - .map_err(|e| format!("tag search failed: {}", e))?; - - let json_results: Vec = results - .into_iter() - .map(|r| { - let repo_name = r - .payload - .extra - .as_ref() - .and_then(|e| e.get("repo_name")) - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - let tag_name = r - .payload - .extra - .as_ref() - .and_then(|e| e.get("tag_name")) - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - let description = r - .payload - .extra - .as_ref() - .and_then(|e| e.get("description")) - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - serde_json::json!({ - "tag_name": tag_name, - "repo_name": repo_name, - "description": description, - "score": r.score, - }) - }) - .collect(); - - Ok(serde_json::to_value(json_results).map_err(|e| e.to_string())?) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - // git_tag_list - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "pattern".into(), - ToolParam { - name: "pattern".into(), - param_type: "string".into(), - description: Some("Filter tags by name pattern (supports * wildcard)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }; - registry.register( - ToolDefinition::new("git_tag_list") - .description("List all tags in a repository, optionally filtered by a name pattern.") - .parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_tag_list_exec(gctx, args) - .await - .map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_tag_info - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "name".into(), - ToolParam { - name: "name".into(), - param_type: "string".into(), - description: Some("Tag name to look up".into()), - required: true, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "name".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_tag_info").description("Get detailed information about a specific tag including target commit, annotation, and tagger.").parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_tag_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_tag_search - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "query".into(), - ToolParam { - name: "query".into(), - param_type: "string".into(), - description: Some("Semantic search query to find relevant tags".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "limit".into(), - ToolParam { - name: "limit".into(), - param_type: "integer".into(), - description: Some("Maximum number of results (default 5)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["project_name".into(), "query".into()]), - }; - registry.register( - ToolDefinition::new("git_tag_search").description("Semantically search tags across repositories in a project. Uses vector search to find tags relevant to the query, with project-level isolation.").parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_tag_search_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/tree.rs b/libs/fctool/src/git_tools/tree.rs deleted file mode 100644 index 115d899..0000000 --- a/libs/fctool/src/git_tools/tree.rs +++ /dev/null @@ -1,504 +0,0 @@ -//! Git tree and file tools. - -use super::ctx::GitToolCtx; -use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema}; -use base64::Engine; -use std::collections::HashMap; - -/// Resolve a rev string to a commit OID using the full rev-parse machinery -/// (branch names, tags, HEAD, hex prefixes, etc.). -fn resolve_commit_oid( - domain: &git::GitDomain, - rev: &str, -) -> Result { - domain.resolve_rev(rev).map_err(|e| e.to_string()) -} - -async fn git_file_content_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "HEAD".to_string()); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let oid = resolve_commit_oid(&domain, &rev)?; - - let entry = domain - .tree_entry_by_path_from_commit(&oid, path) - .map_err(|e| e.to_string())?; - let blob_info = domain.blob_get(&entry.oid).map_err(|e| e.to_string())?; - - let content = domain.blob_content(&entry.oid).map_err(|e| e.to_string())?; - let (display_content, is_binary) = if blob_info.is_binary { - ( - base64::engine::general_purpose::STANDARD.encode(&content.content), - true, - ) - } else { - (String::from_utf8_lossy(&content.content).to_string(), false) - }; - - Ok(serde_json::json!({ - "path": path, - "oid": entry.oid.to_string(), - "size": blob_info.size, - "content": display_content, - "is_binary": is_binary - })) -} - -async fn git_tree_ls_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let dir_path = p - .get("path") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "HEAD".to_string()); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let commit_oid = resolve_commit_oid(&domain, &rev)?; - - // Get tree OID from commit - let commit_meta = domain.commit_get(&commit_oid).map_err(|e| e.to_string())?; - let tree_oid = &commit_meta.tree_id; - - let entries = match dir_path { - Some(ref dp) => { - let entry = domain - .tree_entry_by_path(tree_oid, dp) - .map_err(|e| e.to_string())?; - domain.tree_list(&entry.oid).map_err(|e| e.to_string())? - } - None => domain.tree_list(tree_oid).map_err(|e| e.to_string())?, - }; - - let result: Vec<_> = entries - .iter() - .map(|e| { - serde_json::json!({ - "name": e.name, - "oid": e.oid.to_string(), - "kind": e.kind, - "is_binary": e.is_binary - }) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) -} - -async fn git_file_history_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| "HEAD".to_string()); - let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize; - - let domain = ctx.open_repo(project_name, repo_name).await?; - // Fetch extra commits to have enough candidates after filtering - let walk_limit = limit.saturating_mul(2).max(200); - let commits = domain - .commit_log(Some(&rev), 0, walk_limit) - .map_err(|e| e.to_string())?; - - let result: Vec<_> = commits - .iter() - .filter(|c| domain.tree_entry_by_path(&c.tree_id, path).is_ok()) - .take(limit) - .map(|c| flatten_commit(c)) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| e.to_string())?) -} - -async fn git_blob_get_exec( - ctx: GitToolCtx, - args: serde_json::Value, -) -> Result { - let p: serde_json::Map = - serde_json::from_value(args).map_err(|e| e.to_string())?; - let project_name = p - .get("project_name") - .and_then(|v| v.as_str()) - .ok_or("missing project_name")?; - let repo_name = p - .get("repo_name") - .and_then(|v| v.as_str()) - .ok_or("missing repo_name")?; - let path = p - .get("path") - .and_then(|v| v.as_str()) - .ok_or("missing path")?; - let rev = p - .get("rev") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| "HEAD".to_string()); - - let domain = ctx.open_repo(project_name, repo_name).await?; - let oid = resolve_commit_oid(&domain, &rev).map_err(|e| e.to_string())?; - - let entry = domain - .tree_entry_by_path_from_commit(&oid, path) - .map_err(|e| e.to_string())?; - let blob_info = domain.blob_get(&entry.oid).map_err(|e| e.to_string())?; - - if blob_info.is_binary { - return Err(format!("file '{}' is binary, cannot return as text", path)); - } - - let content = domain.blob_content(&entry.oid).map_err(|e| e.to_string())?; - let text = String::from_utf8_lossy(&content.content).to_string(); - - Ok(serde_json::json!({ - "path": path, - "oid": entry.oid.to_string(), - "size": blob_info.size, - "content": text, - })) -} - -fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value { - use chrono::TimeZone; - let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60); - let author_time = chrono::Utc - .timestamp_opt(ts, 0) - .single() - .map(|dt| dt.to_rfc3339()) - .unwrap_or_else(|| format!("{}", c.author.time_secs)); - let oid = c.oid.to_string(); - serde_json::json!({ - "oid": oid.clone(), - "short_oid": oid.get(..7).unwrap_or(&oid).to_string(), - "message": c.message, "summary": c.summary, - "author_name": c.author.name, "author_email": c.author.email, "author_time": author_time, - "committer_name": c.committer.name, "committer_email": c.committer.email, - "parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::>(), - "tree_oid": c.tree_id.to_string() - }) -} - -pub fn register_git_tools(registry: &mut ToolRegistry) { - // git_file_content - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("File path within the repository".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Revision to read file from (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "path".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_file_content").description("Read the full content of a file at a given revision. Handles both text and binary files.").parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_file_content_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_tree_ls - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("Directory path to list (root if omitted)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Revision to list tree from (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["project_name".into(), "repo_name".into()]), - }; - registry.register( - ToolDefinition::new("git_tree_ls").description("List the contents of a directory (tree) at a given revision, showing files and subdirectories.").parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_tree_ls_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_file_history - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("File path to trace history for".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Revision to start history from (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ), - ( - "limit".into(), - ToolParam { - name: "limit".into(), - param_type: "integer".into(), - description: Some("Maximum number of commits to return (default: 20)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "path".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_file_history").description("Show the commit history for a specific file, listing all commits that modified it.").parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_file_history_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); - - // git_blob_get - let p = HashMap::from([ - ( - "project_name".into(), - ToolParam { - name: "project_name".into(), - param_type: "string".into(), - description: Some("Project name (slug)".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("File path within the repository".into()), - required: true, - properties: None, - items: None, - }, - ), - ( - "rev".into(), - ToolParam { - name: "rev".into(), - param_type: "string".into(), - description: Some("Revision to read file from (default: HEAD)".into()), - required: false, - properties: None, - items: None, - }, - ), - ]); - let schema = ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec![ - "project_name".into(), - "repo_name".into(), - "path".into(), - ]), - }; - registry.register( - ToolDefinition::new("git_blob_get").description("Retrieve the raw content of a single file (blob) at a given revision. Returns error if the file is binary.").parameters(schema), - ToolHandler::new(|ctx, args| { - let gctx = super::ctx::GitToolCtx::new(ctx); - Box::pin(async move { - git_blob_get_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError) - }) - }), - ); -} diff --git a/libs/fctool/src/git_tools/types.rs b/libs/fctool/src/git_tools/types.rs deleted file mode 100644 index d470086..0000000 --- a/libs/fctool/src/git_tools/types.rs +++ /dev/null @@ -1,472 +0,0 @@ -//! Flat, JSON-friendly output types for git tools. -//! -//! These types convert from internal `git` crate types to clean, flat structures -//! suitable for JSON serialization (function call responses). - -use base64::Engine; -use chrono::TimeZone; -use git::commit::types::{CommitMeta, CommitReflogEntry}; -use git::diff::types::{DiffDelta, DiffDeltaStatus, DiffStats}; -use git::tree::types::TreeEntry; -use serde::{Deserialize, Serialize}; - -#[derive(serde::Deserialize)] -pub struct RevQuery { - pub rev: String, -} - -#[derive(serde::Deserialize)] -pub struct SearchCommits { - pub query: String, - #[serde(default = "dl")] - pub limit: u32, -} -fn dl() -> u32 { - 20 -} - -#[derive(serde::Deserialize)] -pub struct GraphParams { - #[serde(default)] - pub rev: Option, - #[serde(default = "dl")] - pub limit: u32, -} - -#[derive(serde::Deserialize)] -pub struct ReflogParams { - #[serde(default)] - pub ref_name: Option, - #[serde(default = "reflog_def")] - pub limit: u32, -} -fn reflog_def() -> u32 { - 50 -} - -#[derive(serde::Deserialize)] -pub struct SingleBranch { - pub name: String, -} - -#[derive(serde::Deserialize)] -pub struct BranchesMerged { - pub branch: String, - #[serde(default)] - pub into: Option, -} - -#[derive(serde::Deserialize)] -pub struct BranchDiffP { - pub local: String, - #[serde(default)] - pub remote: Option, -} - -#[derive(serde::Deserialize)] -pub struct DiffP { - #[serde(default)] - pub base: Option, - #[serde(default)] - pub head: Option, - #[serde(default)] - pub paths: Option>, -} - -#[derive(serde::Deserialize)] -pub struct DiffStatsP { - pub base: String, - pub head: String, -} - -#[derive(serde::Deserialize)] -pub struct BlameP { - pub path: String, - #[serde(default)] - pub rev: Option, - #[serde(default)] - pub from_line: Option, - #[serde(default)] - pub to_line: Option, -} - -#[derive(serde::Deserialize)] -pub struct FileContentP { - pub path: String, - #[serde(default)] - pub rev: Option, -} - -#[derive(serde::Deserialize)] -pub struct TreeLsP { - #[serde(default)] - pub path: Option, - #[serde(default)] - pub rev: Option, -} - -#[derive(serde::Deserialize)] -pub struct FileHistoryP { - pub path: String, - #[serde(default = "dl")] - pub limit: u32, -} - -#[derive(serde::Deserialize)] -pub struct TagListP { - #[serde(default)] - pub pattern: Option, -} - -#[derive(serde::Deserialize)] -pub struct SingleTagP { - pub name: String, -} - -#[derive(serde::Deserialize)] -pub struct GitLogP { - #[serde(default)] - pub rev: Option, - #[serde(default = "dl")] - pub limit: u32, - #[serde(default)] - pub skip: u32, -} - -/// Flat commit information for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitInfo { - pub oid: String, - pub short_oid: String, - pub message: String, - pub summary: String, - pub author_name: String, - pub author_email: String, - pub author_time: String, - pub committer_name: String, - pub committer_email: String, - pub parent_oids: Vec, - pub tree_oid: String, -} - -impl CommitInfo { - pub fn from_meta(meta: &CommitMeta) -> Self { - let ts = meta.author.time_secs; - let offset = meta.author.offset_minutes; - let author_time = format_rfc3339(ts, offset); - - Self { - oid: meta.oid.to_string(), - short_oid: meta - .oid - .to_string() - .get(..7) - .unwrap_or(&meta.oid.to_string()) - .to_string(), - message: meta.message.clone(), - summary: meta.summary.clone(), - author_name: meta.author.name.clone(), - author_email: meta.author.email.clone(), - author_time, - committer_name: meta.committer.name.clone(), - committer_email: meta.committer.email.clone(), - parent_oids: meta.parent_ids.iter().map(|p| p.to_string()).collect(), - tree_oid: meta.tree_id.to_string(), - } - } -} - -/// Commit reflog entry for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReflogEntryInfo { - pub oid_new: String, - pub oid_old: String, - pub committer_name: String, - pub committer_email: String, - pub time: String, - pub message: Option, - pub ref_name: String, -} - -impl ReflogEntryInfo { - pub fn from_entry(entry: &CommitReflogEntry) -> Self { - let ts = entry.time_secs; - let offset = entry.offset_minutes; - let time = format_rfc3339(ts, offset); - Self { - oid_new: entry.oid_new.to_string(), - oid_old: entry.oid_old.to_string(), - committer_name: entry.committer_name.clone(), - committer_email: entry.committer_email.clone(), - time, - message: entry.message.clone(), - ref_name: entry.ref_name.clone(), - } - } -} - -/// Flat branch info for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BranchInfoOut { - pub name: String, - pub oid: String, - pub short_oid: String, - pub is_head: bool, - pub is_remote: bool, - pub is_current: bool, - pub upstream: Option, -} - -impl From<&git::branch::types::BranchInfo> for BranchInfoOut { - fn from(b: &git::branch::types::BranchInfo) -> Self { - let oid = b.oid.to_string(); - Self { - name: b.name.clone(), - oid: oid.clone(), - short_oid: oid.get(..7).unwrap_or(&oid).to_string(), - is_head: b.is_head, - is_remote: b.is_remote, - is_current: b.is_current, - upstream: b.upstream.clone(), - } - } -} - -/// Branch diff (ahead/behind) for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BranchDiffOut { - pub ahead: usize, - pub behind: usize, - pub diverged: bool, -} - -impl From<&git::branch::types::BranchDiff> for BranchDiffOut { - fn from(d: &git::branch::types::BranchDiff) -> Self { - Self { - ahead: d.ahead, - behind: d.behind, - diverged: d.diverged, - } - } -} - -/// Diff statistics for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffStatsOut { - pub files_changed: u32, - pub insertions: u32, - pub deletions: u32, -} - -impl From<&DiffStats> for DiffStatsOut { - fn from(s: &DiffStats) -> Self { - Self { - files_changed: s.files_changed as u32, - insertions: s.insertions as u32, - deletions: s.deletions as u32, - } - } -} - -/// A single diff file change for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffFileOut { - pub path: Option, - pub status: String, - pub is_binary: bool, - pub size: u64, -} - -impl DiffFileOut { - pub fn from_delta(delta: &DiffDelta) -> Self { - // For deleted files, use old_file for all metadata; for all others, use new_file. - let (path, is_binary, size) = match delta.status { - DiffDeltaStatus::Deleted => ( - delta.old_file.path.clone(), - delta.old_file.is_binary, - delta.old_file.size, - ), - _ => ( - delta.new_file.path.clone(), - delta.new_file.is_binary, - delta.new_file.size, - ), - }; - Self { - path, - status: format!("{:?}", delta.status), - is_binary, - size, - } - } -} - -/// Diff summary (files + stats) for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffOut { - pub stats: DiffStatsOut, - pub files: Vec, -} - -impl DiffOut { - pub fn from_result(result: &git::diff::types::DiffResult) -> Self { - let stats = DiffStatsOut::from(&result.stats); - let files = result.deltas.iter().map(DiffFileOut::from_delta).collect(); - Self { stats, files } - } -} - -/// Blame hunk for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BlameHunkOut { - pub commit_oid: String, - pub short_oid: String, - pub final_start_line: u32, - pub final_lines: u32, - pub orig_start_line: u32, - pub orig_path: Option, - pub boundary: bool, -} - -impl From<&git::commit::types::CommitBlameHunk> for BlameHunkOut { - fn from(h: &git::commit::types::CommitBlameHunk) -> Self { - let oid = h.commit_oid.to_string(); - Self { - commit_oid: oid.clone(), - short_oid: oid.get(..7).unwrap_or(&oid).to_string(), - final_start_line: h.final_start_line, - final_lines: h.final_lines, - orig_start_line: h.orig_start_line, - orig_path: h.orig_path.clone(), - boundary: h.boundary, - } - } -} - -/// Directory entry for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TreeLsEntry { - pub name: String, - pub oid: String, - pub kind: String, - pub is_binary: bool, -} - -impl From<&TreeEntry> for TreeLsEntry { - fn from(entry: &TreeEntry) -> Self { - Self { - name: entry.name.clone(), - oid: entry.oid.to_string(), - kind: entry.kind.clone(), - is_binary: entry.is_binary, - } - } -} - -/// File content for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileContentOut { - pub path: String, - pub oid: String, - pub size: u64, - pub content: String, - pub is_binary: bool, -} - -impl FileContentOut { - pub fn from_blob( - path: String, - oid: &git::commit::types::CommitOid, - content: &[u8], - is_binary: bool, - ) -> Self { - let (display_content, size) = if is_binary { - ( - base64::engine::general_purpose::STANDARD.encode(content), - content.len() as u64, - ) - } else { - ( - String::from_utf8_lossy(content).to_string(), - content.len() as u64, - ) - }; - - Self { - path, - oid: oid.to_string(), - size, - content: display_content, - is_binary, - } - } -} - -/// Tag info for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TagInfoOut { - pub name: String, - pub oid: String, - pub target: String, - pub is_annotated: bool, - pub message: Option, - pub tagger_name: Option, - pub tagger_email: Option, -} - -impl From<&git::tags::types::TagInfo> for TagInfoOut { - fn from(t: &git::tags::types::TagInfo) -> Self { - Self { - name: t.name.clone(), - oid: t.oid.to_string(), - target: t.target.to_string(), - is_annotated: t.is_annotated, - message: t.message.clone(), - tagger_name: t.tagger.clone(), - tagger_email: t.tagger_email.clone(), - } - } -} - -/// Commit graph line for tool responses. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GraphLineOut { - pub oid: String, - pub short_oid: String, - pub refs: String, - pub short_message: String, - pub lane_index: usize, - pub author_name: String, - pub author_email: String, - pub author_time: String, - pub parent_oids: Vec, -} - -impl From<&git::commit::graph::CommitGraphLine> for GraphLineOut { - fn from(line: &git::commit::graph::CommitGraphLine) -> Self { - let oid = line.oid.to_string(); - let ts = line.meta.author.time_secs; - let offset = line.meta.author.offset_minutes; - Self { - oid: oid.clone(), - short_oid: oid.get(..7).unwrap_or(&oid).to_string(), - refs: line.refs.clone(), - short_message: line.short_message.clone(), - lane_index: line.lane_index, - author_name: line.meta.author.name.clone(), - author_email: line.meta.author.email.clone(), - author_time: format_rfc3339(ts, offset), - parent_oids: line.meta.parent_ids.iter().map(|p| p.to_string()).collect(), - } - } -} - -fn format_rfc3339(time_secs: i64, offset_minutes: i32) -> String { - // Git stores local time + offset. To convert to UTC, subtract the offset. - let secs = time_secs - (offset_minutes as i64 * 60); - chrono::Utc - .timestamp_opt(secs, 0) - .single() - .map(|dt| dt.to_rfc3339()) - .unwrap_or_else(|| format!("{}", time_secs)) -} diff --git a/libs/fctool/src/lib.rs b/libs/fctool/src/lib.rs deleted file mode 100644 index b8adbcc..0000000 --- a/libs/fctool/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! AI agent function-call tools: git operations, file parsing/search, project management, and chat tools. - -pub mod chat_tools; -pub mod file_tools; -pub mod git_tools; -pub mod project_tools; diff --git a/libs/fctool/src/project_tools/arxiv.rs b/libs/fctool/src/project_tools/arxiv.rs deleted file mode 100644 index 7e83960..0000000 --- a/libs/fctool/src/project_tools/arxiv.rs +++ /dev/null @@ -1,244 +0,0 @@ -//! Tool: project_arxiv_search — search arXiv papers by query - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use serde::Deserialize; -use std::collections::HashMap; - -/// Number of results to return by default. -const DEFAULT_MAX_RESULTS: usize = 10; -const MAX_MAX_RESULTS: usize = 50; - -/// arXiv API base URL (Atom feed). -const ARXIV_API: &str = "https://export.arxiv.org/api/query"; - -/// arXiv Atom feed entry fields we care about. -#[derive(Debug, Deserialize)] -struct ArxivEntry { - #[serde(rename = "id")] - entry_id: String, - #[serde(rename = "title")] - title: String, - #[serde(rename = "summary")] - summary: String, - #[serde(default, rename = "author")] - author: Vec, - #[serde(rename = "published")] - published: String, - #[serde(default, rename = "link")] - link: Vec, -} - -#[derive(Debug, Deserialize)] -struct ArxivAuthor { - #[serde(rename = "name")] - name: String, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -struct ArxivLink { - #[serde(rename = "type", default)] - link_type: String, - #[serde(rename = "href", default)] - href: String, - #[serde(rename = "title", default)] - title: String, - #[serde(rename = "rel", default)] - rel: String, -} - -#[derive(Debug, Deserialize)] -struct ArxivFeed { - #[serde(default, rename = "entry")] - entry: Vec, -} - -/// Search arXiv papers by query string. -/// -/// Returns up to `max_results` papers (default 10, max 50) matching the query. -/// Each result includes arXiv ID, title, authors, abstract, published date, and PDF URL. -pub async fn arxiv_search_exec( - _ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let query = args - .get("query") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("query is required".into()))?; - - let max_results = args - .get("max_results") - .and_then(|v| v.as_u64()) - .unwrap_or(DEFAULT_MAX_RESULTS as u64) - .min(MAX_MAX_RESULTS as u64) as usize; - - let start = args.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as usize; - - // Build arXiv API query URL - // Encode query for URL - let encoded_query = urlencoding_encode(query); - let url = format!( - "{}?search_query=all:{}&start={}&max_results={}&sortBy=relevance&sortOrder=descending", - ARXIV_API, encoded_query, start, max_results - ); - - let response = reqwest::get(&url) - .await - .map_err(|e| ToolError::ExecutionError(format!("HTTP request failed: {}", e)))?; - - if !response.status().is_success() { - return Err(ToolError::ExecutionError(format!( - "arXiv API returned status {}", - response.status() - ))); - } - - let body = response - .text() - .await - .map_err(|e| ToolError::ExecutionError(format!("Failed to read response: {}", e)))?; - - let feed: ArxivFeed = quick_xml::de::from_str(&body) - .map_err(|e| ToolError::ExecutionError(format!("Failed to parse Atom feed: {}", e)))?; - - let results: Vec = feed - .entry - .into_iter() - .map(|entry| { - // Extract PDF link - let pdf_url = entry - .link - .iter() - .find(|l| l.link_type == "application/pdf") - .map(|l| l.href.clone()) - .or_else(|| { - entry - .link - .iter() - .find(|l| l.rel == "alternate" && l.link_type.is_empty()) - .map(|l| l.href.replace("/abs/", "/pdf/")) - }) - .unwrap_or_default(); - - // Extract arXiv ID from entry id URL - // e.g. http://arxiv.org/abs/2312.12345v1 -> 2312.12345v1 - let arxiv_id = entry - .entry_id - .rsplit('/') - .next() - .unwrap_or(&entry.entry_id) - .trim(); - - // Whitespace-normalize title and abstract - let title = normalize_whitespace(&entry.title); - let summary = normalize_whitespace(&entry.summary); - let author_str = if entry.author.is_empty() { - "Unknown".to_string() - } else { - entry - .author - .iter() - .map(|a| a.name.as_str()) - .collect::>() - .join(", ") - }; - - serde_json::json!({ - "arxiv_id": arxiv_id, - "title": title, - "authors": author_str, - "abstract": summary, - "published": entry.published, - "pdf_url": pdf_url, - "abs_url": entry.entry_id, - }) - }) - .collect(); - - Ok(serde_json::json!({ - "count": results.len(), - "query": query, - "results": results, - })) -} - -// ─── helpers ─────────────────────────────────────────────────────────────────── - -fn urlencoding_encode(s: &str) -> String { - let mut encoded = String::with_capacity(s.len() * 2); - for b in s.bytes() { - match b { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { - encoded.push(b as char); - } - _ => { - encoded.push_str(&format!("%{:02X}", b)); - } - } - } - encoded -} - -fn normalize_whitespace(s: &str) -> String { - let s = s.trim(); - let mut result = String::with_capacity(s.len()); - let mut last_was_space = false; - for c in s.chars() { - if c.is_whitespace() { - if !last_was_space { - result.push(' '); - last_was_space = true; - } - } else { - result.push(c); - last_was_space = false; - } - } - result -} - -// ─── tool definition ───────────────────────────────────────────────────────── - -pub fn tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert("query".into(), ToolParam { - name: "query".into(), param_type: "string".into(), - description: Some("Search query (required). Supports arXiv search syntax, e.g. 'ti:transformer AND au:bengio'.".into()), - required: true, properties: None, items: None, - }); - p.insert( - "max_results".into(), - ToolParam { - name: "max_results".into(), - param_type: "integer".into(), - description: Some( - "Maximum number of results to return (default 10, max 50). Optional.".into(), - ), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "start".into(), - ToolParam { - name: "start".into(), - param_type: "integer".into(), - description: Some("Offset for pagination. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_arxiv_search") - .description( - "Search arXiv papers by keyword or phrase. \ - Returns paper titles, authors, abstracts, arXiv IDs, and PDF URLs. \ - Useful for finding academic papers relevant to the project or a task.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["query".into()]), - }) -} diff --git a/libs/fctool/src/project_tools/bing.rs b/libs/fctool/src/project_tools/bing.rs deleted file mode 100644 index 8e2550f..0000000 --- a/libs/fctool/src/project_tools/bing.rs +++ /dev/null @@ -1,280 +0,0 @@ -//! Tool: project_bing_search - search the web with Bing Web Search API. - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use serde::Deserialize; -use std::collections::HashMap; -use std::sync::OnceLock; - -const DEFAULT_COUNT: u64 = 10; -const MAX_COUNT: u64 = 50; -const DEFAULT_ENDPOINT: &str = "https://api.bing.microsoft.com/v7.0/search"; - -static SHARED_CLIENT: OnceLock = OnceLock::new(); - -fn shared_client() -> &'static reqwest::Client { - SHARED_CLIENT.get_or_init(|| { - reqwest::Client::builder() - .connect_timeout(std::time::Duration::from_secs(10)) - .timeout(std::time::Duration::from_secs(30)) - .build() - .expect("reqwest client build should not fail") - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct BingSearchResponse { - #[serde(default)] - web_pages: Option, - #[serde(default)] - query_context: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct BingWebPages { - #[serde(default)] - total_estimated_matches: Option, - #[serde(default)] - value: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct BingWebResult { - #[serde(default)] - name: String, - #[serde(default)] - url: String, - #[serde(default)] - display_url: String, - #[serde(default)] - snippet: String, - #[serde(default)] - date_last_crawled: Option, - #[serde(default)] - language: Option, - #[serde(default)] - is_family_friendly: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct BingQueryContext { - #[serde(default)] - original_query: String, - #[serde(default)] - altered_query: Option, -} - -pub async fn bing_search_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let query = args - .get("query") - .and_then(|v| v.as_str()) - .map(str::trim) - .filter(|s| !s.is_empty()) - .ok_or_else(|| ToolError::ExecutionError("query is required".into()))?; - - let count = args - .get("count") - .and_then(|v| v.as_u64()) - .unwrap_or(DEFAULT_COUNT) - .clamp(1, MAX_COUNT); - let offset = args.get("offset").and_then(|v| v.as_u64()).unwrap_or(0); - let market = args - .get("market") - .and_then(|v| v.as_str()) - .unwrap_or("en-US"); - let safe_search = args - .get("safe_search") - .and_then(|v| v.as_str()) - .unwrap_or("Moderate"); - let freshness = args.get("freshness").and_then(|v| v.as_str()); - - let api_key = ctx - .config() - .env - .get("APP_BING_SEARCH_API_KEY") - .or_else(|| ctx.config().env.get("BING_SEARCH_API_KEY")) - .map(String::as_str) - .filter(|s| !s.trim().is_empty()) - .ok_or_else(|| { - ToolError::ExecutionError( - "Bing search API key is required: set APP_BING_SEARCH_API_KEY or BING_SEARCH_API_KEY" - .into(), - ) - })?; - - let endpoint = ctx - .config() - .env - .get("APP_BING_SEARCH_ENDPOINT") - .map(String::as_str) - .unwrap_or(DEFAULT_ENDPOINT); - - let mut url = reqwest::Url::parse(endpoint) - .map_err(|e| ToolError::ExecutionError(format!("Invalid Bing endpoint: {}", e)))?; - { - let mut query_pairs = url.query_pairs_mut(); - query_pairs - .append_pair("q", query) - .append_pair("count", &count.to_string()) - .append_pair("offset", &offset.to_string()) - .append_pair("mkt", market) - .append_pair("safeSearch", safe_search) - .append_pair("responseFilter", "Webpages") - .append_pair("textFormat", "Raw"); - if let Some(freshness) = freshness { - query_pairs.append_pair("freshness", freshness); - } - } - - let response = shared_client() - .get(url) - .header("Ocp-Apim-Subscription-Key", api_key) - .send() - .await - .map_err(|e| ToolError::ExecutionError(format!("Bing search request failed: {}", e)))?; - let status = response.status(); - let body = response - .text() - .await - .map_err(|e| ToolError::ExecutionError(format!("Failed to read Bing response: {}", e)))?; - - if !status.is_success() { - return Err(ToolError::ExecutionError(format!( - "Bing search returned status {}: {}", - status, - truncate(&body, 500) - ))); - } - - let parsed: BingSearchResponse = serde_json::from_str(&body) - .map_err(|e| ToolError::ExecutionError(format!("Failed to parse Bing response: {}", e)))?; - - let results = parsed - .web_pages - .as_ref() - .map(|pages| { - pages - .value - .iter() - .map(|item| { - serde_json::json!({ - "title": item.name, - "url": item.url, - "display_url": item.display_url, - "snippet": item.snippet, - "date_last_crawled": item.date_last_crawled, - "language": item.language, - "is_family_friendly": item.is_family_friendly, - }) - }) - .collect::>() - }) - .unwrap_or_default(); - - Ok(serde_json::json!({ - "query": query, - "original_query": parsed.query_context.as_ref().map(|q| q.original_query.as_str()).unwrap_or(query), - "altered_query": parsed.query_context.and_then(|q| q.altered_query), - "count": results.len(), - "total_estimated_matches": parsed.web_pages.and_then(|p| p.total_estimated_matches), - "results": results, - })) -} - -fn truncate(s: &str, max_chars: usize) -> String { - let mut chars = s.chars(); - let truncated: String = chars.by_ref().take(max_chars).collect(); - if chars.next().is_some() { - format!("{}...", truncated) - } else { - truncated - } -} - -pub fn tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "query".into(), - ToolParam { - name: "query".into(), - param_type: "string".into(), - description: Some("Web search query. Required.".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "count".into(), - ToolParam { - name: "count".into(), - param_type: "integer".into(), - description: Some("Number of results to return. Default 10, max 50.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "offset".into(), - ToolParam { - name: "offset".into(), - param_type: "integer".into(), - description: Some("Result offset for pagination. Default 0.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "market".into(), - ToolParam { - name: "market".into(), - param_type: "string".into(), - description: Some("Market code such as en-US or zh-CN. Default en-US.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "safe_search".into(), - ToolParam { - name: "safe_search".into(), - param_type: "string".into(), - description: Some( - "Bing safe search level: Off, Moderate, or Strict. Default Moderate.".into(), - ), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "freshness".into(), - ToolParam { - name: "freshness".into(), - param_type: "string".into(), - description: Some("Optional freshness filter: Day, Week, or Month.".into()), - required: false, - properties: None, - items: None, - }, - ); - - ToolDefinition::new("project_bing_search") - .description( - "Search the public web with Bing Web Search API. Returns titles, URLs, snippets, crawl dates, and estimated match counts. Requires APP_BING_SEARCH_API_KEY or BING_SEARCH_API_KEY.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["query".into()]), - }) -} diff --git a/libs/fctool/src/project_tools/boards.rs b/libs/fctool/src/project_tools/boards.rs deleted file mode 100644 index 6cd7181..0000000 --- a/libs/fctool/src/project_tools/boards.rs +++ /dev/null @@ -1,989 +0,0 @@ -//! Tools: project_list_boards, project_create_board, project_update_board, -//! project_create_board_card, project_update_board_card, project_delete_board_card - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use chrono::Utc; -use models::projects::{MemberRole, ProjectBoard, ProjectBoardCard, ProjectBoardColumn}; -use models::projects::{project_board, project_board_card, project_board_column, project_members}; -use models::users::user::Model as UserModel; -use sea_orm::*; -use std::collections::HashMap; -use uuid::Uuid; - -// ─── helpers ────────────────────────────────────────────────────────────────── - -/// Check if the sender is an admin or owner of the project. -async fn require_admin( - db: &impl ConnectionTrait, - project_id: Uuid, - sender_id: Uuid, -) -> Result<(), ToolError> { - let member = project_members::Entity::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(sender_id)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let member = member - .ok_or_else(|| ToolError::ExecutionError("You are not a member of this project".into()))?; - let role = member - .scope_role() - .map_err(|_| ToolError::ExecutionError("Unknown member role".into()))?; - - match role { - MemberRole::Admin | MemberRole::Owner => Ok(()), - MemberRole::Member => Err(ToolError::ExecutionError( - "Only admin or owner can perform this action".into(), - )), - } -} - -#[allow(dead_code)] -fn serde_user(u: &UserModel) -> serde_json::Value { - serde_json::json!({ - "id": u.uid.to_string(), - "username": u.username, - "display_name": u.display_name, - }) -} - -// ─── list boards ────────────────────────────────────────────────────────────── - -pub async fn list_boards_exec( - ctx: ToolContext, - _args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let db = ctx.db(); - - let boards = project_board::Entity::find() - .filter(project_board::Column::Project.eq(project_id)) - .order_by_asc(project_board::Column::CreatedAt) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let board_ids: Vec<_> = boards.iter().map(|b| b.id).collect(); - - // Batch-load columns and cards - let columns = project_board_column::Entity::find() - .filter(project_board_column::Column::Board.is_in(board_ids.clone())) - .order_by_asc(project_board_column::Column::Position) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let column_ids: Vec<_> = columns.iter().map(|c| c.id).collect(); - let cards = project_board_card::Entity::find() - .filter(project_board_card::Column::Column.is_in(column_ids.clone())) - .order_by_asc(project_board_card::Column::Position) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - // Build column map - let col_map: std::collections::HashMap> = columns - .clone() - .into_iter() - .map(|c| { - let cards_in_col: Vec = cards - .iter() - .filter(|card| card.column == c.id) - .map(|card| { - serde_json::json!({ - "id": card.id.to_string(), - "issue_id": card.issue_id.map(|id| id.to_string()), - "title": card.title, - "description": card.description, - "position": card.position, - "assignee_id": card.assignee_id.map(|id| id.to_string()), - "due_date": card.due_date.map(|t| t.to_rfc3339()), - "priority": card.priority, - "created_at": card.created_at.to_rfc3339(), - }) - }) - .collect(); - (c.id, cards_in_col) - }) - .collect(); - - // Build column list per board - let mut board_col_map: std::collections::HashMap> = columns - .into_iter() - .fold(std::collections::HashMap::new(), |mut acc, c| { - let cards = col_map.get(&c.id).cloned().unwrap_or_default(); - acc.entry(c.board).or_default().push(serde_json::json!({ - "id": c.id.to_string(), - "name": c.name, - "position": c.position, - "wip_limit": c.wip_limit, - "color": c.color, - "cards": cards, - })); - acc - }); - - let result: Vec<_> = boards - .into_iter() - .map(|b| { - let cols = board_col_map.remove(&b.id).unwrap_or_default(); - serde_json::json!({ - "id": b.id.to_string(), - "name": b.name, - "description": b.description, - "created_by": b.created_by.to_string(), - "created_at": b.created_at.to_rfc3339(), - "updated_at": b.updated_at.to_rfc3339(), - "columns": cols, - }) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| ToolError::ExecutionError(e.to_string()))?) -} - -// ─── create board ───────────────────────────────────────────────────────────── - -pub async fn create_board_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - require_admin(db, project_id, sender_id).await?; - - let name = args - .get("name") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("name is required".into()))? - .to_string(); - - let description = args - .get("description") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let now = Utc::now(); - let active = project_board::ActiveModel { - id: Set(Uuid::now_v7()), - project: Set(project_id), - name: Set(name.clone()), - description: Set(description), - created_by: Set(sender_id), - created_at: Set(now), - updated_at: Set(now), - }; - - let model = active - .insert(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - Ok(serde_json::json!({ - "id": model.id.to_string(), - "name": model.name, - "description": model.description, - "created_by": model.created_by.to_string(), - "created_at": model.created_at.to_rfc3339(), - "updated_at": model.updated_at.to_rfc3339(), - "columns": Vec::::new(), - })) -} - -// ─── update board ───────────────────────────────────────────────────────────── - -pub async fn update_board_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - require_admin(db, project_id, sender_id).await?; - - let board_id = args - .get("board_id") - .and_then(|v| Uuid::parse_str(v.as_str()?).ok()) - .ok_or_else(|| ToolError::ExecutionError("board_id is required".into()))?; - - let board = ProjectBoard::find_by_id(board_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?; - - if board.project != project_id { - return Err(ToolError::ExecutionError( - "Board does not belong to this project".into(), - )); - } - - let mut active: project_board::ActiveModel = board.clone().into(); - let mut updated = false; - - if let Some(name) = args.get("name").and_then(|v| v.as_str()) { - active.name = Set(name.to_string()); - updated = true; - } - if let Some(description) = args.get("description") { - active.description = Set(description.as_str().map(|s| s.to_string())); - updated = true; - } - - if !updated { - return Err(ToolError::ExecutionError( - "At least one field must be provided".into(), - )); - } - - active.updated_at = Set(Utc::now()); - let model = active - .update(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - Ok(serde_json::json!({ - "id": model.id.to_string(), - "name": model.name, - "description": model.description, - "created_by": model.created_by.to_string(), - "created_at": model.created_at.to_rfc3339(), - "updated_at": model.updated_at.to_rfc3339(), - })) -} - -// ─── create board card ─────────────────────────────────────────────────────── - -pub async fn create_board_card_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - require_admin(db, project_id, sender_id).await?; - - let board_id = args - .get("board_id") - .and_then(|v| Uuid::parse_str(v.as_str()?).ok()) - .ok_or_else(|| ToolError::ExecutionError("board_id is required".into()))?; - - let title = args - .get("title") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("title is required".into()))? - .to_string(); - - let column_id = args - .get("column_id") - .and_then(|v| Uuid::parse_str(v.as_str()?).ok()); - - let description = args - .get("description") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let priority = args - .get("priority") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let assignee_id = args - .get("assignee_id") - .and_then(|v| Uuid::parse_str(v.as_str()?).ok()); - - let issue_id = args.get("issue_id").and_then(|v| v.as_i64()); - - // Verify board belongs to project - let board = ProjectBoard::find_by_id(board_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?; - - if board.project != project_id { - return Err(ToolError::ExecutionError( - "Board does not belong to this project".into(), - )); - } - - // Get target column (first column if not specified) - let target_column = if let Some(col_id) = column_id { - let col = ProjectBoardColumn::find_by_id(col_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?; - if col.board != board_id { - return Err(ToolError::ExecutionError( - "Column does not belong to this board".into(), - )); - } - col - } else { - ProjectBoardColumn::find() - .filter(project_board_column::Column::Board.eq(board_id)) - .order_by_asc(project_board_column::Column::Position) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("No columns found in this board".into()))? - }; - - // Next position - let max_pos: Option> = ProjectBoardCard::find() - .filter(project_board_card::Column::Column.eq(target_column.id)) - .select_only() - .column_as(project_board_card::Column::Position.max(), "max_pos") - .into_tuple::>() - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - let position = max_pos.flatten().unwrap_or(0) + 1; - - let now = Utc::now(); - let active = project_board_card::ActiveModel { - id: Set(Uuid::now_v7()), - column: Set(target_column.id), - issue_id: Set(issue_id), - project: Set(Some(project_id)), - title: Set(title), - description: Set(description), - position: Set(position), - assignee_id: Set(assignee_id), - due_date: Set(None), - priority: Set(priority), - created_by: Set(sender_id), - created_at: Set(now), - updated_at: Set(now), - }; - - let model = active - .insert(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - Ok(serde_json::json!({ - "id": model.id.to_string(), - "column_id": model.column.to_string(), - "title": model.title, - "description": model.description, - "position": model.position, - "assignee_id": model.assignee_id.map(|id| id.to_string()), - "issue_id": model.issue_id.map(|id| id.to_string()), - "priority": model.priority, - "created_at": model.created_at.to_rfc3339(), - "updated_at": model.updated_at.to_rfc3339(), - })) -} - -// ─── update board card ──────────────────────────────────────────────────────── - -pub async fn update_board_card_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - require_admin(db, project_id, sender_id).await?; - - let card_id = args - .get("card_id") - .and_then(|v| Uuid::parse_str(v.as_str()?).ok()) - .ok_or_else(|| ToolError::ExecutionError("card_id is required".into()))?; - - let card = ProjectBoardCard::find_by_id(card_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Card not found".into()))?; - - // Verify card belongs to a column in this project's board - let col = ProjectBoardColumn::find_by_id(card.column) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?; - - let board = ProjectBoard::find_by_id(col.board) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?; - - if board.project != project_id { - return Err(ToolError::ExecutionError( - "Card does not belong to this project".into(), - )); - } - - let mut active: project_board_card::ActiveModel = card.clone().into(); - let mut updated = false; - - if let Some(title) = args.get("title").and_then(|v| v.as_str()) { - active.title = Set(title.to_string()); - updated = true; - } - if let Some(description) = args.get("description") { - active.description = Set(description.as_str().map(|s| s.to_string())); - updated = true; - } - if let Some(column_id) = args - .get("column_id") - .and_then(|v| Uuid::parse_str(v.as_str()?).ok()) - { - // Verify column belongs to the same board - let new_col = ProjectBoardColumn::find_by_id(column_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?; - if new_col.board != col.board { - return Err(ToolError::ExecutionError( - "Column does not belong to this board".into(), - )); - } - active.column = Set(column_id); - updated = true; - } - if let Some(position) = args.get("position").and_then(|v| v.as_i64()) { - active.position = Set(position as i32); - updated = true; - } - if let Some(assignee_id) = args.get("assignee_id") { - active.assignee_id = Set(assignee_id.as_str().and_then(|s| Uuid::parse_str(s).ok())); - updated = true; - } - if let Some(issue_id) = args.get("issue_id") { - active.issue_id = Set(issue_id.as_i64()); - updated = true; - } - if let Some(priority) = args.get("priority") { - active.priority = Set(priority.as_str().map(|s| s.to_string())); - updated = true; - } - - if !updated { - return Err(ToolError::ExecutionError( - "At least one field must be provided".into(), - )); - } - - active.updated_at = Set(Utc::now()); - let model = active - .update(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - Ok(serde_json::json!({ - "id": model.id.to_string(), - "column_id": model.column.to_string(), - "title": model.title, - "description": model.description, - "position": model.position, - "assignee_id": model.assignee_id.map(|id| id.to_string()), - "priority": model.priority, - "created_at": model.created_at.to_rfc3339(), - "updated_at": model.updated_at.to_rfc3339(), - })) -} - -// ─── delete board card ───────────────────────────────────────────────────────── - -pub async fn delete_board_card_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - require_admin(db, project_id, sender_id).await?; - - let card_id = args - .get("card_id") - .and_then(|v| Uuid::parse_str(v.as_str()?).ok()) - .ok_or_else(|| ToolError::ExecutionError("card_id is required".into()))?; - - let card = ProjectBoardCard::find_by_id(card_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Card not found".into()))?; - - let col = ProjectBoardColumn::find_by_id(card.column) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Column not found".into()))?; - - let board = ProjectBoard::find_by_id(col.board) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?; - - if board.project != project_id { - return Err(ToolError::ExecutionError( - "Card does not belong to this project".into(), - )); - } - - ProjectBoardCard::delete_by_id(card_id) - .exec(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - Ok(serde_json::json!({ "deleted": true })) -} - -// ─── tool definitions ───────────────────────────────────────────────────────── - -pub fn list_tool_definition() -> ToolDefinition { - ToolDefinition::new("project_list_boards") - .description( - "List all Kanban boards in the current project. \ - Returns boards with their columns and cards, including positions and priorities.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: None, - required: None, - }) -} - -pub fn create_board_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "name".into(), - ToolParam { - name: "name".into(), - param_type: "string".into(), - description: Some("Board name (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "description".into(), - ToolParam { - name: "description".into(), - param_type: "string".into(), - description: Some("Board description. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_create_board") - .description( - "Create a new Kanban board in the current project. Requires admin or owner role.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["name".into()]), - }) -} - -pub fn update_board_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "board_id".into(), - ToolParam { - name: "board_id".into(), - param_type: "string".into(), - description: Some("Board UUID (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "name".into(), - ToolParam { - name: "name".into(), - param_type: "string".into(), - description: Some("New board name. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "description".into(), - ToolParam { - name: "description".into(), - param_type: "string".into(), - description: Some("New board description. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_update_board") - .description("Update a Kanban board (name or description). Requires admin or owner role.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["board_id".into()]), - }) -} - -pub fn create_card_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "board_id".into(), - ToolParam { - name: "board_id".into(), - param_type: "string".into(), - description: Some("Board UUID (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "column_id".into(), - ToolParam { - name: "column_id".into(), - param_type: "string".into(), - description: Some("Column UUID. Optional — defaults to first column.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "title".into(), - ToolParam { - name: "title".into(), - param_type: "string".into(), - description: Some("Card title (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "description".into(), - ToolParam { - name: "description".into(), - param_type: "string".into(), - description: Some("Card description. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "priority".into(), - ToolParam { - name: "priority".into(), - param_type: "string".into(), - description: Some("Card priority (e.g. 'low', 'medium', 'high'). Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "assignee_id".into(), - ToolParam { - name: "assignee_id".into(), - param_type: "string".into(), - description: Some("Card assignee user UUID. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "issue_id".into(), - ToolParam { - name: "issue_id".into(), - param_type: "integer".into(), - description: Some("Link a project issue NUMBER to this card. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_create_board_card") - .description( - "Create a card on a Kanban board. If column_id is not provided, \ - the card is added to the first column. Optionally link to a project issue.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["board_id".into(), "title".into()]), - }) -} - -pub fn update_card_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "card_id".into(), - ToolParam { - name: "card_id".into(), - param_type: "string".into(), - description: Some("Card UUID (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "title".into(), - ToolParam { - name: "title".into(), - param_type: "string".into(), - description: Some("New card title. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "description".into(), - ToolParam { - name: "description".into(), - param_type: "string".into(), - description: Some("New card description. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "column_id".into(), - ToolParam { - name: "column_id".into(), - param_type: "string".into(), - description: Some("Move card to a different column. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "position".into(), - ToolParam { - name: "position".into(), - param_type: "integer".into(), - description: Some("New position within column. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "priority".into(), - ToolParam { - name: "priority".into(), - param_type: "string".into(), - description: Some("New priority. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "assignee_id".into(), - ToolParam { - name: "assignee_id".into(), - param_type: "string".into(), - description: Some("New assignee UUID. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "issue_id".into(), - ToolParam { - name: "issue_id".into(), - param_type: "integer".into(), - description: Some( - "Link to a project issue number. Set to 0 to unlink. Optional.".into(), - ), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_update_board_card") - .description( - "Update a board card (title, description, column, position, assignee, priority). \ - Requires admin or owner role.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["card_id".into()]), - }) -} - -pub fn delete_card_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "card_id".into(), - ToolParam { - name: "card_id".into(), - param_type: "string".into(), - description: Some("Card UUID (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_delete_board_card") - .description("Delete a board card. Requires admin or owner role.") - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["card_id".into()]), - }) -} - -// ─── create board column ────────────────────────────────────────────────────── - -pub async fn create_board_column_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender".into()))?; - let db = ctx.db(); - - require_admin(db, project_id, sender_id).await?; - - let board_id = args - .get("board_id") - .and_then(|v| Uuid::parse_str(v.as_str()?).ok()) - .ok_or_else(|| ToolError::ExecutionError("board_id is required".into()))?; - - let name = args - .get("name") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("name is required".into()))? - .to_string(); - - let color = args - .get("color") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let board = ProjectBoard::find_by_id(board_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?; - if board.project != project_id { - return Err(ToolError::ExecutionError( - "Board does not belong to this project".into(), - )); - } - - let max_pos: Option> = ProjectBoardColumn::find() - .filter(project_board_column::Column::Board.eq(board_id)) - .select_only() - .column_as(project_board_column::Column::Position.max(), "max_pos") - .into_tuple::>() - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - let position = max_pos.flatten().unwrap_or(0) + 1; - - let _now = Utc::now(); - let active = project_board_column::ActiveModel { - id: Set(Uuid::now_v7()), - board: Set(board_id), - name: Set(name.clone()), - position: Set(position), - wip_limit: Set(None), - color: Set(color.clone()), - }; - - let model = active - .insert(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - Ok(serde_json::json!({ - "id": model.id.to_string(), - "board_id": model.board.to_string(), - "name": model.name, - "position": model.position, - "wip_limit": model.wip_limit, - "color": model.color, - })) -} - -pub fn create_column_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "board_id".into(), - ToolParam { - name: "board_id".into(), - param_type: "string".into(), - description: Some("Board UUID (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "name".into(), - ToolParam { - name: "name".into(), - param_type: "string".into(), - description: Some("Column name (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "color".into(), - ToolParam { - name: "color".into(), - param_type: "string".into(), - description: Some("Column color (e.g. '#ff0000'). Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_create_board_column") - .description( - "Create a new column on a Kanban board. \ - The column is appended at the end. Requires admin or owner role.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["board_id".into(), "name".into()]), - }) -} diff --git a/libs/fctool/src/project_tools/curl.rs b/libs/fctool/src/project_tools/curl.rs deleted file mode 100644 index 00c5174..0000000 --- a/libs/fctool/src/project_tools/curl.rs +++ /dev/null @@ -1,355 +0,0 @@ -//! Tool: project_curl — perform HTTP requests (GET/POST/PUT/DELETE) -//! -//! Security measures: -//! - SSRF protection: blocks private IPs and blocks redirects to private IPs -//! - Sensitive header injection: blocks Host, Authorization, Cookie, Proxy-* -//! - Connection pooling via a shared reqwest::Client - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use std::collections::HashMap; -use std::sync::OnceLock; - -/// Maximum response body size: 1 MB. -const MAX_BODY_BYTES: usize = 1 << 20; - -/// Headers that are blocked from user-supplied values to prevent injection attacks. -const BLOCKED_HEADERS: &[&str] = &[ - "host", - "authorization", - "cookie", - "proxy-authorization", - "proxy-connection", - "proxy-authenticate", -]; - -/// Shared reqwest::Client for connection pooling. -static SHARED_CLIENT: OnceLock = OnceLock::new(); - -fn shared_client() -> &'static reqwest::Client { - SHARED_CLIENT.get_or_init(|| { - reqwest::Client::builder() - .connect_timeout(std::time::Duration::from_secs(10)) - .timeout(std::time::Duration::from_secs(120)) - // Block automatic redirect following so we can validate each hop - .redirect(reqwest::redirect::Policy::limited(0)) - .build() - .expect("reqwest client build should not fail") - }) -} - -/// Check if a host string resolves to or is a private/internal IP. -fn is_private_host(host: &str) -> bool { - host.eq_ignore_ascii_case("localhost") - || host.eq_ignore_ascii_case("127.0.0.1") - || host.eq_ignore_ascii_case("::1") - || host.eq_ignore_ascii_case("0.0.0.0") - || host.eq_ignore_ascii_case("metadata.google.internal") - || host.eq_ignore_ascii_case("169.254.169.254") - || host.starts_with("10.") - || host.starts_with("172.16.") - || host.starts_with("172.17.") - || host.starts_with("172.18.") - || host.starts_with("172.19.") - || host.starts_with("172.20.") - || host.starts_with("172.21.") - || host.starts_with("172.22.") - || host.starts_with("172.23.") - || host.starts_with("172.24.") - || host.starts_with("172.25.") - || host.starts_with("172.26.") - || host.starts_with("172.27.") - || host.starts_with("172.28.") - || host.starts_with("172.29.") - || host.starts_with("172.30.") - || host.starts_with("172.31.") - || host.starts_with("192.168.") -} - -/// Validate URL and any redirect hops against SSRF rules. -fn validate_url_against_ssrf(url_str: &str) -> Result { - let parsed = reqwest::Url::parse(url_str) - .map_err(|e| ToolError::ExecutionError(format!("Invalid URL: {}", e)))?; - if let Some(host) = parsed.host_str() { - if is_private_host(host) { - return Err(ToolError::ExecutionError( - "Requests to internal/private IPs are not allowed for security reasons".into(), - )); - } - } - Ok(parsed) -} - -/// Perform an HTTP request and return the response body and metadata. -/// Supports GET, POST, PUT, DELETE methods. Useful for fetching web pages, -/// calling external APIs, or downloading resources. -pub async fn curl_exec( - _ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let url_str = args - .get("url") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("url is required".into()))?; - - // SSRF protection: validate initial URL - validate_url_against_ssrf(url_str)?; - - let method = args - .get("method") - .and_then(|v| v.as_str()) - .unwrap_or("GET") - .to_uppercase(); - - let body = args.get("body").and_then(|v| v.as_str()).map(String::from); - - let headers: Vec<(String, String)> = args - .get("headers") - .and_then(|v| v.as_object()) - .map(|obj| { - obj.iter() - .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) - .collect() - }) - .unwrap_or_default(); - - // Block sensitive headers that could be used for injection attacks - for (key, _) in &headers { - if BLOCKED_HEADERS.contains(&key.to_lowercase().as_str()) { - return Err(ToolError::ExecutionError(format!( - "Header '{}' is not allowed for security reasons", - key - ))); - } - } - - let timeout_secs = args - .get("timeout") - .and_then(|v| v.as_u64()) - .unwrap_or(30) - .min(120); - - let client = shared_client(); - // Build a per-request client with the specific timeout by using the shared - // client's connection pool but overriding timeout per request via request builder. - // Since reqwest::Client::builder().redirect(Policy::limited(0)) disables auto-redirects, - // we manually follow up to 5 redirects with SSRF validation on each hop. - - let mut current_url = url_str.to_string(); - let mut redirect_count = 0u32; - const MAX_REDIRECTS: u32 = 5; - - loop { - let mut request = match method.as_str() { - "GET" => client.get(¤t_url), - "POST" => client.post(¤t_url), - "PUT" => client.put(¤t_url), - "DELETE" => client.delete(¤t_url), - "PATCH" => client.patch(¤t_url), - "HEAD" => client.head(¤t_url), - _ => { - return Err(ToolError::ExecutionError(format!( - "Unsupported HTTP method: {}. Use GET, POST, PUT, DELETE, PATCH, or HEAD.", - method - ))); - } - }; - - request = request.timeout(std::time::Duration::from_secs(timeout_secs)); - - for (key, value) in &headers { - request = request.header(key, value); - } - - // Set default Content-Type for POST/PUT/PATCH if not provided and body exists - if body.is_some() - && !headers - .iter() - .any(|(k, _)| k.to_lowercase() == "content-type") - { - request = request.header("Content-Type", "application/json"); - } - - if let Some(ref b) = body { - request = request.body(b.clone()); - } - - let response = request - .send() - .await - .map_err(|e| ToolError::ExecutionError(format!("HTTP request failed: {}", e)))?; - - let status = response.status().as_u16(); - - // Handle redirects manually with SSRF validation - if status >= 300 && status < 400 { - redirect_count += 1; - if redirect_count > MAX_REDIRECTS { - return Err(ToolError::ExecutionError(format!( - "Too many redirects (max {})", - MAX_REDIRECTS - ))); - } - let location = response - .headers() - .get("location") - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_string()); - let location = match location { - Some(l) => l, - None => { - return Err(ToolError::ExecutionError( - "Redirect with no Location header".into(), - )); - } - }; - // Resolve relative redirect against current URL - let base = reqwest::Url::parse(¤t_url) - .map_err(|e| ToolError::ExecutionError(format!("Invalid current URL: {}", e)))?; - let next_url = base - .join(&location) - .map_err(|e| ToolError::ExecutionError(format!("Invalid redirect URL: {}", e)))?; - // Validate redirect target against SSRF rules - if let Some(host) = next_url.host_str() { - if is_private_host(host) { - return Err(ToolError::ExecutionError( - "Redirect to internal/private IP is not allowed".into(), - )); - } - } - current_url = next_url.to_string(); - continue; - } - - let status_text = response.status().canonical_reason().unwrap_or(""); - - let response_headers: std::collections::HashMap = response - .headers() - .iter() - .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) - .collect(); - - let content_type = response - .headers() - .get("content-type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("") - .to_string(); - - let is_text = content_type.starts_with("text/") - || content_type.contains("json") - || content_type.contains("xml") - || content_type.contains("javascript"); - - let body_bytes = response.bytes().await.map_err(|e| { - ToolError::ExecutionError(format!("Failed to read response body: {}", e)) - })?; - - let body_len = body_bytes.len(); - let truncated = body_len > MAX_BODY_BYTES; - let body_text = if truncated { - String::from("[Response truncated — exceeds 1 MB limit]") - } else if is_text { - String::from_utf8_lossy(&body_bytes).to_string() - } else { - format!( - "[Binary body, {} bytes, Content-Type: {}]", - body_len, content_type - ) - }; - - return Ok(serde_json::json!({ - "url": current_url, - "method": method, - "status": status, - "status_text": status_text, - "headers": response_headers, - "body": body_text, - "truncated": truncated, - "size_bytes": body_len, - })); - } -} - -// ─── tool definition ───────────────────────────────────────────────────────── - -fn tool_definition_with_name(name: &str) -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "url".into(), - ToolParam { - name: "url".into(), - param_type: "string".into(), - description: Some("Full URL to request (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "method".into(), - ToolParam { - name: "method".into(), - param_type: "string".into(), - description: Some("HTTP method: GET (default), POST, PUT, DELETE, PATCH, HEAD.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "body".into(), - ToolParam { - name: "body".into(), - param_type: "string".into(), - description: Some( - "Request body. Defaults to 'application/json' Content-Type if provided. Optional." - .into(), - ), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "headers".into(), - ToolParam { - name: "headers".into(), - param_type: "object".into(), - description: Some("HTTP headers as key-value pairs. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "timeout".into(), - ToolParam { - name: "timeout".into(), - param_type: "integer".into(), - description: Some("Request timeout in seconds (default 30, max 120). Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new(name) - .description( - "Perform an HTTP request to any URL. Supports GET, POST, PUT, DELETE, PATCH, HEAD. \ - Returns status code, headers, and response body. \ - Response body is truncated at 1 MB. Binary responses are described as text metadata. \ - Useful for fetching web pages, calling APIs, or downloading resources.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["url".into()]), - }) -} - -pub fn tool_definition() -> ToolDefinition { - tool_definition_with_name("project_curl") -} - -pub fn alias_tool_definition() -> ToolDefinition { - tool_definition_with_name("curl_exec") -} diff --git a/libs/fctool/src/project_tools/issues.rs b/libs/fctool/src/project_tools/issues.rs deleted file mode 100644 index 8dddcf2..0000000 --- a/libs/fctool/src/project_tools/issues.rs +++ /dev/null @@ -1,963 +0,0 @@ -//! Tools: project_list_issues, project_create_issue, project_update_issue - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use chrono::Utc; -use models::issues::{ - Issue, IssueAssignee, IssueLabel, IssueState, issue, issue_assignee, issue_label, -}; -use models::projects::project_members; -use models::projects::{MemberRole, ProjectMember}; -use models::system::{Label, label}; -use models::users::User; -use sea_orm::*; -use std::collections::HashMap; -use uuid::Uuid; - -// ─── list ───────────────────────────────────────────────────────────────────── - -pub async fn list_issues_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let db = ctx.db(); - - let state_filter = args - .get("state") - .and_then(|v| v.as_str()) - .map(|s| s.to_lowercase()); - - let mut query = issue::Entity::find().filter(issue::Column::Project.eq(project_id)); - - if let Some(ref state) = state_filter { - query = query.filter(issue::Column::State.eq(state)); - } - - let issues = query - .order_by_desc(issue::Column::CreatedAt) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let issue_ids: Vec<_> = issues.iter().map(|i| i.id).collect(); - - let assignees = IssueAssignee::find() - .filter(issue_assignee::Column::Issue.is_in(issue_ids.clone())) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let assignee_user_ids: Vec<_> = assignees.iter().map(|a| a.user).collect(); - let assignee_users = User::find() - .filter(models::users::user::Column::Uid.is_in(assignee_user_ids)) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - let user_map: std::collections::HashMap<_, _> = - assignee_users.into_iter().map(|u| (u.uid, u)).collect(); - - let issue_labels = IssueLabel::find() - .filter(issue_label::Column::Issue.is_in(issue_ids.clone())) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let label_ids: Vec<_> = issue_labels.iter().map(|l| l.label).collect(); - let labels = Label::find() - .filter(label::Column::Id.is_in(label_ids)) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - let label_map: std::collections::HashMap<_, _> = - labels.into_iter().map(|l| (l.id, l)).collect(); - - let assignee_map: std::collections::HashMap<_, Vec<_>> = assignees - .into_iter() - .filter_map(|a| { - let user = user_map.get(&a.user)?; - Some(( - a.issue, - serde_json::json!({ - "id": a.user.to_string(), - "username": user.username, - "display_name": user.display_name, - }), - )) - }) - .fold( - std::collections::HashMap::new(), - |mut acc, (issue_id, user)| { - acc.entry(issue_id).or_default().push(user); - acc - }, - ); - - let issue_label_map: std::collections::HashMap<_, Vec<_>> = issue_labels - .into_iter() - .filter_map(|il| { - let label = label_map.get(&il.label)?; - Some(( - il.issue, - serde_json::json!({ - "id": il.label, - "name": label.name, - "color": label.color, - }), - )) - }) - .fold( - std::collections::HashMap::new(), - |mut acc, (issue_id, label)| { - acc.entry(issue_id).or_default().push(label); - acc - }, - ); - - let result: Vec<_> = issues - .into_iter() - .map(|i| { - serde_json::json!({ - "id": i.id.to_string(), - "number": i.number, - "title": i.title, - "body": i.body, - "state": i.state, - "author_id": i.author.to_string(), - "milestone": i.milestone, - "created_at": i.created_at.to_rfc3339(), - "updated_at": i.updated_at.to_rfc3339(), - "closed_at": i.closed_at.map(|t| t.to_rfc3339()), - "assignees": assignee_map.get(&i.id).unwrap_or(&vec![]), - "labels": issue_label_map.get(&i.id).unwrap_or(&vec![]), - }) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| ToolError::ExecutionError(e.to_string()))?) -} - -// ─── helpers ─────────────────────────────────────────────────────────────────── - -/// Check if the user is the issue author OR an admin/owner of the project. -async fn require_issue_modifier( - db: &impl ConnectionTrait, - project_id: Uuid, - sender_id: Uuid, - author_id: Uuid, -) -> Result<(), ToolError> { - // Author can always modify their own issue - if sender_id == author_id { - return Ok(()); - } - // Otherwise require admin or owner - let member = ProjectMember::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(sender_id)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - let member = member - .ok_or_else(|| ToolError::ExecutionError("You are not a member of this project".into()))?; - let role = member - .scope_role() - .map_err(|_| ToolError::ExecutionError("Unknown member role".into()))?; - match role { - MemberRole::Admin | MemberRole::Owner => Ok(()), - MemberRole::Member => Err(ToolError::ExecutionError( - "Only the issue author or admin/owner can modify this issue".into(), - )), - } -} - -// ─── create ─────────────────────────────────────────────────────────────────── - -async fn next_issue_number(db: &impl ConnectionTrait, project_id: Uuid) -> Result { - let max_num: Option> = Issue::find() - .filter(issue::Column::Project.eq(project_id)) - .select_only() - .column_as(issue::Column::Number.max(), "max_num") - .into_tuple::>() - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - Ok(max_num.flatten().unwrap_or(0) + 1) -} - -pub async fn create_issue_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let db = ctx.db(); - - let title = args - .get("title") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("title is required".into()))? - .to_string(); - - let body = args - .get("body") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let milestone = args - .get("milestone") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let assignee_ids: Vec = args - .get("assignee_ids") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| Uuid::parse_str(v.as_str()?).ok()) - .collect() - }) - .unwrap_or_default(); - - let label_ids: Vec = args - .get("label_ids") - .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(|v| v.as_i64()).collect()) - .unwrap_or_default(); - - let author_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - - // Membership check: only project members can create issues - let member = ProjectMember::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(author_id)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - if member.is_none() { - return Err(ToolError::ExecutionError( - "You are not a member of this project".into(), - )); - } - - let number = next_issue_number(db, project_id).await?; - let now = Utc::now(); - - let active = issue::ActiveModel { - id: Set(Uuid::now_v7()), - project: Set(project_id), - number: Set(number), - title: Set(title.clone()), - body: Set(body), - state: Set(IssueState::Open.to_string()), - author: Set(author_id), - milestone: Set(milestone), - created_at: Set(now), - updated_at: Set(now), - closed_at: Set(None), - created_by_ai: Set(true), - ..Default::default() - }; - - let model = active - .insert(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - // Add assignees (collect errors for partial failure reporting) - let mut assignee_errors = Vec::new(); - for uid in &assignee_ids { - let a = issue_assignee::ActiveModel { - issue: Set(model.id), - user: Set(*uid), - assigned_at: Set(now), - ..Default::default() - }; - if let Err(e) = a.insert(db).await { - assignee_errors.push(format!("assignee {}: {}", uid, e)); - } - } - - // Add labels - let mut label_errors = Vec::new(); - for lid in &label_ids { - let l = issue_label::ActiveModel { - issue: Set(model.id), - label: Set(*lid), - relation_at: Set(now), - ..Default::default() - }; - if let Err(e) = l.insert(db).await { - label_errors.push(format!("label {}: {}", lid, e)); - } - } - - // Build assignee/label maps for response - let assignee_map: std::collections::HashMap = - if !assignee_ids.is_empty() { - let users = User::find() - .filter(models::users::user::Column::Uid.is_in(assignee_ids.clone())) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - users - .into_iter() - .map(|u| { - ( - u.uid, - serde_json::json!({ - "id": u.uid.to_string(), - "username": u.username, - "display_name": u.display_name, - }), - ) - }) - .collect() - } else { - std::collections::HashMap::new() - }; - - let label_map: std::collections::HashMap = if !label_ids.is_empty() { - let labels = Label::find() - .filter(label::Column::Id.is_in(label_ids.clone())) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - labels - .into_iter() - .map(|l| { - ( - l.id, - serde_json::json!({ - "id": l.id, - "name": l.name, - "color": l.color, - }), - ) - }) - .collect() - } else { - std::collections::HashMap::new() - }; - - Ok(serde_json::json!({ - "id": model.id.to_string(), - "number": model.number, - "title": model.title, - "body": model.body, - "state": model.state, - "author_id": model.author.to_string(), - "milestone": model.milestone, - "created_at": model.created_at.to_rfc3339(), - "updated_at": model.updated_at.to_rfc3339(), - "assignees": assignee_ids.iter().filter_map(|uid| assignee_map.get(uid)).collect::>(), - "labels": label_ids.iter().filter_map(|lid| label_map.get(lid)).collect::>(), - "warnings": if assignee_errors.is_empty() && label_errors.is_empty() { - None - } else { - Some([assignee_errors, label_errors].concat()) - }, - })) -} - -// ─── update ─────────────────────────────────────────────────────────────────── - -pub async fn update_issue_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - let number = args - .get("number") - .and_then(|v| v.as_i64()) - .ok_or_else(|| ToolError::ExecutionError("number is required".into()))?; - - // Find the issue - let issue = Issue::find() - .filter(issue::Column::Project.eq(project_id)) - .filter(issue::Column::Number.eq(number)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError(format!("Issue #{} not found", number)))?; - - // Permission check: author OR admin/owner - require_issue_modifier(db, project_id, sender_id, issue.author).await?; - - let mut active: issue::ActiveModel = issue.clone().into(); - let mut updated = false; - let now = Utc::now(); - - if let Some(title) = args.get("title").and_then(|v| v.as_str()) { - active.title = Set(title.to_string()); - updated = true; - } - if let Some(body) = args.get("body").and_then(|v| v.as_str()) { - active.body = Set(Some(body.to_string())); - updated = true; - } - if let Some(state) = args.get("state").and_then(|v| v.as_str()) { - let s = state.to_lowercase(); - if s == "open" || s == "closed" { - active.state = Set(s.clone()); - active.updated_at = Set(now); - if s == "closed" { - active.closed_at = Set(Some(now)); - } else { - active.closed_at = Set(None); - } - updated = true; - } - } - if let Some(milestone) = args.get("milestone") { - if milestone.is_null() { - active.milestone = Set(None); - } else if let Some(m) = milestone.as_str() { - active.milestone = Set(Some(m.to_string())); - } - updated = true; - } - - if updated { - active.updated_at = Set(now); - active - .update(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - } - - // Reload for response - let updated_issue = Issue::find() - .filter(issue::Column::Id.eq(issue.id)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Issue not found after update".into()))?; - - Ok(serde_json::json!({ - "id": updated_issue.id.to_string(), - "number": updated_issue.number, - "title": updated_issue.title, - "body": updated_issue.body, - "state": updated_issue.state, - "author_id": updated_issue.author.to_string(), - "milestone": updated_issue.milestone, - "created_at": updated_issue.created_at.to_rfc3339(), - "updated_at": updated_issue.updated_at.to_rfc3339(), - "closed_at": updated_issue.closed_at.map(|t| t.to_rfc3339()), - })) -} - -// ─── tool definitions ───────────────────────────────────────────────────────── - -pub fn list_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "state".into(), - ToolParam { - name: "state".into(), - param_type: "string".into(), - description: Some("Filter by issue state: 'open' or 'closed'. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_list_issues") - .description( - "List all issues in the current project. \ - Returns issue number, title, body, state, author, assignees, labels, and timestamps.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: None, - }) -} - -pub fn create_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "title".into(), - ToolParam { - name: "title".into(), - param_type: "string".into(), - description: Some("Issue title (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "body".into(), - ToolParam { - name: "body".into(), - param_type: "string".into(), - description: Some("Issue body / description. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "milestone".into(), - ToolParam { - name: "milestone".into(), - param_type: "string".into(), - description: Some("Milestone name. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "assignee_ids".into(), - ToolParam { - name: "assignee_ids".into(), - param_type: "array".into(), - description: Some("Array of user UUIDs to assign. Optional.".into()), - required: false, - properties: None, - items: Some(Box::new(ToolParam { - name: "".into(), - param_type: "string".into(), - description: None, - required: false, - properties: None, - items: None, - })), - }, - ); - p.insert( - "label_ids".into(), - ToolParam { - name: "label_ids".into(), - param_type: "array".into(), - description: Some("Array of label IDs to apply. Optional.".into()), - required: false, - properties: None, - items: Some(Box::new(ToolParam { - name: "".into(), - param_type: "integer".into(), - description: None, - required: false, - properties: None, - items: None, - })), - }, - ); - ToolDefinition::new("project_create_issue") - .description( - "Create a new issue in the current project. \ - Returns the created issue with its number, id, and full details.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["title".into()]), - }) -} - -pub fn update_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "number".into(), - ToolParam { - name: "number".into(), - param_type: "integer".into(), - description: Some("Issue number (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "title".into(), - ToolParam { - name: "title".into(), - param_type: "string".into(), - description: Some("New issue title. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "body".into(), - ToolParam { - name: "body".into(), - param_type: "string".into(), - description: Some("New issue body. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "state".into(), - ToolParam { - name: "state".into(), - param_type: "string".into(), - description: Some("New issue state: 'open' or 'closed'. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "milestone".into(), - ToolParam { - name: "milestone".into(), - param_type: "string".into(), - description: Some("New milestone name. Set to null to remove. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_update_issue") - .description( - "Update an existing issue in the current project by its number. \ - Requires the issue author or a project admin/owner. \ - Returns the updated issue. At least one field must be provided.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["number".into()]), - }) -} - -// ─── assign ──────────────────────────────────────────────────────────────────── - -/// Assign or unassign users to/from an issue. -pub async fn assign_issue_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender".into()))?; - let db = ctx.db(); - - let number = args - .get("number") - .and_then(|v| v.as_i64()) - .ok_or_else(|| ToolError::ExecutionError("number is required".into()))?; - - let issue = Issue::find() - .filter(issue::Column::Project.eq(project_id)) - .filter(issue::Column::Number.eq(number)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError(format!("Issue #{} not found", number)))?; - - require_issue_modifier(db, project_id, sender_id, issue.author).await?; - - let add_ids: Vec = args - .get("add_user_ids") - .and_then(|v| v.as_array()) - .map(|a| { - a.iter() - .filter_map(|v| Uuid::parse_str(v.as_str()?).ok()) - .collect() - }) - .unwrap_or_default(); - - let remove_ids: Vec = args - .get("remove_user_ids") - .and_then(|v| v.as_array()) - .map(|a| { - a.iter() - .filter_map(|v| Uuid::parse_str(v.as_str()?).ok()) - .collect() - }) - .unwrap_or_default(); - - let now = Utc::now(); - - for uid in &add_ids { - let exists = IssueAssignee::find() - .filter(issue_assignee::Column::Issue.eq(issue.id)) - .filter(issue_assignee::Column::User.eq(*uid)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - if exists.is_some() { - continue; - } - let am = issue_assignee::ActiveModel { - issue: Set(issue.id), - user: Set(*uid), - assigned_at: Set(now), - ..Default::default() - }; - am.insert(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - } - - for uid in &remove_ids { - IssueAssignee::delete_many() - .filter(issue_assignee::Column::Issue.eq(issue.id)) - .filter(issue_assignee::Column::User.eq(*uid)) - .exec(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - } - - // Build response - let current_assignee_ids: Vec = IssueAssignee::find() - .filter(issue_assignee::Column::Issue.eq(issue.id)) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .into_iter() - .map(|a| a.user) - .collect(); - - let users = if current_assignee_ids.is_empty() { - Vec::new() - } else { - User::find() - .filter(models::users::user::Column::Uid.is_in(current_assignee_ids.clone())) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - }; - - Ok(serde_json::json!({ - "issue_id": issue.id.to_string(), - "issue_number": issue.number, - "assignees": users.into_iter().map(|u| serde_json::json!({ - "id": u.uid.to_string(), - "username": u.username, - "display_name": u.display_name, - })).collect::>(), - })) -} - -pub fn assign_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "number".into(), - ToolParam { - name: "number".into(), - param_type: "integer".into(), - description: Some("Issue number (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "add_user_ids".into(), - ToolParam { - name: "add_user_ids".into(), - param_type: "array".into(), - description: Some("Array of user UUIDs to add as assignees. Optional.".into()), - required: false, - properties: None, - items: Some(Box::new(ToolParam { - name: "".into(), - param_type: "string".into(), - description: Some("User UUID".into()), - required: false, - properties: None, - items: None, - })), - }, - ); - p.insert( - "remove_user_ids".into(), - ToolParam { - name: "remove_user_ids".into(), - param_type: "array".into(), - description: Some("Array of user UUIDs to remove from assignees. Optional.".into()), - required: false, - properties: None, - items: Some(Box::new(ToolParam { - name: "".into(), - param_type: "string".into(), - description: Some("User UUID".into()), - required: false, - properties: None, - items: None, - })), - }, - ); - ToolDefinition::new("project_assign_issue") - .description( - "Add or remove assignees on an issue by its number. \ - Requires the issue author or a project admin/owner. \ - Returns the updated list of assignees.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["number".into()]), - }) -} - -// ─── add comment ─────────────────────────────────────────────────────────────── - -pub async fn add_comment_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender".into()))?; - let db = ctx.db(); - - let number = args - .get("number") - .and_then(|v| v.as_i64()) - .ok_or_else(|| ToolError::ExecutionError("number is required".into()))?; - - let body = args - .get("body") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("body is required".into()))? - .to_string(); - - let issue = Issue::find() - .filter(issue::Column::Project.eq(project_id)) - .filter(issue::Column::Number.eq(number)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError(format!("Issue #{} not found", number)))?; - - // Only project members can comment - let member = ProjectMember::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(sender_id)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - if member.is_none() { - return Err(ToolError::ExecutionError( - "You are not a member of this project".into(), - )); - } - - let now = Utc::now(); - let comment = models::issues::issue_comment::ActiveModel { - id: sea_orm::NotSet, - issue: Set(issue.id), - author: Set(sender_id), - body: Set(body.clone()), - created_at: Set(now), - updated_at: Set(now), - }; - let model = comment - .insert(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - // Update issue updated_at - let mut i_active: issue::ActiveModel = issue.into(); - i_active.updated_at = Set(now); - i_active - .update(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - // Look up author name - let author_name = User::find_by_id(sender_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .map(|u| u.display_name.unwrap_or(u.username)); - - Ok(serde_json::json!({ - "comment_id": model.id.to_string(), - "issue_number": number, - "body": body, - "author_id": sender_id.to_string(), - "author_name": author_name, - "created_at": now.to_rfc3339(), - })) -} - -pub fn add_comment_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "number".into(), - ToolParam { - name: "number".into(), - param_type: "integer".into(), - description: Some("Issue number (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "body".into(), - ToolParam { - name: "body".into(), - param_type: "string".into(), - description: Some("Comment body text (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_add_comment") - .description( - "Add a comment to an issue in the current project by its number. \ - Requires project membership. Returns the created comment.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["number".into(), "body".into()]), - }) -} - -// ─── list labels ─────────────────────────────────────────────────────────────── - -pub async fn list_labels_exec( - ctx: ToolContext, - _args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let db = ctx.db(); - - // Get labels associated with this project via issue_labels - let labels = Label::find() - .filter(label::Column::Project.eq(project_id)) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let result: Vec = labels - .into_iter() - .map(|l| { - serde_json::json!({ - "id": l.id, - "name": l.name, - "color": l.color, - }) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| ToolError::ExecutionError(e.to_string()))?) -} - -pub fn list_labels_tool_definition() -> ToolDefinition { - ToolDefinition::new("project_list_labels").description( - "List all labels available in the current project. \ - Returns label id, name, color, and description. \ - Use label IDs when creating or updating issues.", - ) -} diff --git a/libs/fctool/src/project_tools/members.rs b/libs/fctool/src/project_tools/members.rs deleted file mode 100644 index daa65d3..0000000 --- a/libs/fctool/src/project_tools/members.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Tool: project_list_members - -use agent::{ToolContext, ToolDefinition, ToolError, ToolSchema}; -use models::projects::project_members; -use models::users::User; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; - -pub async fn list_members_exec( - ctx: ToolContext, - _args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let db = ctx.db(); - - let members = project_members::Entity::find() - .filter(project_members::Column::Project.eq(project_id)) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let user_ids: Vec<_> = members.iter().map(|m| m.user).collect(); - let users = User::find() - .filter(models::users::user::Column::Uid.is_in(user_ids)) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let user_map: std::collections::HashMap<_, _> = users.into_iter().map(|u| (u.uid, u)).collect(); - - let result: Vec<_> = members - .into_iter() - .filter_map(|m| { - let user = user_map.get(&m.user)?; - let role = m - .scope_role() - .map(|r| r.to_string()) - .unwrap_or_else(|_| "member".to_string()); - Some(serde_json::json!({ - "id": m.user.to_string(), - "username": user.username, - "display_name": user.display_name, - "organization": user.organization, - "role": role, - "joined_at": m.joined_at.to_rfc3339(), - })) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| ToolError::ExecutionError(e.to_string()))?) -} - -pub fn tool_definition() -> ToolDefinition { - ToolDefinition::new("project_list_members") - .description( - "List all members in the current project. \ - Returns username, display name, organization, role, and join time.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: None, - required: None, - }) -} diff --git a/libs/fctool/src/project_tools/mod.rs b/libs/fctool/src/project_tools/mod.rs deleted file mode 100644 index 0fd2e5d..0000000 --- a/libs/fctool/src/project_tools/mod.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Project tools for AI agent function calling. -//! -//! Tools that let the agent perceive and modify the current project context: -//! - list repos in the project -//! - list members in the project -//! - list / create / update issues -//! - list / create / update boards and board cards - -mod arxiv; -mod bing; -mod boards; -mod curl; -mod issues; -mod members; -mod repos; - -use agent::{ToolHandler, ToolRegistry}; - -pub use arxiv::arxiv_search_exec; -pub use bing::bing_search_exec; -pub use boards::{ - create_board_card_exec, create_board_column_exec, create_board_exec, delete_board_card_exec, - list_boards_exec, update_board_card_exec, update_board_exec, -}; -pub use curl::curl_exec; -pub use issues::{ - add_comment_exec, assign_issue_exec, create_issue_exec, list_issues_exec, list_labels_exec, - update_issue_exec, -}; -pub use members::list_members_exec; -pub use repos::{create_commit_exec, create_repo_exec, list_repos_exec, update_repo_exec}; - -pub fn register_all(registry: &mut ToolRegistry) { - registry.register( - arxiv::tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(arxiv_search_exec(ctx, args))), - ); - - registry.register( - bing::tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(bing_search_exec(ctx, args))), - ); - - registry.register( - curl::tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(curl_exec(ctx, args))), - ); - registry.register( - curl::alias_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(curl_exec(ctx, args))), - ); - - registry.register( - repos::list_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(list_repos_exec(ctx, args))), - ); - registry.register( - repos::create_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(create_repo_exec(ctx, args))), - ); - registry.register( - repos::update_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(update_repo_exec(ctx, args))), - ); - registry.register( - repos::create_commit_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(create_commit_exec(ctx, args))), - ); - - registry.register( - members::tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(list_members_exec(ctx, args))), - ); - - registry.register( - issues::list_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(list_issues_exec(ctx, args))), - ); - registry.register( - issues::create_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(create_issue_exec(ctx, args))), - ); - registry.register( - issues::update_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(update_issue_exec(ctx, args))), - ); - registry.register( - issues::assign_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(assign_issue_exec(ctx, args))), - ); - registry.register( - issues::add_comment_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(add_comment_exec(ctx, args))), - ); - registry.register( - issues::list_labels_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(list_labels_exec(ctx, args))), - ); - - registry.register( - boards::list_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(list_boards_exec(ctx, args))), - ); - registry.register( - boards::create_board_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(create_board_exec(ctx, args))), - ); - registry.register( - boards::update_board_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(update_board_exec(ctx, args))), - ); - registry.register( - boards::create_card_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(create_board_card_exec(ctx, args))), - ); - registry.register( - boards::update_card_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(update_board_card_exec(ctx, args))), - ); - registry.register( - boards::delete_card_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(delete_board_card_exec(ctx, args))), - ); - registry.register( - boards::create_column_tool_definition(), - ToolHandler::new(|ctx, args| Box::pin(create_board_column_exec(ctx, args))), - ); -} diff --git a/libs/fctool/src/project_tools/repos.rs b/libs/fctool/src/project_tools/repos.rs deleted file mode 100644 index 3b4514c..0000000 --- a/libs/fctool/src/project_tools/repos.rs +++ /dev/null @@ -1,784 +0,0 @@ -//! Tool: project_list_repos, project_create_repo, project_create_commit - -use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema}; -use chrono::Utc; -use git::commit::types::CommitOid; -use git::commit::types::CommitSignature; -use git2; -use models::projects::project_members; -use models::projects::{MemberRole, ProjectMember}; -use models::repos::repo; -use models::users::user_email; -use sea_orm::*; -use std::collections::HashMap; -use std::path::PathBuf; -use uuid::Uuid; - -// ─── list ───────────────────────────────────────────────────────────────────── - -pub async fn list_repos_exec( - ctx: ToolContext, - _args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let db = ctx.db(); - - // Resolve project name so the AI can use it for git_tools operations - let project = models::projects::project::Entity::find_by_id(project_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Project not found".into()))?; - let project_name = project.name.clone(); - - let repos = repo::Entity::find() - .filter(repo::Column::Project.eq(project_id)) - .order_by_asc(repo::Column::RepoName) - .all(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - let result: Vec<_> = repos - .into_iter() - .map(|r| { - serde_json::json!({ - "id": r.id.to_string(), - "name": r.repo_name, - "project_name": project_name, - "description": r.description, - "default_branch": r.default_branch, - "is_private": r.is_private, - "created_at": r.created_at.to_rfc3339(), - }) - }) - .collect(); - - Ok(serde_json::to_value(result).map_err(|e| ToolError::ExecutionError(e.to_string()))?) -} - -// ─── create ─────────────────────────────────────────────────────────────────── - -pub async fn create_repo_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - // Admin/owner check - let member = ProjectMember::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(sender_id)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - let member = member - .ok_or_else(|| ToolError::ExecutionError("You are not a member of this project".into()))?; - let role = member - .scope_role() - .map_err(|_| ToolError::ExecutionError("Unknown member role".into()))?; - match role { - MemberRole::Admin | MemberRole::Owner => {} - MemberRole::Member => { - return Err(ToolError::ExecutionError( - "Only admin or owner can create repositories".into(), - )); - } - } - - let repo_name = args - .get("name") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("name is required".into()))? - .to_string(); - - // Validate repo name: no path traversal, no special chars - if repo_name.contains("..") - || repo_name.contains('/') - || repo_name.contains('\\') - || repo_name.is_empty() - || repo_name.len() > 100 - || !repo_name - .chars() - .next() - .map_or(false, |c| c.is_alphanumeric()) - { - return Err(ToolError::ExecutionError( - "Invalid repository name: must start with alphanumeric, contain no path separators or '..', max 100 chars".into(), - )); - } - - let description = args - .get("description") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let is_private = args - .get("is_private") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - // Check name uniqueness within project - let existing = repo::Entity::find() - .filter(repo::Column::Project.eq(project_id)) - .filter(repo::Column::RepoName.eq(&repo_name)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - if existing.is_some() { - return Err(ToolError::ExecutionError(format!( - "Repository '{}' already exists in this project", - repo_name - ))); - } - - // Look up project name for storage_path - let project = models::projects::project::Entity::find_by_id(project_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Project not found".into()))?; - - let repos_root = ctx - .config() - .repos_root() - .map_err(|e| ToolError::ExecutionError(format!("repos_root config error: {}", e)))?; - - let repo_dir: PathBuf = [&repos_root, &project.name, &format!("{}.git", repo_name)] - .iter() - .collect(); - - let now = Utc::now(); - let active = repo::ActiveModel { - id: Set(Uuid::now_v7()), - repo_name: Set(repo_name.clone()), - project: Set(project_id), - description: Set(description), - default_branch: Set("main".to_string()), - is_private: Set(is_private), - storage_path: Set(repo_dir.to_string_lossy().to_string()), - created_by: Set(sender_id), - created_at: Set(now), - updated_at: Set(now), - ai_code_review_enabled: Set(false), - }; - - let model = active - .insert(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - // Initialize the bare git repository on disk - git2::Repository::init_bare(&repo_dir) - .map_err(|e| ToolError::ExecutionError(format!("Failed to init bare repo: {}", e)))?; - - // Embed repo into Qdrant for semantic search (non-blocking) - if let Some(embed) = ctx.embed_service() { - let es = embed.clone(); - let repo_id = model.id.to_string(); - let repo_name = model.repo_name.clone(); - let repo_desc = model.description.clone(); - tokio::spawn(async move { - if let Err(e) = es - .embed_repo(&repo_id, &repo_name, repo_desc.as_deref()) - .await - { - tracing::warn!(error = %e, repo_id = %repo_id, "failed to embed repo"); - } - }); - } - - Ok(serde_json::json!({ - "id": model.id.to_string(), - "name": model.repo_name, - "description": model.description, - "default_branch": model.default_branch, - "is_private": model.is_private, - "created_at": model.created_at.to_rfc3339(), - })) -} - -// ─── update ─────────────────────────────────────────────────────────────────── - -pub async fn update_repo_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - // Admin/owner check - let member = ProjectMember::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(sender_id)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - let member = member - .ok_or_else(|| ToolError::ExecutionError("You are not a member of this project".into()))?; - let role = member - .scope_role() - .map_err(|_| ToolError::ExecutionError("Unknown member role".into()))?; - match role { - MemberRole::Admin | MemberRole::Owner => {} - MemberRole::Member => { - return Err(ToolError::ExecutionError( - "Only admin or owner can update repositories".into(), - )); - } - } - - let repo_name = args - .get("name") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("name is required".into()))? - .to_string(); - - let repo = repo::Entity::find() - .filter(repo::Column::Project.eq(project_id)) - .filter(repo::Column::RepoName.eq(&repo_name)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| { - ToolError::ExecutionError(format!("Repository '{}' not found", repo_name)) - })?; - - let mut active: repo::ActiveModel = repo.clone().into(); - let mut updated = false; - - if let Some(description) = args.get("description") { - active.description = Set(description.as_str().map(|s| s.to_string())); - updated = true; - } - if let Some(is_private) = args.get("is_private").and_then(|v| v.as_bool()) { - active.is_private = Set(is_private); - updated = true; - } - if let Some(default_branch) = args.get("default_branch").and_then(|v| v.as_str()) { - active.default_branch = Set(default_branch.to_string()); - updated = true; - } - - if !updated { - return Err(ToolError::ExecutionError( - "At least one field must be provided".into(), - )); - } - - active.updated_at = Set(Utc::now()); - let model = active - .update(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - - // Re-embed repo on update (non-blocking) - if let Some(embed) = ctx.embed_service() { - let es = embed.clone(); - let repo_id = model.id.to_string(); - let repo_name = model.repo_name.clone(); - let repo_desc = model.description.clone(); - tokio::spawn(async move { - if let Err(e) = es - .embed_repo(&repo_id, &repo_name, repo_desc.as_deref()) - .await - { - tracing::warn!(error = %e, repo_id = %repo_id, "failed to re-embed repo on update"); - } - }); - } - - Ok(serde_json::json!({ - "id": model.id.to_string(), - "name": model.repo_name, - "description": model.description, - "default_branch": model.default_branch, - "is_private": model.is_private, - "created_at": model.created_at.to_rfc3339(), - "updated_at": model.updated_at.to_rfc3339(), - })) -} - -// ─── create commit ──────────────────────────────────────────────────────────── - -pub async fn create_commit_exec( - ctx: ToolContext, - args: serde_json::Value, -) -> Result { - let project_id = ctx.project_id(); - let sender_id = ctx - .sender_id() - .ok_or_else(|| ToolError::ExecutionError("No sender context".into()))?; - let db = ctx.db(); - - // Admin/owner check - let member = ProjectMember::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(sender_id)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))?; - let member = member - .ok_or_else(|| ToolError::ExecutionError("You are not a member of this project".into()))?; - let role = member - .scope_role() - .map_err(|_| ToolError::ExecutionError("Unknown member role".into()))?; - match role { - MemberRole::Admin | MemberRole::Owner => {} - MemberRole::Member => { - return Err(ToolError::ExecutionError( - "Only admin or owner can create commits".into(), - )); - } - } - - let repo_name = args - .get("repo_name") - .or_else(|| args.get("name")) - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("repo_name (or name) is required".into()))?; - - let message = args - .get("message") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("message is required".into()))? - .to_string(); - - let branch = args - .get("branch") - .and_then(|v| v.as_str()) - .unwrap_or("main") - .to_string(); - - // Validate branch: no path traversal, no backslashes, not empty, no lock files - if branch.contains("..") - || branch.contains('\\') - || branch.is_empty() - || branch.ends_with(".lock") - || branch.starts_with('-') - { - return Err(ToolError::ExecutionError( - "Invalid branch name: must not contain '..' or backslashes, and must not be empty" - .into(), - )); - } - - let files = args - .get("files") - .and_then(|v| v.as_array()) - .ok_or_else(|| { - ToolError::ExecutionError("files is required and must be an array".into()) - })?; - - if files.is_empty() { - return Err(ToolError::ExecutionError( - "files array cannot be empty".into(), - )); - } - - // Clone files data for spawn_blocking - let files_data: Vec = files.iter().cloned().collect(); - - // Look up sender username and email - let sender = models::users::user::Entity::find_by_id(sender_id) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| ToolError::ExecutionError("Sender user not found".into()))?; - - let sender_email = user_email::Entity::find_by_id(sender_id) - .one(db) - .await - .ok() - .flatten() - .map(|e| e.email) - .unwrap_or_else(|| format!("{}@gitdata.ai", sender.username)); - - let author_name = sender - .display_name - .unwrap_or_else(|| sender.username.clone()); - - // Find repo - let repo_model = repo::Entity::find() - .filter(repo::Column::Project.eq(project_id)) - .filter(repo::Column::RepoName.eq(repo_name)) - .one(db) - .await - .map_err(|e| ToolError::ExecutionError(e.to_string()))? - .ok_or_else(|| { - ToolError::ExecutionError(format!("Repository '{}' not found", repo_name)) - })?; - - let storage_path = repo_model.storage_path.clone(); - - // Run git operations in a blocking thread - let result = tokio::task::spawn_blocking(move || { - let domain = git::GitDomain::open(&storage_path) - .map_err(|e| ToolError::ExecutionError(format!("Failed to open repo: {}", e)))?; - - let repo = domain.repo(); - - // Get current head commit (parent) - // If the repo already has commits (has HEAD), the branch must exist. - // Only allow root commits on truly empty repos (no HEAD at all). - let has_head = repo.head().is_ok(); - let parent_oid = repo.refname_to_id(&format!("refs/heads/{}", branch)).ok(); - - if has_head && parent_oid.is_none() { - return Err(ToolError::ExecutionError(format!( - "Branch '{}' does not exist in this repository", - branch - ))); - } - - let parent_ids: Vec = parent_oid - .map(|oid| CommitOid::from_git2(oid)) - .into_iter() - .collect(); - - // Build index from existing tree first (preserves all previous files), - // then add/overwrite with the new files. - let mut index = repo - .index() - .map_err(|e| ToolError::ExecutionError(format!("Failed to get index: {}", e)))?; - - // If repo has a parent commit, read its tree into the index so we don't - // lose existing files when write_tree() is called. - if let Some(oid) = &parent_oid { - let parent_commit = repo.find_commit(*oid).map_err(|e| { - ToolError::ExecutionError(format!("Failed to find parent commit: {}", e)) - })?; - let parent_tree = parent_commit.tree().map_err(|e| { - ToolError::ExecutionError(format!("Failed to get parent tree: {}", e)) - })?; - index.read_tree(&parent_tree).map_err(|e| { - ToolError::ExecutionError(format!("Failed to read parent tree into index: {}", e)) - })?; - } - - for file in files_data { - let path = file - .get("path") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("Each file must have a 'path'".into()))?; - - // Validate path: no traversal, no absolute paths, no .git/ prefix - if path.contains("..") - || path.starts_with('/') - || path.starts_with('\\') - || path.is_empty() - || path.starts_with(".git/") - || path == ".git" - { - return Err(ToolError::ExecutionError(format!( - "Invalid file path '{}': must be relative, no '..' or absolute path components", - path - ))); - } - let content = file - .get("content") - .and_then(|v| v.as_str()) - .ok_or_else(|| ToolError::ExecutionError("Each file must have 'content'".into()))?; - - // add_frombuffer requires an IndexEntry with at minimum a path field set. - // It works for both bare and non-bare repos (add_path requires a working tree). - let mut entry = git2::IndexEntry { - ctime: git2::IndexTime::new(0, 0), - mtime: git2::IndexTime::new(0, 0), - dev: 0, - ino: 0, - mode: 0o100644, - uid: 0, - gid: 0, - file_size: 0, - id: git2::Oid::zero(), - flags: 0, - flags_extended: 0, - path: path.as_bytes().to_vec(), - }; - index - .add_frombuffer(&mut entry, content.as_bytes()) - .map_err(|e| { - ToolError::ExecutionError(format!("Failed to add '{}' to index: {}", path, e)) - })?; - } - - let tree_oid = index - .write_tree() - .map_err(|e| ToolError::ExecutionError(format!("Failed to write tree: {}", e)))?; - - let tree_id = CommitOid::from_git2(tree_oid); - - // Author signature - let author = CommitSignature { - name: author_name.clone(), - email: sender_email.clone(), - time_secs: chrono::Utc::now().timestamp(), - offset_minutes: 0, - }; - - // Committer signature: gitpanda - let committer = CommitSignature { - name: "gitpanda".to_string(), - email: "info@gitdata.ai".to_string(), - time_secs: chrono::Utc::now().timestamp(), - offset_minutes: 0, - }; - - let commit_oid = domain - .commit_create( - Some(&format!("refs/heads/{}", branch)), - &author, - &committer, - &message, - &tree_id, - &parent_ids, - ) - .map_err(|e| ToolError::ExecutionError(format!("Failed to create commit: {}", e)))?; - - Ok::<_, ToolError>(serde_json::json!({ - "commit_oid": commit_oid.to_string(), - "branch": branch, - "message": message, - "author_name": author_name, - "author_email": sender_email, - })) - }) - .await - .map_err(|e| ToolError::ExecutionError(format!("Task join error: {}", e)))?; - - result -} - -// ─── tool definitions ───────────────────────────────────────────────────────── - -pub fn list_tool_definition() -> ToolDefinition { - ToolDefinition::new("project_list_repos") - .description( - "List all repositories in the current project. \ - Returns repo name, description, default branch, privacy status, and creation time.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: None, - required: None, - }) -} - -pub fn create_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "name".into(), - ToolParam { - name: "name".into(), - param_type: "string".into(), - description: Some( - "Repository name (required). Must be unique within the project.".into(), - ), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "description".into(), - ToolParam { - name: "description".into(), - param_type: "string".into(), - description: Some("Repository description. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "is_private".into(), - ToolParam { - name: "is_private".into(), - param_type: "boolean".into(), - description: Some("Whether the repo is private. Defaults to false. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_create_repo") - .description( - "Create a new repository in the current project. \ - Requires admin or owner role. \ - The repo is initialized with a bare git structure.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["name".into()]), - }) -} - -pub fn update_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "name".into(), - ToolParam { - name: "name".into(), - param_type: "string".into(), - description: Some("Repository name (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "description".into(), - ToolParam { - name: "description".into(), - param_type: "string".into(), - description: Some("New repository description. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "is_private".into(), - ToolParam { - name: "is_private".into(), - param_type: "boolean".into(), - description: Some("New privacy setting. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "default_branch".into(), - ToolParam { - name: "default_branch".into(), - param_type: "string".into(), - description: Some("New default branch name. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - ToolDefinition::new("project_update_repo") - .description( - "Update a repository's description, privacy, or default branch. \ - Requires admin or owner role.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["name".into()]), - }) -} - -pub fn create_commit_tool_definition() -> ToolDefinition { - let mut p = HashMap::new(); - p.insert( - "repo_name".into(), - ToolParam { - name: "repo_name".into(), - param_type: "string".into(), - description: Some("Repository name. Can also use 'name' as alias. Required.".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "name".into(), - ToolParam { - name: "name".into(), - param_type: "string".into(), - description: Some( - "Alias for repo_name. Use the same value as returned by project_list_repos.".into(), - ), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "branch".into(), - ToolParam { - name: "branch".into(), - param_type: "string".into(), - description: Some("Branch to commit to. Defaults to 'main'. Optional.".into()), - required: false, - properties: None, - items: None, - }, - ); - p.insert( - "message".into(), - ToolParam { - name: "message".into(), - param_type: "string".into(), - description: Some("Commit message (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - // files items - let mut file_item = HashMap::new(); - file_item.insert( - "path".into(), - ToolParam { - name: "path".into(), - param_type: "string".into(), - description: Some("File path in the repo (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - file_item.insert( - "content".into(), - ToolParam { - name: "content".into(), - param_type: "string".into(), - description: Some("Full file content as string (required).".into()), - required: true, - properties: None, - items: None, - }, - ); - p.insert( - "files".into(), - ToolParam { - name: "files".into(), - param_type: "array".into(), - description: Some("Array of files to commit (required, non-empty).".into()), - required: true, - properties: None, - items: Some(Box::new(ToolParam { - name: "".into(), - param_type: "object".into(), - description: None, - required: true, - properties: Some(file_item), - items: None, - })), - }, - ); - ToolDefinition::new("project_create_commit") - .description( - "Create a new commit in a repository. Commits the given files to the specified branch. \ - Requires admin or owner role. \ - Committer is always 'gitpanda '. \ - Author is the sender's display name and email.", - ) - .parameters(ToolSchema { - schema_type: "object".into(), - properties: Some(p), - required: Some(vec!["repo_name".into(), "message".into(), "files".into()]), - }) -} diff --git a/libs/git/Cargo.toml b/libs/git/Cargo.toml deleted file mode 100644 index aa56f7f..0000000 --- a/libs/git/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "git" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true -[lib] -path = "lib.rs" -name = "git" -[dependencies] -git2 = { workspace = true, features = [] } -git2-hooks = { workspace = true, features = [] } -git2-ext = { workspace = true, features = [] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -tar = { workspace = true } -flate2 = { workspace = true } -zip = { workspace = true } -globset = { workspace = true } -models = { workspace = true } -db = { workspace = true } -deadpool-redis = { workspace = true, features = ["rt_tokio_1", "cluster-async", "cluster"] } -config = { workspace = true } -tokio = { workspace = true, features = ["sync", "rt", "process"] } -tracing = { workspace = true } -tokio-util = { workspace = true } -redis = { workspace = true } -uuid = { workspace = true, features = ["v4", "v7"] } -sea-orm = { workspace = true, features = ["macros"] } -chrono = { workspace = true } -num_cpus = { workspace = true } -futures = { workspace = true } -russh = { workspace = true, features = ["legacy-ed25519-pkcs8-parser"] } -anyhow = { workspace = true } -argon2 = { workspace = true } -rsa = { workspace = true } -password-hash = { workspace = true } -base64 = { workspace = true } -sha1 = { workspace = true } -sha2 = { workspace = true } -futures-util = { workspace = true } -async-stream = { workspace = true } -ssh-key = { workspace = true } -actix-web = { workspace = true } -hex = { workspace = true } -reqwest = { workspace = true } -metrics = { workspace = true } -async-trait = { workspace = true } -[lints] -workspace = true diff --git a/libs/git/archive/mod.rs b/libs/git/archive/mod.rs deleted file mode 100644 index c11e298..0000000 --- a/libs/git/archive/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Archive domain — generate .tar, .tar.gz, and .zip archives from git trees. -pub mod ops; -pub mod types; diff --git a/libs/git/archive/ops.rs b/libs/git/archive/ops.rs deleted file mode 100644 index 7dc69f3..0000000 --- a/libs/git/archive/ops.rs +++ /dev/null @@ -1,584 +0,0 @@ -//! Archive operations. -//! -//! Generates .tar, .tar.gz, and .zip archives from git trees with caching support. - -use std::fs; -use std::io::{Cursor, Write}; -use std::path::PathBuf; - -use flate2::Compression; -use flate2::write::GzEncoder; - -use crate::archive::types::{ArchiveEntry, ArchiveFormat, ArchiveOptions, ArchiveSummary}; -use crate::commit::types::CommitOid; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - /// Directory where cached archives are stored. - fn archive_cache_dir(&self) -> PathBuf { - PathBuf::from(self.repo().path()).join(".git-archives") - } - - /// Path to the cached archive file for a given commit/format/options. - fn archive_cache_path( - &self, - commit_oid: &CommitOid, - format: ArchiveFormat, - opts: &ArchiveOptions, - ) -> PathBuf { - let ext = match format { - ArchiveFormat::Tar => "tar", - ArchiveFormat::TarGz => "tar.gz", - ArchiveFormat::Zip => "zip", - }; - let key = opts.cache_key(); - self.archive_cache_dir() - .join(format!("{}{}.{}", commit_oid.as_str(), key, ext)) - } - - /// Ensure the cache directory exists. - fn ensure_archive_cache_dir(&self) -> GitResult<()> { - let dir = self.archive_cache_dir(); - if !dir.exists() { - fs::create_dir_all(&dir).map_err(|e| GitError::IoError(e.to_string()))?; - } - Ok(()) - } - - /// Generate a plain tar archive from a commit's tree. - /// Caches the result after first build. - pub fn archive_tar( - &self, - commit_oid: &CommitOid, - opts: Option, - ) -> GitResult> { - let opts = opts.unwrap_or_default(); - let cache_path = self.archive_cache_path(commit_oid, ArchiveFormat::Tar, &opts); - - if cache_path.exists() { - return fs::read(&cache_path).map_err(|e| GitError::IoError(e.to_string())); - } - - let tree = self.tree_from_commit(commit_oid)?; - let mut buf = Vec::new(); - let base = opts.prefix.as_deref().unwrap_or(""); - self.walk_tar(&mut buf, &tree, base, &opts)?; - - self.ensure_archive_cache_dir()?; - fs::write(&cache_path, &buf).map_err(|e| GitError::IoError(e.to_string()))?; - - Ok(buf) - } - - /// Generate a tar.gz archive from a commit's tree. - /// Caches the result after first build. - pub fn archive_tar_gz( - &self, - commit_oid: &CommitOid, - opts: Option, - ) -> GitResult> { - let opts = opts.unwrap_or_default(); - let cache_path = self.archive_cache_path(commit_oid, ArchiveFormat::TarGz, &opts); - - if cache_path.exists() { - return fs::read(&cache_path).map_err(|e| GitError::IoError(e.to_string())); - } - - let tree = self.tree_from_commit(commit_oid)?; - let mut buf = Vec::new(); - { - let encoder = GzEncoder::new(&mut buf, Compression::default()); - let mut builder = tar::Builder::new(encoder); - let base = opts.prefix.as_deref().unwrap_or(""); - self.walk_tar_builder(&mut builder, &tree, base, &opts)?; - let encoder = builder - .into_inner() - .map_err(|e| GitError::Internal(e.to_string()))?; - encoder - .finish() - .map_err(|e| GitError::Internal(e.to_string()))?; - } - - self.ensure_archive_cache_dir()?; - fs::write(&cache_path, &buf).map_err(|e| GitError::IoError(e.to_string()))?; - - Ok(buf) - } - - /// Generate a zip archive from a commit's tree. - /// Caches the result after first build. - pub fn archive_zip( - &self, - commit_oid: &CommitOid, - opts: Option, - ) -> GitResult> { - let opts = opts.unwrap_or_default(); - let cache_path = self.archive_cache_path(commit_oid, ArchiveFormat::Zip, &opts); - - if cache_path.exists() { - return fs::read(&cache_path).map_err(|e| GitError::IoError(e.to_string())); - } - - let tree = self.tree_from_commit(commit_oid)?; - let mut zip_buf = Vec::new(); - let base = opts.prefix.as_deref().unwrap_or(""); - self.walk_zip(&mut zip_buf, &tree, base, &opts)?; - - self.ensure_archive_cache_dir()?; - fs::write(&cache_path, &zip_buf).map_err(|e| GitError::IoError(e.to_string()))?; - - Ok(zip_buf) - } - - /// Generate an archive in the specified format. - /// Results are cached keyed by (commit_oid, format, options). - pub fn archive( - &self, - commit_oid: &CommitOid, - format: ArchiveFormat, - opts: Option, - ) -> GitResult> { - match format { - ArchiveFormat::Tar => self.archive_tar(commit_oid, opts), - ArchiveFormat::TarGz => self.archive_tar_gz(commit_oid, opts), - ArchiveFormat::Zip => self.archive_zip(commit_oid, opts), - } - } - - /// List all entries that would be included in an archive. - pub fn archive_list( - &self, - commit_oid: &CommitOid, - opts: Option, - ) -> GitResult> { - let tree = self.tree_from_commit(commit_oid)?; - let opts = opts.unwrap_or_default(); - let mut entries = Vec::new(); - self.collect_tree_entries(&mut entries, &tree, "", 0, &opts)?; - Ok(entries) - } - - pub fn archive_summary( - &self, - commit_oid: &CommitOid, - format: ArchiveFormat, - opts: Option, - ) -> GitResult { - let entries = self.archive_list(commit_oid, opts)?; - let total_size: u64 = entries.iter().map(|e| e.size).sum(); - Ok(ArchiveSummary { - commit_oid: commit_oid.to_string(), - format, - total_entries: entries.len(), - total_size, - }) - } - - pub fn archive_cached( - &self, - commit_oid: &CommitOid, - format: ArchiveFormat, - opts: Option, - ) -> bool { - let opts = opts.unwrap_or_default(); - self.archive_cache_path(commit_oid, format, &opts).exists() - } - - /// Invalidate (delete) a cached archive, if it exists. - /// Call this when you need a fresh build after the repo state changes. - pub fn archive_invalidate( - &self, - commit_oid: &CommitOid, - format: ArchiveFormat, - opts: Option, - ) -> GitResult { - let opts = opts.unwrap_or_default(); - let path = self.archive_cache_path(commit_oid, format, &opts); - if path.exists() { - fs::remove_file(&path).map_err(|e| GitError::IoError(e.to_string()))?; - Ok(true) - } else { - Ok(false) - } - } - - /// List all cached archive paths for a given commit. - pub fn archive_cache_list(&self, commit_oid: &CommitOid) -> GitResult> { - let dir = self.archive_cache_dir(); - if !dir.exists() { - return Ok(Vec::new()); - } - let prefix = commit_oid.as_str(); - let mut paths = Vec::new(); - for entry in fs::read_dir(&dir).map_err(|e| GitError::IoError(e.to_string()))? { - let entry = entry.map_err(|e| GitError::IoError(e.to_string()))?; - let name = entry.file_name(); - let name = name.to_string_lossy(); - if name.starts_with(prefix) { - paths.push(entry.path()); - } - } - Ok(paths) - } - - /// Invalidate all cached archives for a given commit. - pub fn archive_invalidate_all(&self, commit_oid: &CommitOid) -> GitResult { - let paths = self.archive_cache_list(commit_oid)?; - let count = paths.len(); - for p in paths { - fs::remove_file(&p).map_err(|e| GitError::IoError(e.to_string()))?; - } - Ok(count) - } - - fn tree_from_commit(&self, commit_oid: &CommitOid) -> GitResult> { - let oid = commit_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(commit_oid.to_string()))?; - let commit = self - .repo() - .find_commit(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - self.repo() - .find_tree(commit.tree_id()) - .map_err(|e| GitError::Internal(e.to_string())) - } - - fn walk_tar( - &self, - buf: &mut Vec, - tree: &git2::Tree<'_>, - base: &str, - opts: &ArchiveOptions, - ) -> GitResult<()> { - for entry in tree.iter() { - let name = entry.name().unwrap_or(""); - let full_path = if base.is_empty() { - name.to_string() - } else { - format!("{}/{}", base, name) - }; - - if !self.entry_passes_filter(&full_path, opts) { - continue; - } - - let oid = entry.id(); - let obj = match self.repo().find_object(oid, None) { - Ok(o) => o, - Err(e) => { - tracing::warn!( - "archive_skip_missing_object oid={} path={} error={}", - oid, - full_path, - e - ); - continue; - } - }; - - let mode = entry.filemode() as u32; - if obj.kind() == Some(git2::ObjectType::Tree) { - if opts - .max_depth - .map_or(true, |d| full_path.matches('/').count() < d) - { - let sub_tree = self - .repo() - .find_tree(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - self.walk_tar(buf, &sub_tree, &full_path, opts)?; - } - } else { - let blob = match obj.as_blob() { - Some(b) => b, - None => continue, - }; - let content = blob.content(); - let size = content.len() as u64; - - let mut header = [0u8; 512]; - let path_bytes = full_path.as_bytes(); - // tar USTAR format: prefix (≤155) + "/" + name (≤100) = max 255 bytes. - // Split at the last "/" that keeps prefix ≤ 155. Fall back to truncation error. - const NAME_MAX: usize = 100; - const PREFIX_MAX: usize = 155; - if path_bytes.len() <= NAME_MAX { - // Fits directly in name field. - header[..path_bytes.len()].copy_from_slice(path_bytes); - } else if path_bytes.len() <= PREFIX_MAX + 1 + NAME_MAX { - // Find last "/" that leaves prefix ≤ PREFIX_MAX. - let split_at = path_bytes[..path_bytes.len() - NAME_MAX] - .iter() - .rposition(|&b| b == b'/') - .map(|pos| pos + 1) - .unwrap_or(0); - let prefix_len = split_at; - let name_len = path_bytes.len() - split_at; - if prefix_len > PREFIX_MAX || name_len > NAME_MAX { - return Err(GitError::Internal(format!( - "path too long for tar format: {}", - full_path - ))); - } - header[..prefix_len].copy_from_slice(&path_bytes[..prefix_len]); - header[prefix_len..prefix_len + 1].copy_from_slice(b"/"); - header[prefix_len + 1..prefix_len + 1 + name_len] - .copy_from_slice(&path_bytes[prefix_len..]); - } else { - return Err(GitError::Internal(format!( - "path too long for tar format: {}", - full_path - ))); - } - let mode_octal = format!("{:o}", mode & 0o777); - header[100..108].copy_from_slice(mode_octal.as_bytes()); - let size_octal = format!("{:o}", size); - if size_octal.len() > 12 { - return Err(GitError::Internal(format!( - "file size {} exceeds maximum for tar format (12-byte octal field)", - size - ))); - } - header[124..136].copy_from_slice(size_octal.as_bytes()); - header[136..148].copy_from_slice(b"0 "); - header[148..156].copy_from_slice(b" "); - header[156] = b'0'; - header[257..265].copy_from_slice(b"ustar\0"); - - // Calculate checksum: sum all 512 bytes with checksum field filled with spaces. - let sum: u32 = header.iter().map(|&b| b as u32).sum::(); - // tar spec: 8-byte checksum field, formatted as 6 octal digits + space + null. - let sum_octal = format!("{:06o} \0", sum); - header[148..156].copy_from_slice(sum_octal.as_bytes()); - - buf.write_all(&header) - .map_err(|e| GitError::IoError(e.to_string()))?; - buf.write_all(content) - .map_err(|e| GitError::IoError(e.to_string()))?; - let written = 512 + content.len(); - let padding = (512 - written % 512) % 512; - if padding > 0 { - buf.write_all(&vec![0u8; padding]) - .map_err(|e| GitError::IoError(e.to_string()))?; - } - } - } - Ok(()) - } - - fn walk_tar_builder( - &self, - builder: &mut tar::Builder>>, - tree: &git2::Tree<'_>, - base: &str, - opts: &ArchiveOptions, - ) -> GitResult<()> { - for entry in tree.iter() { - let name = entry.name().unwrap_or(""); - let full_path = if base.is_empty() { - name.to_string() - } else { - format!("{}/{}", base, name) - }; - - if !self.entry_passes_filter(&full_path, opts) { - continue; - } - - let oid = entry.id(); - let obj = match self.repo().find_object(oid, None) { - Ok(o) => o, - Err(e) => { - tracing::warn!( - "archive_skip_missing_object oid={} path={} error={}", - oid, - full_path, - e - ); - continue; - } - }; - - let mode = entry.filemode() as u32; - if obj.kind() == Some(git2::ObjectType::Tree) { - if opts - .max_depth - .map_or(true, |d| full_path.matches('/').count() < d) - { - let sub_tree = self - .repo() - .find_tree(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - self.walk_tar_builder(builder, &sub_tree, &full_path, opts)?; - } - } else { - let blob = match obj.as_blob() { - Some(b) => b, - None => continue, - }; - let content = blob.content(); - - let mut header = tar::Header::new_gnu(); - header - .set_path(&full_path) - .map_err(|e| GitError::Internal(e.to_string()))?; - header.set_size(content.len() as u64); - header.set_mode(mode & 0o777); - header.set_cksum(); - - builder - .append(&header, content) - .map_err(|e| GitError::Internal(e.to_string()))?; - } - } - Ok(()) - } - - fn walk_zip( - &self, - zip_buf: &mut Vec, - tree: &git2::Tree<'_>, - base: &str, - opts: &ArchiveOptions, - ) -> GitResult<()> { - let cursor = Cursor::new(zip_buf); - let mut zip = zip::ZipWriter::new(cursor); - zip = self.walk_zip_impl(zip, tree, base, opts)?; - let _cursor = zip - .finish() - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(()) - } - - fn walk_zip_impl<'a>( - &'a self, - mut zip: zip::ZipWriter>>, - tree: &git2::Tree<'_>, - base: &str, - opts: &ArchiveOptions, - ) -> GitResult>>> { - for entry in tree.iter() { - let name = entry.name().unwrap_or(""); - let full_path = if base.is_empty() { - name.to_string() - } else { - format!("{}/{}", base, name) - }; - - if !self.entry_passes_filter(&full_path, opts) { - continue; - } - - let oid = entry.id(); - let obj = match self.repo().find_object(oid, None) { - Ok(o) => o, - Err(e) => { - tracing::warn!( - "archive_skip_missing_object oid={} path={} error={}", - oid, - full_path, - e - ); - continue; - } - }; - - let mode = entry.filemode() as u32; - if obj.kind() == Some(git2::ObjectType::Tree) { - if opts - .max_depth - .map_or(true, |d| full_path.matches('/').count() < d) - { - let sub_tree = self - .repo() - .find_tree(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - zip = self.walk_zip_impl(zip, &sub_tree, &full_path, opts)?; - } - } else { - let blob = match obj.as_blob() { - Some(b) => b, - None => continue, - }; - let content = blob.content(); - let options = zip::write::SimpleFileOptions::default() - .compression_method(zip::CompressionMethod::Deflated) - .unix_permissions(mode & 0o777); - - zip.start_file(&full_path, options) - .map_err(|e| GitError::Internal(e.to_string()))?; - zip.write_all(content) - .map_err(|e| GitError::Internal(e.to_string()))?; - } - } - Ok(zip) - } - - fn collect_tree_entries( - &self, - entries: &mut Vec, - tree: &git2::Tree<'_>, - prefix: &str, - depth: usize, - opts: &ArchiveOptions, - ) -> GitResult<()> { - for entry in tree.iter() { - let name = entry.name().unwrap_or(""); - let full_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - - if !self.entry_passes_filter(&full_path, opts) { - continue; - } - - if opts.max_depth.map_or(false, |d| depth > d) { - continue; - } - - let oid = entry.id(); - let obj = match self.repo().find_object(oid, None) { - Ok(o) => o, - Err(e) => { - tracing::warn!( - "archive_list_skip_missing_object oid={} path={} error={}", - oid, - full_path, - e - ); - continue; - } - }; - - let mode = entry.filemode() as u32; - let size = obj.as_blob().map(|b| b.size() as u64).unwrap_or(0); - if obj.kind() == Some(git2::ObjectType::Tree) { - let sub_tree = self - .repo() - .find_tree(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - self.collect_tree_entries(entries, &sub_tree, &full_path, depth + 1, opts)?; - } else { - entries.push(ArchiveEntry { - path: full_path, - oid: oid.to_string(), - size, - mode, - }); - } - } - Ok(()) - } - - fn entry_passes_filter(&self, full_path: &str, opts: &ArchiveOptions) -> bool { - if let Some(ref filter) = opts.path_filter { - if !full_path.starts_with(filter) { - return false; - } - } - true - } -} diff --git a/libs/git/archive/types.rs b/libs/git/archive/types.rs deleted file mode 100644 index 7bcb8d1..0000000 --- a/libs/git/archive/types.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Serializable types for the archive domain. - -use serde::{Deserialize, Serialize}; - -/// The format of the archive. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ArchiveFormat { - Tar, - TarGz, - Zip, -} - -/// Metadata for a single file in the archive. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ArchiveEntry { - pub path: String, - pub oid: String, - pub size: u64, - pub mode: u32, -} - -/// Summary of an archive. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ArchiveSummary { - pub commit_oid: String, - pub format: ArchiveFormat, - pub total_entries: usize, - pub total_size: u64, -} - -/// Options for archive generation. -#[derive(Debug, Clone, Default)] -pub struct ArchiveOptions { - /// Prefix path to prepend to all entries (e.g. "project-name/"). - pub prefix: Option, - /// Maximum directory depth to recurse into. - pub max_depth: Option, - /// Include only entries under this path prefix. - pub path_filter: Option, -} - -impl ArchiveOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn prefix(mut self, p: &str) -> Self { - self.prefix = Some(p.to_string()); - self - } - - pub fn max_depth(mut self, d: usize) -> Self { - self.max_depth = Some(d); - self - } - - pub fn path_filter(mut self, p: &str) -> Self { - self.path_filter = Some(p.to_string()); - self - } - - /// Two Option sets with the same values produce the same key. - pub(crate) fn cache_key(&self) -> String { - let prefix = self.prefix.as_deref().unwrap_or(""); - let filter = self.path_filter.as_deref().unwrap_or(""); - let depth = self.max_depth.map_or("0".to_string(), |d| d.to_string()); - if prefix.is_empty() && filter.is_empty() && self.max_depth.is_none() { - String::new() - } else { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - let mut h = DefaultHasher::new(); - (prefix, filter, depth).hash(&mut h); - format!("-{:x}", h.finish()) - } - } -} diff --git a/libs/git/blame/mod.rs b/libs/git/blame/mod.rs deleted file mode 100644 index 02718c8..0000000 --- a/libs/git/blame/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Blame domain — per-line commit attribution (git blame). -pub mod ops; diff --git a/libs/git/blame/ops.rs b/libs/git/blame/ops.rs deleted file mode 100644 index ad67a51..0000000 --- a/libs/git/blame/ops.rs +++ /dev/null @@ -1,265 +0,0 @@ -//! Blame operations. - -use crate::commit::types::{CommitBlameHunk, CommitBlameLine, CommitOid}; -use crate::{GitDomain, GitError, GitResult}; - -/// Options for blame operations. -#[derive(Debug, Clone, Default)] -pub struct BlameOptions { - pub min_line: Option, - pub max_line: Option, - pub track_copies_same_file: bool, - pub track_copies_same_commit_moves: bool, - pub ignore_whitespace: bool, -} - -impl BlameOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn min_line(mut self, line: usize) -> Self { - self.min_line = Some(line); - self - } - - pub fn max_line(mut self, line: usize) -> Self { - self.max_line = Some(line); - self - } - - pub fn track_copies_same_file(mut self) -> Self { - self.track_copies_same_file = true; - self - } - - pub fn track_copies_same_commit_moves(mut self) -> Self { - self.track_copies_same_commit_moves = true; - self - } - - pub fn ignore_whitespace(mut self) -> Self { - self.ignore_whitespace = true; - self - } - - fn apply_to(&self, opts: &mut git2::BlameOptions) { - if let Some(min) = self.min_line { - opts.min_line(min); - } - if let Some(max) = self.max_line { - opts.max_line(max); - } - if self.track_copies_same_file { - opts.track_copies_same_file(true); - } - if self.track_copies_same_commit_moves { - opts.track_copies_same_commit_moves(true); - } - if self.ignore_whitespace { - opts.ignore_whitespace(true); - } - } -} - -impl GitDomain { - /// Blame a file. Note: git2's `blame_file` always operates on HEAD, - /// not an arbitrary commit. The `commit_oid` parameter is only - /// validated for existence; blame results reflect the current HEAD. - pub fn blame_file( - &self, - commit_oid: &CommitOid, - path: &str, - opts: Option, - ) -> GitResult> { - let oid = commit_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(commit_oid.to_string()))?; - - let commit = self - .repo() - .find_commit(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let _tree = self - .repo() - .find_tree(commit.tree_id()) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut blame_opts = git2::BlameOptions::new(); - if let Some(ref o) = opts { - o.apply_to(&mut blame_opts); - } - - let blame = self - .repo() - .blame_file(std::path::Path::new(path), Some(&mut blame_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Use get_index to iterate hunks - let num_hunks = blame.len(); - let mut hunks: Vec = Vec::with_capacity(num_hunks); - - for i in 0..num_hunks { - if let Some(hunk) = blame.get_index(i) { - hunks.push(CommitBlameHunk { - commit_oid: CommitOid::from_git2(hunk.orig_commit_id()), - final_start_line: hunk.final_start_line() as u32, - final_lines: hunk.lines_in_hunk() as u32, - orig_start_line: hunk.orig_start_line() as u32, - // NOTE: git2 0.20.4 BlameHunk does not expose the original hunk line count. - // orig_lines_in_hunk() is not available, so we reuse lines_in_hunk(). - // The caller should prefer final_lines and orig_start_line for accuracy. - orig_lines: hunk.lines_in_hunk() as u32, - boundary: hunk.is_boundary(), - orig_path: hunk.path().map(|p| p.to_string_lossy().to_string()), - }); - } - } - - Ok(hunks) - } - - /// Alias for blame_file. - pub fn blame_path( - &self, - commit_oid: &CommitOid, - path: &str, - opts: Option, - ) -> GitResult> { - self.blame_file(commit_oid, path, opts) - } - - /// Reconstructs line attribution from hunk ranges. - pub fn blame_lines( - &self, - commit_oid: &CommitOid, - path: &str, - opts: Option, - ) -> GitResult> { - let oid = commit_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(commit_oid.to_string()))?; - - let commit = self - .repo() - .find_commit(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut blame_opts = git2::BlameOptions::new(); - if let Some(ref o) = opts { - o.apply_to(&mut blame_opts); - } - - let blame = self - .repo() - .blame_file(std::path::Path::new(path), Some(&mut blame_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Get file content for line text - let tree = self - .repo() - .find_tree(commit.tree_id()) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let content_lines: Vec = String::from_utf8_lossy( - self.repo() - .find_blob( - tree.get_path(std::path::Path::new(path)) - .map_err(|_| { - GitError::ObjectNotFound(format!("file not found in commit: {}", path)) - })? - .id(), - ) - .map_err(|e| GitError::Internal(e.to_string()))? - .content(), - ) - .lines() - .map(String::from) - .collect(); - - // Collect hunks - let hunks: Vec<_> = (0..blame.len()) - .filter_map(|i| blame.get_index(i)) - .map(|h| { - ( - h.orig_commit_id(), - h.final_start_line(), - h.lines_in_hunk(), - h.path().map(|p| p.to_string_lossy().to_string()), - ) - }) - .collect(); - - let mut lines: Vec = Vec::with_capacity(content_lines.len()); - - for (line_idx, content) in content_lines.iter().enumerate() { - let line_no = line_idx as u32; - let hunk = hunks.iter().find(|(_, start, count, _)| { - let end = *start + *count; - line_idx + 1 >= *start && line_idx + 1 < end - }); - - let (commit_oid, orig_path) = hunk - .map(|(oid, _, _, path)| (CommitOid::from_git2(*oid), path.clone())) - .unwrap_or_else(|| { - ( - CommitOid::from_git2(git2::Oid::zero()), - Some(path.to_string()), - ) - }); - - lines.push(CommitBlameLine { - commit_oid, - line_no, - content: content.clone(), - orig_path, - }); - } - - Ok(lines) - } - - pub fn blame_hunk_at( - &self, - commit_oid: &CommitOid, - path: &str, - line_no: usize, - ) -> GitResult { - // Validate commit exists (git2 blame_file always operates on HEAD, - // so we validate commit for early error rather than semantic correctness). - let oid = commit_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(commit_oid.to_string()))?; - let _commit = self - .repo() - .find_commit(oid) - .map_err(|e| GitError::ObjectNotFound(e.to_string()))?; - let mut blame_opts = git2::BlameOptions::new(); - let blame = self - .repo() - .blame_file(std::path::Path::new(path), Some(&mut blame_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // get_line expects 1-based line numbers; caller provides 0-based. - let hunk_opt = blame.get_line(line_no + 1); - - match hunk_opt { - Some(hunk) => Ok(CommitBlameHunk { - commit_oid: CommitOid::from_git2(hunk.orig_commit_id()), - final_start_line: hunk.final_start_line() as u32, - final_lines: hunk.lines_in_hunk() as u32, - orig_start_line: hunk.orig_start_line() as u32, - // NOTE: git2 0.20.4 BlameHunk does not expose the original hunk line count. - // orig_lines_in_hunk() is not available, so we reuse lines_in_hunk(). - orig_lines: hunk.lines_in_hunk() as u32, - boundary: hunk.is_boundary(), - orig_path: hunk.path().map(|p| p.to_string_lossy().to_string()), - }), - None => Err(GitError::Internal(format!( - "no blame hunk found for line {}", - line_no - ))), - } - } -} diff --git a/libs/git/blob/mod.rs b/libs/git/blob/mod.rs deleted file mode 100644 index 990d3d2..0000000 --- a/libs/git/blob/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Blob domain — all blob-related operations on a GitDomain. -pub mod ops; -pub mod types; diff --git a/libs/git/blob/ops.rs b/libs/git/blob/ops.rs deleted file mode 100644 index c6ed2e2..0000000 --- a/libs/git/blob/ops.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Blob operations. - -use std::path::Path; - -use crate::blob::types::{BlobContent, BlobInfo}; -use crate::commit::types::CommitOid; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn blob_get(&self, oid: &CommitOid) -> GitResult { - let oid = oid - .to_oid() - .map_err(|_| GitError::InvalidOid(oid.to_string()))?; - - let blob = self - .repo() - .find_blob(oid) - .map_err(|_| GitError::ObjectNotFound(oid.to_string()))?; - - Ok(BlobInfo::from_git2(&blob)) - } - - pub fn blob_exists(&self, oid: &CommitOid) -> bool { - oid.to_oid() - .ok() - .and_then(|oid| self.repo.find_blob(oid).ok()) - .is_some() - } - - pub fn blob_is_binary(&self, oid: &CommitOid) -> GitResult { - let oid = oid - .to_oid() - .map_err(|_| GitError::InvalidOid(oid.to_string()))?; - - let blob = self - .repo() - .find_blob(oid) - .map_err(|_| GitError::ObjectNotFound(oid.to_string()))?; - - Ok(blob.is_binary()) - } - - pub fn blob_content(&self, oid: &CommitOid) -> GitResult { - let oid = oid - .to_oid() - .map_err(|_| GitError::InvalidOid(oid.to_string()))?; - - let blob = self - .repo() - .find_blob(oid) - .map_err(|_| GitError::ObjectNotFound(oid.to_string()))?; - - Ok(BlobContent::from_git2(&blob)) - } - - pub fn blob_size(&self, oid: &CommitOid) -> GitResult { - let info = self.blob_get(oid)?; - Ok(info.size) - } - - pub fn blob_create(&self, data: &[u8]) -> GitResult { - let oid = self - .repo() - .blob(data) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitOid::from_git2(oid)) - } - - pub fn blob_create_from_path(&self, path: &Path) -> GitResult { - let oid = self - .repo() - .blob_path(path) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitOid::from_git2(oid)) - } - - pub fn blob_create_from_string(&self, content: &str) -> GitResult { - self.blob_create(content.as_bytes()) - } -} diff --git a/libs/git/blob/types.rs b/libs/git/blob/types.rs deleted file mode 100644 index 6d93ea9..0000000 --- a/libs/git/blob/types.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Serializable types for the blob domain. - -use serde::{Deserialize, Serialize}; - -use crate::commit::types::CommitOid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BlobInfo { - pub oid: CommitOid, - pub size: usize, - pub is_binary: bool, -} - -impl BlobInfo { - pub fn from_git2(blob: &git2::Blob<'_>) -> Self { - Self { - oid: CommitOid::from_git2(blob.id()), - size: blob.size(), - is_binary: blob.is_binary(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BlobContent { - pub oid: CommitOid, - pub size: usize, - pub is_binary: bool, - pub content: Vec, -} - -impl BlobContent { - pub fn from_git2(blob: &git2::Blob<'_>) -> Self { - Self { - oid: CommitOid::from_git2(blob.id()), - size: blob.size(), - is_binary: blob.is_binary(), - content: blob.content().to_vec(), - } - } -} diff --git a/libs/git/branch/merge.rs b/libs/git/branch/merge.rs deleted file mode 100644 index 3914f56..0000000 --- a/libs/git/branch/merge.rs +++ /dev/null @@ -1,150 +0,0 @@ -//! Branch merge operations. - -use crate::commit::types::CommitOid; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn branch_is_merged(&self, branch: &str, into: &str) -> GitResult { - let branch_oid = self - .branch_target(branch)? - .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch)))? - .to_oid()?; - let into_oid = self - .branch_target(into)? - .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", into)))? - .to_oid()?; - - self.repo - .graph_ahead_behind(into_oid, branch_oid) - .map(|(ahead, _)| ahead == 0) - .map_err(|e| GitError::Internal(e.to_string())) - } - - pub fn branch_merge_base(&self, branch1: &str, branch2: &str) -> GitResult { - let oid1 = self - .branch_target(branch1)? - .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch1)))? - .to_oid()?; - let oid2 = self - .branch_target(branch2)? - .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", branch2)))? - .to_oid()?; - - let base = self - .repo() - .merge_base(oid1, oid2) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(CommitOid::from_git2(base)) - } - - pub fn branch_is_ancestor(&self, child: &str, ancestor: &str) -> GitResult { - let child_oid = self - .branch_target(child)? - .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", child)))? - .to_oid()?; - let ancestor_oid = self - .branch_target(ancestor)? - .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", ancestor)))? - .to_oid()?; - - self.repo - .graph_ahead_behind(child_oid, ancestor_oid) - .map(|(_, behind)| behind == 0) - .map_err(|e| GitError::Internal(e.to_string())) - } - - pub fn branch_fast_forward(&self, target: &str, force: bool) -> GitResult { - let target_oid = self - .branch_target(target)? - .ok_or_else(|| GitError::InvalidOid(format!("branch {} has no target", target)))? - .to_oid()?; - - let head = self - .repo() - .head() - .ok() - .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?; - let head_oid = head - .target() - .ok_or_else(|| GitError::Internal("HEAD has no target".to_string()))?; - let head_name = head - .name() - .ok_or_else(|| GitError::Internal("HEAD has no name".to_string()))?; - - let ref_name = if head_name.starts_with("refs/") { - head_name.to_string() - } else { - format!("refs/heads/{}", head_name) - }; - - let (ahead, behind) = self - .repo() - .graph_ahead_behind(head_oid, target_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - if behind == 0 { - return Ok(CommitOid::from_git2(head_oid)); - } - - if !force && ahead > 0 { - // ahead > 0 && behind > 0 means diverged; ahead == 0 && behind > 0 is valid FF - return Err(GitError::Internal( - "not a fast-forward: branches have diverged".to_string(), - )); - } - - self.repo - .reference_matching( - ref_name.as_str(), - target_oid, - true, - head_oid, - "fast-forward", - ) - .map_err(|e| GitError::Internal(e.to_string()))?; - - self.repo - .set_head_detached(target_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - self.repo - .checkout_tree(self.repo.find_commit(target_oid)?.as_object(), None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(CommitOid::from_git2(target_oid)) - } - - pub fn branch_abort_merge(&self) -> GitResult<()> { - let head_oid = self - .repo() - .head() - .ok() - .and_then(|r| r.target()) - .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?; - - let obj = self - .repo() - .find_object(head_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - self.repo - .reset(&obj, git2::ResetType::Hard, None) - .map_err(|e| GitError::Internal(e.to_string())) - } - - pub fn branch_is_conflicted(&self) -> bool { - self.repo - .index() - .map(|idx| idx.has_conflicts()) - .unwrap_or(false) - } - - pub fn branch_tracking_difference(&self, name: &str) -> GitResult<(usize, usize)> { - let upstream = self.branch_upstream(name)?; - match upstream { - Some(u) => self.branch_ahead_behind(name, &u.name), - None => Ok((0, 0)), - } - } -} diff --git a/libs/git/branch/mod.rs b/libs/git/branch/mod.rs deleted file mode 100644 index 80384d2..0000000 --- a/libs/git/branch/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Branch domain — all branch-related operations on a GitDomain. -pub mod merge; -pub mod ops; -pub mod query; -pub mod types; diff --git a/libs/git/branch/ops.rs b/libs/git/branch/ops.rs deleted file mode 100644 index 6d9f900..0000000 --- a/libs/git/branch/ops.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! Branch create/delete/rename operations. - -use git2::BranchType; - -use crate::branch::types::BranchInfo; -use crate::commit::types::CommitOid; -use crate::ref_utils::validate_ref_name; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn branch_create(&self, name: &str, oid: &CommitOid, force: bool) -> GitResult { - validate_ref_name(name)?; - - let target = oid - .to_oid() - .map_err(|_| GitError::InvalidOid(oid.to_string()))?; - - let commit = self - .repo() - .find_commit(target) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let branch = self.repo.branch(name, &commit, force).map_err(|e| { - if e.code() == git2::ErrorCode::Exists && !force { - GitError::BranchExists(name.to_string()) - } else { - GitError::Internal(e.to_string()) - } - })?; - - let full_name = branch.get().name().unwrap_or("").to_string(); - - Ok(BranchInfo { - name: full_name, - oid: CommitOid::from_git2(target), - is_head: false, - is_remote: false, - is_current: false, - upstream: None, - }) - } - - pub fn branch_create_from_head(&self, name: &str, force: bool) -> GitResult { - let head_oid = self - .repo() - .head() - .ok() - .and_then(|r| r.target()) - .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?; - - self.branch_create(name, &CommitOid::from_git2(head_oid), force) - } - - pub fn branch_delete(&self, name: &str) -> GitResult<()> { - let full_name = if name.starts_with("refs/heads/") { - name.to_string() - } else { - format!("refs/heads/{}", name) - }; - - let mut branch = self - .repo() - .find_branch(&full_name, BranchType::Local) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - branch - .delete() - .map_err(|e| GitError::Internal(e.to_string())) - } - - pub fn branch_delete_remote(&self, name: &str) -> GitResult<()> { - let full_name = format!("refs/remotes/{}", name); - - let mut branch = self - .repo() - .find_branch(&full_name, BranchType::Local) - .map_err(|_e| GitError::RefNotFound(full_name.clone()))?; - - branch - .delete() - .map_err(|e| GitError::Internal(e.to_string())) - } - - pub fn branch_rename(&self, old_name: &str, new_name: &str) -> GitResult { - validate_ref_name(new_name)?; - - let old_full = if old_name.starts_with("refs/heads/") { - old_name.to_string() - } else { - format!("refs/heads/{}", old_name) - }; - - let mut branch = self - .repo() - .find_branch(&old_full, BranchType::Local) - .map_err(|_e| GitError::RefNotFound(old_name.to_string()))?; - - let target = branch - .get() - .target() - .ok_or_else(|| GitError::Internal("branch has no target".to_string()))?; - - branch.rename(new_name, false).map_err(|e| { - if e.code() == git2::ErrorCode::Exists { - GitError::BranchExists(new_name.to_string()) - } else { - GitError::Internal(e.to_string()) - } - })?; - - Ok(BranchInfo { - name: format!("refs/heads/{}", new_name), - oid: CommitOid::from_git2(target), - is_head: false, - is_remote: false, - is_current: false, - upstream: None, - }) - } - - pub fn branch_move(&self, name: &str, new_name: &str, force: bool) -> GitResult { - validate_ref_name(new_name)?; - - let full_name = if name.starts_with("refs/heads/") { - name.to_string() - } else { - format!("refs/heads/{}", name) - }; - - let mut branch = self - .repo() - .find_branch(&full_name, BranchType::Local) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - let target = branch - .get() - .target() - .ok_or_else(|| GitError::Internal("branch has no target".to_string()))?; - - let commit = self - .repo() - .find_commit(target) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Delete the old branch first. If deletion fails, we fail atomically. - branch - .delete() - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Create the new branch pointing to the same commit. - self.repo().branch(new_name, &commit, force).map_err(|e| { - if e.code() == git2::ErrorCode::Exists && !force { - GitError::BranchExists(new_name.to_string()) - } else { - GitError::Internal(e.to_string()) - } - })?; - - Ok(BranchInfo { - name: format!("refs/heads/{}", new_name), - oid: CommitOid::from_git2(target), - is_head: false, - is_remote: false, - is_current: false, - upstream: None, - }) - } - - pub fn branch_set_upstream(&self, name: &str, upstream: Option<&str>) -> GitResult<()> { - let full_name = if name.starts_with("refs/heads/") { - name.to_string() - } else { - format!("refs/heads/{}", name) - }; - - let mut branch = self - .repo() - .find_branch(&full_name, BranchType::Local) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - match upstream { - Some(u) => { - let upstream_name = if u.starts_with("refs/remotes/") || u.contains('/') { - u.to_string() - } else { - format!("refs/remotes/{}", u) - }; - - branch - .set_upstream(Some(&upstream_name)) - .map_err(|e| GitError::Internal(e.to_string())) - } - None => branch - .set_upstream(None) - .map_err(|e| GitError::Internal(e.to_string())), - } - } -} diff --git a/libs/git/branch/query.rs b/libs/git/branch/query.rs deleted file mode 100644 index 7912fc1..0000000 --- a/libs/git/branch/query.rs +++ /dev/null @@ -1,280 +0,0 @@ -//! Branch querying operations. - -use git2::BranchType; - -use crate::branch::types::{BranchDiff, BranchInfo, BranchSummary}; -use crate::commit::types::CommitOid; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn branch_list(&self, remote_only: bool) -> GitResult> { - let branch_type = if remote_only { - BranchType::Remote - } else { - BranchType::Local - }; - - let mut branches = Vec::with_capacity(16); - // Keep head_name as full ref for comparison with branch names - let head_name = self - .repo - .head() - .ok() - .and_then(|r| r.name().map(String::from)); - - for branch_result in self - .repo() - .branches(Some(branch_type)) - .map_err(|e| GitError::Internal(e.to_string()))? - { - let (branch, _) = branch_result.map_err(|e| GitError::Internal(e.to_string()))?; - if let Some(name) = branch.name().ok().flatten() { - let Some(target) = branch.get().target() else { - continue; // skip branches without a target - }; - let name = name.to_string(); - let oid = CommitOid::from_git2(target); - // Compare full ref names (e.g., "refs/heads/main" == "refs/heads/main") - let is_head = head_name.as_ref().map_or(false, |h| h == &name); - let is_current = branch.is_head(); - - branches.push(BranchInfo { - name, - oid, - is_head, - is_remote: remote_only, - is_current, - upstream: None, - }); - } - } - - Ok(branches) - } - - pub fn branch_list_local(&self) -> GitResult> { - self.branch_list(false) - } - - pub fn branch_list_remote(&self) -> GitResult> { - self.branch_list(true) - } - - pub fn branch_list_all(&self) -> GitResult> { - let mut all = self.branch_list(false)?; - let remote = self.branch_list(true)?; - for mut r in remote { - r.is_remote = true; - all.push(r); - } - Ok(all) - } - - pub fn branch_summary(&self) -> GitResult { - let local = self.branch_list(false)?; - let remote = self.branch_list(true)?; - Ok(BranchSummary { - local_count: local.len(), - remote_count: remote.len(), - all_count: local.len() + remote.len(), - }) - } - - pub fn branch_get(&self, name: &str) -> GitResult { - // Determine candidates: try full refs as-is, then construct refs/heads/ and refs/remotes/ - let candidates: Vec = if name.starts_with("refs/") { - vec![name.to_string()] - } else { - vec![ - format!("refs/heads/{}", name), - format!("refs/remotes/{}", name), - ] - }; - - // Try local branch first, then remote - let branch = candidates - .iter() - .find_map(|full_name| { - self.repo - .find_branch(full_name, git2::BranchType::Local) - .ok() - .or_else(|| { - self.repo - .find_branch(full_name, git2::BranchType::Remote) - .ok() - }) - }) - .ok_or_else(|| GitError::RefNotFound(name.to_string()))?; - - let full_name = branch.name().ok().flatten().unwrap_or_default().to_string(); - let target = branch - .get() - .target() - .ok_or_else(|| GitError::Internal("branch has no target".to_string()))?; - let oid = CommitOid::from_git2(target); - let head_name = self - .repo() - .head() - .ok() - .and_then(|r| r.name().map(String::from)); - let branch_name = branch.name().ok().flatten().unwrap_or_default(); - - Ok(BranchInfo { - name: branch_name.to_string(), - oid, - is_head: head_name.as_ref().map_or(false, |h| h == &full_name), - is_remote: full_name.starts_with("refs/remotes/"), - is_current: branch.is_head(), - upstream: branch.upstream().ok().map(|u| { - u.name() - .ok() - .and_then(|n| n) - .unwrap_or_default() - .to_string() - }), - }) - } - - pub fn branch_exists(&self, name: &str) -> bool { - // Same candidate logic as branch_get - let candidates: Vec = if name.starts_with("refs/") { - vec![name.to_string()] - } else { - vec![ - format!("refs/heads/{}", name), - format!("refs/remotes/{}", name), - ] - }; - - candidates.iter().any(|full_name| { - self.repo.find_branch(full_name, BranchType::Local).is_ok() - || self - .repo() - .find_branch(full_name, BranchType::Remote) - .is_ok() - }) - } - - pub fn branch_is_head(&self, name: &str) -> GitResult { - let info = self.branch_get(name)?; - Ok(info.is_head) - } - - pub fn branch_current(&self) -> GitResult> { - let head = self - .repo() - .head() - .map_err(|e| GitError::Internal(e.to_string()))?; - - if let Some(name) = head.name() { - let name = name.to_string(); - if name.starts_with("refs/heads/") { - return Ok(Some(self.branch_get(&name)?)); - } - } - - Ok(None) - } - - pub fn branch_target(&self, name: &str) -> GitResult> { - let info = self.branch_get(name)?; - if info.oid.0.is_empty() { - Ok(None) - } else { - Ok(Some(info.oid)) - } - } - - pub fn branch_upstream(&self, name: &str) -> GitResult> { - let full_name = if name.starts_with("refs/heads/") { - name.to_string() - } else { - format!("refs/heads/{}", name) - }; - - let branch = self - .repo() - .find_branch(&full_name, BranchType::Local) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - match branch.upstream() { - Ok(up) => { - let up_target = up.get().target().ok_or_else(|| { - GitError::Internal("upstream branch has no target".to_string()) - })?; - Ok(Some(BranchInfo { - name: up - .name() - .ok() - .and_then(|n| n) - .unwrap_or_default() - .to_string(), - oid: CommitOid::from_git2(up_target), - is_head: false, - is_remote: true, - is_current: false, - upstream: None, - })) - } - Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None), - Err(e) => Err(GitError::Internal(e.to_string())), - } - } - - pub fn branch_upstream_name(&self, name: &str) -> GitResult> { - let upstream = self.branch_upstream(name)?; - Ok(upstream.map(|u| u.name)) - } - - pub fn branch_has_upstream(&self, name: &str) -> GitResult { - let full_name = if name.starts_with("refs/heads/") { - name.to_string() - } else { - format!("refs/heads/{}", name) - }; - - let branch = self - .repo() - .find_branch(&full_name, BranchType::Local) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - Ok(branch.upstream().is_ok()) - } - - pub fn branch_is_detached(&self) -> bool { - self.repo.head().map_or(false, |h| !h.is_branch()) - } - - pub fn branch_diff(&self, local: &str, remote: &str) -> GitResult { - let local_oid = self.branch_target(local)?; - let remote_oid = self.branch_target(remote)?; - - match (local_oid, remote_oid) { - (Some(l), Some(r)) => { - let l_oid = l.to_oid()?; - let r_oid = r.to_oid()?; - - let (ahead, behind) = self - .repo() - .graph_ahead_behind(l_oid, r_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(BranchDiff { - ahead, - behind, - diverged: ahead > 0 && behind > 0, - }) - } - _ => Ok(BranchDiff { - ahead: 0, - behind: 0, - diverged: false, - }), - } - } - - pub fn branch_ahead_behind(&self, local: &str, upstream: &str) -> GitResult<(usize, usize)> { - let diff = self.branch_diff(local, upstream)?; - Ok((diff.ahead, diff.behind)) - } -} diff --git a/libs/git/branch/types.rs b/libs/git/branch/types.rs deleted file mode 100644 index 2587e1f..0000000 --- a/libs/git/branch/types.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Serializable types for the branch domain. - -use serde::{Deserialize, Serialize}; - -use crate::commit::types::CommitOid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BranchInfo { - pub name: String, - pub oid: CommitOid, - pub is_head: bool, - pub is_remote: bool, - pub is_current: bool, - pub upstream: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BranchSummary { - pub local_count: usize, - pub remote_count: usize, - pub all_count: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BranchDiff { - pub ahead: usize, - pub behind: usize, - pub diverged: bool, -} diff --git a/libs/git/commit/cherry_pick.rs b/libs/git/commit/cherry_pick.rs deleted file mode 100644 index 153f0a1..0000000 --- a/libs/git/commit/cherry_pick.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Cherry-pick operations. - -use crate::commit::types::*; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn commit_cherry_pick( - &self, - cherrypick_oid: &CommitOid, - author: &CommitSignature, - committer: &CommitSignature, - message: Option<&str>, - mainline: u32, - update_ref: Option<&str>, - ) -> GitResult { - let cherrypick_commit = self - .repo() - .find_commit(cherrypick_oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(cherrypick_oid.to_string()))?; - - let head_oid = self - .repo() - .head() - .ok() - .and_then(|r| r.target()) - .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?; - let our_commit = self - .repo() - .find_commit(head_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut index = self - .repo() - .cherrypick_commit(&cherrypick_commit, &our_commit, mainline, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let tree_oid = index - .write_tree_to(&*self.repo()) - .map_err(|e| GitError::Internal(e.to_string()))?; - let tree = self - .repo() - .find_tree(tree_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let msg = message.map(String::from).unwrap_or_else(|| { - format!( - "cherry-pick commit {}", - &cherrypick_oid.to_string()[..8.min(cherrypick_oid.to_string().len())] - ) - }); - - let author = self.commit_signature_to_git2(author)?; - let committer = self.commit_signature_to_git2(committer)?; - - let oid = self - .repo() - .commit(update_ref, &author, &committer, &msg, &tree, &[&our_commit]) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(CommitOid::from_git2(oid)) - } - - pub fn commit_cherry_pick_sequence( - &self, - cherrypick_oids: &[CommitOid], - author: &CommitSignature, - committer: &CommitSignature, - update_ref: Option<&str>, - ) -> GitResult { - let mut last_oid: Option = None; - for oid in cherrypick_oids { - last_oid = Some(self.commit_cherry_pick(oid, author, committer, None, 0, update_ref)?); - } - last_oid.ok_or_else(|| GitError::Internal("no commits to cherry-pick".to_string())) - } - - pub fn commit_cherry_pick_abort(&self, reset_type: &str) -> GitResult<()> { - let kind = match reset_type { - "soft" => git2::ResetType::Soft, - "mixed" => git2::ResetType::Mixed, - "hard" => git2::ResetType::Hard, - _ => { - return Err(GitError::Internal(format!( - "unknown reset type: {}", - reset_type - ))); - } - }; - - let head_oid = self - .repo() - .head() - .ok() - .and_then(|r| r.target()) - .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?; - let obj = self - .repo() - .find_object(head_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - self.repo() - .reset(&obj, kind, None) - .map_err(|e| GitError::Internal(e.to_string())) - } -} diff --git a/libs/git/commit/create.rs b/libs/git/commit/create.rs deleted file mode 100644 index 35a36e5..0000000 --- a/libs/git/commit/create.rs +++ /dev/null @@ -1,315 +0,0 @@ -//! Commit creation and modification operations. - -use git2::Signature; - -use crate::commit::types::*; -use crate::{GitDomain, GitError, GitResult}; - -fn parents_to_git2<'a>( - repo: &'a git2::Repository, - parent_ids: &[CommitOid], -) -> GitResult>> { - parent_ids - .iter() - .map(|oid| { - repo.find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string())) - }) - .collect() -} - -impl GitDomain { - pub fn commit_default_signature(&self) -> GitResult { - let sig = self - .repo() - .signature() - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitSignature::from_git2(sig)) - } - - pub fn commit_signature_now(&self, name: &str, email: &str) -> GitResult { - let sig = Signature::now(name, email).map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitSignature::from_git2(sig)) - } - - pub fn commit_signature_at( - &self, - name: &str, - email: &str, - time_secs: i64, - offset_minutes: i32, - ) -> GitResult { - let time = git2::Time::new(time_secs, offset_minutes); - let sig = - Signature::new(name, email, &time).map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitSignature::from_git2(sig)) - } - - pub fn commit_signature_to_git2(&self, sig: &CommitSignature) -> GitResult> { - let time = git2::Time::new(sig.time_secs, sig.offset_minutes); - Signature::new(&sig.name, &sig.email, &time).map_err(|e| GitError::Internal(e.to_string())) - } - - pub fn commit_create( - &self, - update_ref: Option<&str>, - author: &CommitSignature, - committer: &CommitSignature, - message: &str, - tree_id: &CommitOid, - parent_ids: &[CommitOid], - ) -> GitResult { - let author = self.commit_signature_to_git2(author)?; - let committer = self.commit_signature_to_git2(committer)?; - let tree = self - .repo() - .find_tree(tree_id.to_oid()?) - .map_err(|e| GitError::Internal(e.to_string()))?; - let parents = parents_to_git2(&*self.repo(), parent_ids)?; - let parent_refs: Vec<&git2::Commit<'_>> = - parents.iter().map(|p| p as &git2::Commit).collect(); - let oid = self - .repo() - .commit( - update_ref, - &author, - &committer, - message, - &tree, - &parent_refs, - ) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitOid::from_git2(oid)) - } - - pub fn commit_create_from_index( - &self, - update_ref: Option<&str>, - author: &CommitSignature, - committer: &CommitSignature, - message: &str, - parent_ids: &[CommitOid], - ) -> GitResult { - let author = self.commit_signature_to_git2(author)?; - let committer = self.commit_signature_to_git2(committer)?; - let mut index = self - .repo() - .index() - .map_err(|e| GitError::Internal(e.to_string()))?; - let tree_oid = index - .write_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - let tree = self - .repo() - .find_tree(tree_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let parents = parents_to_git2(&*self.repo(), parent_ids)?; - let parent_refs: Vec<&git2::Commit<'_>> = - parents.iter().map(|p| p as &git2::Commit).collect(); - let oid = self - .repo() - .commit( - update_ref, - &author, - &committer, - message, - &tree, - &parent_refs, - ) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitOid::from_git2(oid)) - } - - pub fn commit_sign( - &self, - author: &CommitSignature, - committer: &CommitSignature, - message: &str, - tree_id: &CommitOid, - parent_ids: &[CommitOid], - gpg_signature: &str, - signature_field: Option<&str>, - ) -> GitResult { - let author = self.commit_signature_to_git2(author)?; - let committer = self.commit_signature_to_git2(committer)?; - let tree = self - .repo() - .find_tree(tree_id.to_oid()?) - .map_err(|e| GitError::Internal(e.to_string()))?; - let parents = parents_to_git2(&*self.repo(), parent_ids)?; - let parent_refs: Vec<&git2::Commit<'_>> = - parents.iter().map(|p| p as &git2::Commit).collect(); - let buf = self - .repo() - .commit_create_buffer(&author, &committer, message, &tree, &parent_refs) - .map_err(|e| GitError::Internal(e.to_string()))?; - let buf_str = std::str::from_utf8(&*buf) - .map_err(|e| GitError::Internal(format!("commit buffer is not valid UTF-8: {}", e)))?; - let oid = self - .repo() - .commit_signed(buf_str, gpg_signature, signature_field) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitOid::from_git2(oid)) - } - - pub fn commit_extract_signature( - &self, - commit_oid: &CommitOid, - signature_field: Option<&str>, - ) -> GitResult> { - match self - .repo() - .extract_signature(&commit_oid.to_oid()?, signature_field) - { - Ok((sig_buf, content_buf)) => Ok(Some(( - String::from_utf8_lossy(&*sig_buf).to_string(), - String::from_utf8_lossy(&*content_buf).to_string(), - ))), - Err(e) => { - if e.code() == git2::ErrorCode::NotFound { - Ok(None) - } else { - Err(GitError::Internal(e.to_string())) - } - } - } - } - - pub fn commit_empty( - &self, - update_ref: Option<&str>, - author: &CommitSignature, - committer: &CommitSignature, - message: &str, - parent_ids: &[CommitOid], - ) -> GitResult { - let author = self.commit_signature_to_git2(author)?; - let committer = self.commit_signature_to_git2(committer)?; - let tree = if let Some(first) = parent_ids.first() { - let commit = self - .repo() - .find_commit(first.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(first.to_string()))?; - commit - .tree() - .map_err(|e| GitError::Internal(e.to_string()))? - } else { - let mut index = self - .repo() - .index() - .map_err(|e| GitError::Internal(e.to_string()))?; - let tree_oid = index - .write_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - self.repo() - .find_tree(tree_oid) - .map_err(|e| GitError::Internal(e.to_string()))? - }; - let parents = parents_to_git2(&*self.repo(), parent_ids)?; - let parent_refs: Vec<&git2::Commit<'_>> = - parents.iter().map(|p| p as &git2::Commit).collect(); - let oid = self - .repo() - .commit( - update_ref, - &author, - &committer, - message, - &tree, - &parent_refs, - ) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitOid::from_git2(oid)) - } - - pub fn commit_amend( - &self, - commit_oid: &CommitOid, - update_ref: Option<&str>, - author: Option<&CommitSignature>, - committer: Option<&CommitSignature>, - message_encoding: Option<&str>, - message: Option<&str>, - tree_id: Option<&CommitOid>, - ) -> GitResult { - let commit = self - .repo() - .find_commit(commit_oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(commit_oid.to_string()))?; - let author = author - .map(|a| self.commit_signature_to_git2(a)) - .transpose()?; - let committer = committer - .map(|c| self.commit_signature_to_git2(c)) - .transpose()?; - let tree = tree_id - .map(|t| { - self.repo - .find_tree(t.to_oid()?) - .map_err(|e| GitError::Internal(e.to_string())) - }) - .transpose()?; - let oid = commit - .amend( - update_ref, - author.as_ref(), - committer.as_ref(), - message_encoding, - message, - tree.as_ref(), - ) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitOid::from_git2(oid)) - } - - pub fn commit_amend_author( - &self, - commit_oid: &CommitOid, - new_author: &CommitSignature, - update_ref: Option<&str>, - ) -> GitResult { - self.commit_amend( - commit_oid, - update_ref, - Some(new_author), - None, - None, - None, - None, - ) - } - - pub fn commit_amend_message( - &self, - commit_oid: &CommitOid, - new_message: &str, - update_ref: Option<&str>, - ) -> GitResult { - self.commit_amend( - commit_oid, - update_ref, - None, - None, - None, - Some(new_message), - None, - ) - } - - pub fn commit_amend_tree( - &self, - commit_oid: &CommitOid, - new_tree_id: &CommitOid, - update_ref: Option<&str>, - ) -> GitResult { - self.commit_amend( - commit_oid, - update_ref, - None, - None, - None, - None, - Some(new_tree_id), - ) - } -} diff --git a/libs/git/commit/graph.rs b/libs/git/commit/graph.rs deleted file mode 100644 index 92fa9ed..0000000 --- a/libs/git/commit/graph.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! Visual ASCII commit graph output. - -use serde::{Deserialize, Serialize}; - -use crate::commit::types::*; -use crate::{GitDomain, GitResult}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitGraphLine { - pub oid: CommitOid, - pub graph_chars: String, - pub refs: String, - pub short_message: String, - /// Column index (0-based) where the commit dot is rendered. - /// Used by @gitgraph/react to assign lane color. - pub lane_index: usize, - /// Full commit metadata (author, timestamp, parents, etc.) - pub meta: CommitMeta, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitGraph { - pub lines: Vec, - pub max_parents: usize, -} - -#[derive(Default)] -struct GraphState { - column_to_commit: Vec>, - column_prev_commit: Vec>, - active_columns: usize, -} - -pub struct CommitGraphOptions { - pub rev: Option, - pub limit: usize, - pub first_parent_only: bool, - pub show_refs: bool, -} - -impl Default for CommitGraphOptions { - fn default() -> Self { - Self { - rev: None, - limit: 0, - first_parent_only: false, - show_refs: true, - } - } -} - -impl CommitGraphOptions { - pub fn rev(mut self, rev: &str) -> Self { - self.rev = Some(rev.to_string()); - self - } - - pub fn limit(mut self, n: usize) -> Self { - self.limit = n; - self - } - - pub fn first_parent_only(mut self) -> Self { - self.first_parent_only = true; - self - } - - pub fn no_refs(mut self) -> Self { - self.show_refs = false; - self - } -} - -impl GitDomain { - pub fn commit_graph(&self, opts: CommitGraphOptions) -> GitResult { - let commits = self.commit_walk(crate::commit::traverse::CommitWalkOptions { - rev: opts.rev.map(String::from), - sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), - limit: opts.limit, - first_parent_only: opts.first_parent_only, - })?; - - let mut state = GraphState::default(); - let mut lines = Vec::with_capacity(commits.len()); - let mut max_parents = 0; - - for commit in commits { - max_parents = max_parents.max(commit.parent_ids.len()); - let line = self.build_graph_line(&commit, &mut state, opts.show_refs)?; - lines.push(line); - } - - Ok(CommitGraph { lines, max_parents }) - } - - pub fn commit_graph_simple(&self, rev: Option<&str>, limit: usize) -> GitResult { - let opts = CommitGraphOptions::default().limit(limit); - let opts = match rev { - Some(r) => opts.rev(r), - None => opts, - }; - self.commit_graph(opts) - } - - fn build_graph_line( - &self, - commit: &CommitMeta, - state: &mut GraphState, - show_refs: bool, - ) -> GitResult { - let oid = commit.oid.clone(); - let short_message = commit.summary.clone(); - - let refs = if show_refs { - self.get_commit_refs_string(&oid)? - } else { - String::new() - }; - - let (graph_chars, lane_index) = self.render_graph_chars(commit, state); - - Ok(CommitGraphLine { - oid, - graph_chars, - refs, - short_message, - lane_index, - meta: commit.clone(), - }) - } - - fn get_commit_refs_string(&self, oid: &CommitOid) -> GitResult { - let refs = self.commit_refs(oid)?; - let parts: Vec = refs - .iter() - .map(|r| { - if r.is_tag { - r.name.trim_start_matches("refs/tags/").to_string() - } else { - r.name.trim_start_matches("refs/heads/").to_string() - } - }) - .collect(); - Ok(parts.join(", ")) - } - - fn render_graph_chars(&self, commit: &CommitMeta, state: &mut GraphState) -> (String, usize) { - let current_oid = &commit.oid; - let num_parents = commit.parent_ids.len(); - - let mut result = String::new(); - let active: Vec = state - .column_to_commit - .iter() - .map(|col_oid| col_oid.as_ref().map_or(false, |col| col == current_oid)) - .collect(); - - let current_col = if let Some(pos) = active.iter().position(|a| *a) { - pos - } else { - if state.active_columns == 0 { - state.active_columns = 1; - } - let col = state.column_to_commit.len(); - state.column_to_commit.push(Some(current_oid.clone())); - state.column_prev_commit.push(None); - state.active_columns += 1; - col - }; - - for i in 0..state.column_to_commit.len() { - if i == current_col { - result.push_str("* "); - } else if state.column_to_commit[i].is_some() { - result.push_str("| "); - } else { - result.push_str(" "); - } - } - - if num_parents > 1 { - let available = state - .column_to_commit - .len() - .saturating_sub(state.active_columns); - if available > 0 { - result.push_str(&format!("/{}", " ".repeat(available * 2 - 1))); - } - } - - for col in 0..state.column_to_commit.len() { - if state.column_to_commit[col].as_ref() == Some(current_oid) { - state.column_to_commit[col] = None; - state.active_columns = state.active_columns.saturating_sub(1); - } - } - - for (i, parent) in commit.parent_ids.iter().enumerate() { - if i == 0 && state.active_columns == 0 { - state.column_to_commit[0] = Some(parent.clone()); - state.column_prev_commit[0] = Some(current_oid.clone()); - state.active_columns = 1; - } else { - if let Some(idx) = state.column_to_commit.iter().position(|c| c.is_none()) { - state.column_to_commit[idx] = Some(parent.clone()); - state.column_prev_commit[idx] = Some(current_oid.clone()); - state.active_columns += 1; - } else { - state.column_to_commit.push(Some(parent.clone())); - state.column_prev_commit.push(Some(current_oid.clone())); - state.active_columns += 1; - } - } - } - - (result, current_col) - } -} diff --git a/libs/git/commit/meta.rs b/libs/git/commit/meta.rs deleted file mode 100644 index f785bfe..0000000 --- a/libs/git/commit/meta.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! Commit reference (branch/tag) and reflog operations. - -use std::collections::HashMap; - -use crate::commit::types::*; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn commit_refs(&self, oid: &CommitOid) -> GitResult> { - let _ = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - - let mut refs = Vec::new(); - - for branch_result in self - .repo() - .branches(Some(git2::BranchType::Local)) - .map_err(|e| GitError::Internal(e.to_string()))? - { - let (branch, _) = branch_result.map_err(|e| GitError::Internal(e.to_string()))?; - - if let Some(target) = branch.get().target() { - if target == oid.to_oid()? { - let name = branch - .name() - .map_err(|e| GitError::Internal(e.to_string()))? - .unwrap_or("") - .to_string(); - refs.push(CommitRefInfo { - name, - target: oid.clone(), - is_remote: false, - is_tag: false, - }); - } - } - } - - if let Ok(walk) = self.repo.references() { - for ref_result in walk { - if let Ok(r) = ref_result { - if let Some(name) = r.name() { - if name.starts_with("refs/tags/") { - if let Some(target) = r.target() { - if target == oid.to_oid()? { - refs.push(CommitRefInfo { - name: name.to_string(), - target: oid.clone(), - is_remote: false, - is_tag: true, - }); - } - } - } - } - } - } - } - - Ok(refs) - } - - pub fn commit_branches(&self, oid: &CommitOid) -> GitResult> { - let refs = self.commit_refs(oid)?; - Ok(refs - .into_iter() - .filter(|r| !r.is_remote && !r.is_tag) - .map(|r| r.name) - .collect()) - } - - pub fn commit_tags(&self, oid: &CommitOid) -> GitResult> { - let refs = self.commit_refs(oid)?; - Ok(refs - .into_iter() - .filter(|r| r.is_tag) - .map(|r| r.name) - .collect()) - } - - pub fn commit_is_tip(&self, oid: &CommitOid) -> GitResult { - let refs = self.commit_refs(oid)?; - Ok(!refs.is_empty()) - } - - pub fn commit_is_default_tip(&self, oid: &CommitOid) -> GitResult { - let default = self.repo.head().ok().and_then(|r| r.target()); - Ok(default == Some(oid.to_oid()?)) - } - - pub fn commit_reflog( - &self, - oid: &CommitOid, - refname: Option<&str>, - ) -> GitResult> { - let refname = match refname { - Some(name) => name.to_string(), - None => self - .repo() - .head() - .ok() - .and_then(|r| r.name().map(|n| n.to_owned())) - .ok_or_else(|| GitError::Internal("HEAD has no name".to_string()))?, - }; - - let reflog = self - .repo() - .reflog(&refname) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut entries = Vec::new(); - let oid_val = oid.to_oid()?; - for entry in reflog.iter() { - if entry.id_new() == oid_val || entry.id_old() == oid_val { - let sig = entry.committer(); - entries.push(CommitReflogEntry { - oid_new: CommitOid::from_git2(entry.id_new()), - oid_old: CommitOid::from_git2(entry.id_old()), - committer_name: sig.name().unwrap_or("").to_string(), - committer_email: sig.email().unwrap_or("").to_string(), - time_secs: sig.when().seconds(), - offset_minutes: sig.when().offset_minutes(), - message: entry.message().map(String::from), - ref_name: refname.clone(), - }); - } - } - - Ok(entries) - } - - pub fn commit_ref_count(&self, oid: &CommitOid) -> GitResult { - let refs = self.commit_refs(oid)?; - Ok(refs.len()) - } - - /// Returns all refs (branches + tags) grouped by commit OID. - pub fn refs_grouped(&self) -> GitResult, Vec)>> { - let mut map: HashMap, Vec)> = HashMap::new(); - - for branch_result in self - .repo() - .branches(Some(git2::BranchType::Local)) - .map_err(|e| GitError::Internal(e.to_string()))? - { - let (branch, _) = branch_result.map_err(|e| GitError::Internal(e.to_string()))?; - if let Some(target) = branch.get().target() { - let oid_str = target.to_string(); - let name = branch - .name() - .map_err(|e| GitError::Internal(e.to_string()))? - .unwrap_or("") - .to_string(); - let entry = map - .entry(oid_str) - .or_insert_with(|| (Vec::new(), Vec::new())); - entry.0.push(name); - } - } - - if let Ok(walk) = self.repo.references() { - for ref_result in walk { - if let Ok(r) = ref_result { - if let Some(name) = r.name() { - if name.starts_with("refs/tags/") { - if let Some(target) = r.target() { - let oid_str = target.to_string(); - let full_name = name.to_string(); - let entry = map - .entry(oid_str) - .or_insert_with(|| (Vec::new(), Vec::new())); - entry.1.push(full_name); - } - } - } - } - } - } - - Ok(map) - } - - /// Returns all reflog entries for a given ref (defaults to HEAD). - pub fn reflog_entries(&self, refname: Option<&str>) -> GitResult> { - let refname = match refname { - Some(name) => name.to_string(), - None => self - .repo() - .head() - .ok() - .and_then(|r| r.name().map(|n| n.to_owned())) - .ok_or_else(|| GitError::Internal("HEAD has no name".to_string()))?, - }; - - let reflog = self - .repo() - .reflog(&refname) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut entries = Vec::new(); - for entry in reflog.iter() { - let sig = entry.committer(); - entries.push(CommitReflogEntry { - oid_new: CommitOid::from_git2(entry.id_new()), - oid_old: CommitOid::from_git2(entry.id_old()), - committer_name: sig.name().unwrap_or("").to_string(), - committer_email: sig.email().unwrap_or("").to_string(), - time_secs: sig.when().seconds(), - offset_minutes: sig.when().offset_minutes(), - message: entry.message().map(String::from), - ref_name: refname.clone(), - }); - } - - Ok(entries) - } -} diff --git a/libs/git/commit/mod.rs b/libs/git/commit/mod.rs deleted file mode 100644 index 42e1f1f..0000000 --- a/libs/git/commit/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Commit domain — all commit-related operations on a GitDomain. -pub mod cherry_pick; -pub mod create; -pub mod graph; -pub mod meta; -pub mod query; -pub mod rebase; -pub mod revert; -pub mod traverse; -pub mod types; diff --git a/libs/git/commit/query.rs b/libs/git/commit/query.rs deleted file mode 100644 index c8438e8..0000000 --- a/libs/git/commit/query.rs +++ /dev/null @@ -1,253 +0,0 @@ -//! Commit querying operations. - -use crate::commit::types::*; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn commit_get(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(CommitMeta::from_git2(&commit)) - } - - pub fn commit_get_prefix(&self, prefix: &str) -> GitResult { - let commit = self - .repo() - .find_commit_by_prefix(prefix) - .map_err(|_e| GitError::InvalidOid(format!("prefix: {}", prefix)))?; - Ok(CommitMeta::from_git2(&commit)) - } - - pub fn commit_exists(&self, oid: &CommitOid) -> bool { - match oid.to_oid() { - Ok(oid) => self.repo.find_commit(oid).is_ok(), - Err(_) => false, - } - } - - pub fn commit_is_commit(&self, oid: &CommitOid) -> bool { - match oid.to_oid() { - Ok(oid) => match self.repo.find_object(oid, None) { - Ok(obj) => obj.kind() == Some(git2::ObjectType::Commit), - Err(_) => false, - }, - Err(_) => false, - } - } - - pub fn commit_message(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(commit.message().unwrap_or("").to_string()) - } - - pub fn commit_summary(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(commit.summary().unwrap_or("").to_string()) - } - - pub fn commit_short_id(&self, oid: &CommitOid) -> GitResult { - let _ = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - let take = 7.min(oid.0.len()); - Ok(oid.0[..take].to_string()) - } - - pub fn commit_author(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(CommitSignature::from_git2(commit.author())) - } - - pub fn commit_committer(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(CommitSignature::from_git2(commit.committer())) - } - - pub fn commit_time(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(commit.time().seconds()) - } - - pub fn commit_time_offset(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(commit.time().offset_minutes()) - } - - pub fn commit_encoding(&self, oid: &CommitOid) -> GitResult> { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(commit.message_encoding().map(String::from)) - } - - pub fn commit_tree_id(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(CommitOid::from_git2(commit.tree_id())) - } - - pub fn commit_parent_count(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(commit.parent_count()) - } - - pub fn commit_parent_ids(&self, oid: &CommitOid) -> GitResult> { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(commit.parent_ids().map(CommitOid::from_git2).collect()) - } - - pub fn commit_parent(&self, oid: &CommitOid, index: usize) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - let parent = commit - .parent(index) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(CommitMeta::from_git2(&parent)) - } - - pub fn commit_first_parent(&self, oid: &CommitOid) -> GitResult> { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - if commit.parent_count() > 0 { - let parent = commit - .parent(0) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(Some(CommitMeta::from_git2(&parent))) - } else { - Ok(None) - } - } - - pub fn commit_is_merge(&self, oid: &CommitOid) -> GitResult { - let commit = self - .repo() - .find_commit(oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?; - Ok(commit.parent_count() > 1) - } - - pub fn commit_log( - &self, - rev: Option<&str>, - offset: usize, - limit: usize, - ) -> GitResult> { - let mut revwalk = self - .repo() - .revwalk() - .map_err(|e| GitError::Internal(e.to_string()))?; - - if let Some(r) = rev { - if r.contains("..") { - revwalk - .push_range(r) - .map_err(|e| GitError::Internal(e.to_string()))?; - } else { - revwalk - .push_ref(r) - .map_err(|e| GitError::Internal(e.to_string()))?; - } - } else { - revwalk - .push_head() - .map_err(|e| GitError::Internal(e.to_string()))?; - } - - revwalk - .set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut commits = Vec::new(); - let target = offset.saturating_add(limit); - for oid_result in revwalk { - let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; - if target > 0 && commits.len() >= target { - break; - } - if let Ok(commit) = self.repo.find_commit(oid) { - commits.push(CommitMeta::from_git2(&commit)); - } - } - - if limit == 0 { - Ok(commits.into_iter().skip(offset).collect()) - } else { - Ok(commits.into_iter().skip(offset).take(limit).collect()) - } - } - - pub fn commit_range(&self, from: &str, to: &str) -> GitResult> { - let range = format!("{}..{}", from, to); - self.commit_log(Some(&range), 0, 0) - } - - pub fn commit_count(&self, from: Option<&str>, to: Option<&str>) -> GitResult { - let mut revwalk = self - .repo() - .revwalk() - .map_err(|e| GitError::Internal(e.to_string()))?; - - let rev = match (from, to) { - (Some(f), Some(t)) => Some(format!("{}..{}", f, t)), - (Some(f), None) => Some(f.to_string()), - (None, Some(t)) => Some(t.to_string()), - (None, None) => None, - }; - - if let Some(r) = rev.as_deref() { - if r.contains("..") { - revwalk - .push_range(r) - .map_err(|e| GitError::Internal(e.to_string()))?; - } else { - revwalk - .push_ref(r) - .map_err(|e| GitError::Internal(e.to_string()))?; - } - } else { - revwalk - .push_head() - .map_err(|e| GitError::Internal(e.to_string()))?; - } - - Ok(revwalk.count()) - } - - pub fn commit_total(&self, rev: Option<&str>) -> GitResult { - self.commit_count(None, rev) - } -} diff --git a/libs/git/commit/rebase.rs b/libs/git/commit/rebase.rs deleted file mode 100644 index b0e954e..0000000 --- a/libs/git/commit/rebase.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! Rebase operations. -//! -//! Manual rebase implementation since git2's `RebaseSession` has type inference -//! issues with `Option<&[AnnotatedCommit]>` in Rust. The approach: -//! 1. Walk all commits from `base_oid` (exclusive) to `source_oid` (inclusive). -//! 2. For each commit, apply its tree diff onto the current HEAD. -//! 3. Create a new commit with the same message on top of the current HEAD. - -use crate::commit::types::CommitOid; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - /// Rebase the commits on `source_oid` onto `base_oid`. - pub fn rebase_commits( - &self, - base_oid: &CommitOid, - source_oid: &CommitOid, - ) -> GitResult { - let base = base_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(base_oid.to_string()))?; - let source = source_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(source_oid.to_string()))?; - - // Collect all commits from base (exclusive) to source (inclusive). - let mut revwalk = self - .repo() - .revwalk() - .map_err(|e| GitError::Internal(e.to_string()))?; - revwalk - .push(source) - .map_err(|e| GitError::Internal(e.to_string()))?; - revwalk - .hide(base) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut commits: Vec = Vec::new(); - for oid_result in revwalk { - let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; - let commit = self - .repo() - .find_commit(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - commits.push(commit); - } - - if commits.is_empty() { - return Err(GitError::Internal("No commits to rebase".to_string())); - } - - // Sort oldest-first (topological). Use OID as tiebreaker for commits with identical timestamps. - commits.sort_by(|a, b| { - let time_cmp = a.time().seconds().cmp(&b.time().seconds()); - if time_cmp != std::cmp::Ordering::Equal { - time_cmp - } else { - // Same timestamp: use OID for deterministic ordering. - a.id().cmp(&b.id()) - } - }); - - let sig = self - .repo() - .signature() - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Start with the base commit's tree. - let base_commit = self - .repo() - .find_commit(base) - .map_err(|e| GitError::Internal(e.to_string()))?; - let mut current_tree = base_commit - .tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut last_oid = base; - - for commit in &commits { - let parent_tree = ¤t_tree; - let commit_tree = commit - .tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Diff the parent tree with this commit's tree. - let diff = self - .repo() - .diff_tree_to_tree(Some(parent_tree), Some(&commit_tree), None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Apply the diff to parent_tree, producing a new tree. - let mut new_index = self - .repo() - .apply_to_tree(parent_tree, &diff, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let new_tree_oid = new_index - .write_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - - current_tree = self - .repo() - .find_tree(new_tree_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Find the parent commit for the rebased commit. - let parent_commit = self - .repo() - .find_commit(last_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Create the rebased commit on top of the current base. - let new_oid = self - .repo() - .commit( - Some("HEAD"), - &sig, - &sig, - commit.message().unwrap_or(""), - ¤t_tree, - &[&parent_commit], - ) - .map_err(|e| GitError::Internal(e.to_string()))?; - - last_oid = new_oid; - } - - Ok(CommitOid::from_git2(last_oid)) - } - - pub fn rebase_abort(&self) -> GitResult<()> { - self.repo() - .cleanup_state() - .map_err(|e| GitError::Internal(e.to_string())) - } -} diff --git a/libs/git/commit/revert.rs b/libs/git/commit/revert.rs deleted file mode 100644 index 833d521..0000000 --- a/libs/git/commit/revert.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::commit::types::*; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn commit_revert( - &self, - revert_oid: &CommitOid, - author: &CommitSignature, - committer: &CommitSignature, - message: Option<&str>, - mainline: u32, - update_ref: Option<&str>, - ) -> GitResult { - let revert_commit = self - .repo() - .find_commit(revert_oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(revert_oid.to_string()))?; - - let head_oid = self - .repo() - .head() - .ok() - .and_then(|r| r.target()) - .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?; - let our_commit = self - .repo() - .find_commit(head_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut index = self - .repo() - .revert_commit(&revert_commit, &our_commit, mainline, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let tree_oid = index - .write_tree_to(&*self.repo()) - .map_err(|e| GitError::Internal(e.to_string()))?; - let tree = self - .repo() - .find_tree(tree_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let original_summary = revert_commit.summary().unwrap_or(""); - let msg: String = match message { - Some(m) => m.to_string(), - None => format!("Revert \"{}\"", original_summary), - }; - - let author = self.commit_signature_to_git2(author)?; - let committer = self.commit_signature_to_git2(committer)?; - - // When mainline > 0, revert creates a merge commit with two parents: - // (mainline_parent, our_commit). Otherwise single parent (our_commit). - let oid = if mainline > 0 { - let parent_count = revert_commit.parent_count(); - let idx = (mainline - 1) as usize; - if idx >= parent_count { - return Err(GitError::Internal(format!( - "mainline parent index {} out of range (commit has {} parents)", - idx, parent_count - ))); - } - let mainline_parent = revert_commit - .parent(idx) - .map_err(|e| GitError::Internal(e.to_string()))?; - self.repo() - .commit( - update_ref, - &author, - &committer, - &msg, - &tree, - &[&mainline_parent, &our_commit], - ) - .map_err(|e| GitError::Internal(e.to_string()))? - } else { - self.repo() - .commit(update_ref, &author, &committer, &msg, &tree, &[&our_commit]) - .map_err(|e| GitError::Internal(e.to_string()))? - }; - - Ok(CommitOid::from_git2(oid)) - } - - pub fn commit_revert_would_conflict( - &self, - revert_oid: &CommitOid, - mainline: u32, - ) -> GitResult { - let revert_commit = self - .repo() - .find_commit(revert_oid.to_oid()?) - .map_err(|_e| GitError::ObjectNotFound(revert_oid.to_string()))?; - - let state = self.repo().state(); - if matches!( - state, - git2::RepositoryState::Rebase - | git2::RepositoryState::Merge - | git2::RepositoryState::CherryPick - | git2::RepositoryState::CherryPickSequence - ) { - return Err(GitError::Internal(format!( - "repository is in conflicting state for revert: {:?}", - state - ))); - } - - let head_oid = self - .repo() - .head() - .ok() - .and_then(|r| r.target()) - .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?; - let our_commit = self - .repo() - .find_commit(head_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - match self - .repo() - .revert_commit(&revert_commit, &our_commit, mainline, None) - { - Ok(index) => { - let has_conflicts = (0..index.len()).any(|i| { - index - .get(i) - .map(|e| (e.flags >> 12) & 0x3 > 0) - .unwrap_or(false) - }); - Ok(has_conflicts) - } - Err(e) => { - if e.code() == git2::ErrorCode::Conflict { - Ok(true) - } else { - Err(GitError::Internal(e.to_string())) - } - } - } - } - - pub fn commit_revert_abort(&self, reset_type: &str) -> GitResult<()> { - let kind = match reset_type { - "soft" => git2::ResetType::Soft, - "mixed" => git2::ResetType::Mixed, - "hard" => git2::ResetType::Hard, - _ => { - return Err(GitError::Internal(format!( - "unknown reset type: {}", - reset_type - ))); - } - }; - - let head_oid = self - .repo() - .head() - .ok() - .and_then(|r| r.target()) - .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?; - let obj = self - .repo() - .find_object(head_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - self.repo() - .reset(&obj, kind, None) - .map_err(|e| GitError::Internal(e.to_string())) - } -} diff --git a/libs/git/commit/traverse.rs b/libs/git/commit/traverse.rs deleted file mode 100644 index b46c3fd..0000000 --- a/libs/git/commit/traverse.rs +++ /dev/null @@ -1,230 +0,0 @@ -//! Commit traversal and iteration. - -use crate::commit::types::*; -use crate::{GitDomain, GitError, GitResult}; - -#[derive(Debug, Clone)] -pub struct CommitWalkOptions { - pub rev: Option, - pub sort: CommitSort, - pub limit: usize, - pub first_parent_only: bool, -} - -impl CommitWalkOptions { - pub fn new() -> Self { - Self { - rev: None, - sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), - limit: 0, - first_parent_only: false, - } - } - - pub fn rev(mut self, rev: &str) -> Self { - self.rev = Some(rev.to_string()); - self - } - - pub fn topological(mut self) -> Self { - self.sort = CommitSort(git2::Sort::TOPOLOGICAL.bits()); - self - } - - pub fn time_order(mut self) -> Self { - self.sort = CommitSort(git2::Sort::TIME.bits()); - self - } - - pub fn reverse(mut self) -> Self { - self.sort = CommitSort(self.sort.0 | git2::Sort::REVERSE.bits()); - self - } - - pub fn limit(mut self, n: usize) -> Self { - self.limit = n; - self - } - - pub fn first_parent(mut self) -> Self { - self.first_parent_only = true; - self - } -} - -impl Default for CommitWalkOptions { - fn default() -> Self { - Self::new() - } -} - -impl GitDomain { - pub fn commit_walk(&self, opts: CommitWalkOptions) -> GitResult> { - let mut revwalk = self - .repo() - .revwalk() - .map_err(|e| GitError::Internal(e.to_string()))?; - - revwalk - .set_sorting(opts.sort.to_git2()) - .map_err(|e| GitError::Internal(e.to_string()))?; - - if let Some(ref r) = opts.rev { - if r.contains("..") { - revwalk - .push_range(r) - .map_err(|e| GitError::Internal(e.to_string()))?; - } else { - revwalk - .push_ref(r) - .map_err(|e| GitError::Internal(e.to_string()))?; - } - } else { - revwalk - .push_head() - .map_err(|e| GitError::Internal(e.to_string()))?; - } - - let mut commits = Vec::new(); - - if opts.first_parent_only { - let mut prev_oid: Option = None; - - for oid_result in revwalk { - let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; - - if let Some(prev) = prev_oid { - if let Ok(commit) = self.repo.find_commit(oid) { - if commit.parent_ids().next() == Some(prev) { - if limit_check(&commits, opts.limit) { - break; - } - commits.push(CommitMeta::from_git2(&commit)); - prev_oid = Some(oid); - } - } - } else { - if let Ok(commit) = self.repo.find_commit(oid) { - if limit_check(&commits, opts.limit) { - break; - } - commits.push(CommitMeta::from_git2(&commit)); - prev_oid = Some(oid); - } - } - } - } else { - for oid_result in revwalk { - let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; - if limit_check(&commits, opts.limit) { - break; - } - if let Ok(commit) = self.repo.find_commit(oid) { - commits.push(CommitMeta::from_git2(&commit)); - } - } - } - - Ok(commits) - } - - pub fn commit_topo_walk(&self, rev: Option<&str>, limit: usize) -> GitResult> { - self.commit_walk(CommitWalkOptions { - rev: rev.map(String::from), - sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), - limit, - first_parent_only: false, - }) - } - - pub fn commit_reverse_walk( - &self, - rev: Option<&str>, - limit: usize, - ) -> GitResult> { - self.commit_walk(CommitWalkOptions { - rev: rev.map(String::from), - sort: CommitSort(git2::Sort::TIME.bits() | git2::Sort::REVERSE.bits()), - limit, - first_parent_only: false, - }) - } - - pub fn commit_mainline(&self, rev: Option<&str>, limit: usize) -> GitResult> { - self.commit_walk(CommitWalkOptions { - rev: rev.map(String::from), - sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), - limit, - first_parent_only: true, - }) - } - - pub fn commit_ancestors(&self, oid: &CommitOid, limit: usize) -> GitResult> { - let mut revwalk = self - .repo() - .revwalk() - .map_err(|e| GitError::Internal(e.to_string()))?; - revwalk - .set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME) - .map_err(|e| GitError::Internal(e.to_string()))?; - revwalk - .push(oid.to_oid()?) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut commits = Vec::new(); - for oid_result in revwalk { - let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; - if limit > 0 && commits.len() >= limit { - break; - } - if let Ok(commit) = self.repo.find_commit(oid) { - commits.push(CommitMeta::from_git2(&commit)); - } - } - Ok(commits) - } - - pub fn commit_descendants(&self, oid: &CommitOid, limit: usize) -> GitResult> { - let range = format!("{}..", oid); - self.commit_walk(CommitWalkOptions { - rev: Some(range), - sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), - limit, - first_parent_only: false, - }) - } - - pub fn resolve_rev(&self, rev: &str) -> GitResult { - if let Ok(oid) = git2::Oid::from_str(rev) { - return Ok(CommitOid::from_git2(oid)); - } - - // "HEAD" — use repo.head() to get the actual OID - if rev == "HEAD" { - if let Ok(reference) = self.repo.head() { - if let Some(target) = reference.target() { - return Ok(CommitOid::from_git2(target)); - } - } - return Err(GitError::InvalidOid( - "cannot resolve: HEAD (detached or empty)".into(), - )); - } - - if let Ok(reference) = self.repo.find_reference(rev) { - if let Some(target) = reference.target() { - return Ok(CommitOid::from_git2(target)); - } - } - - if let Ok(commit) = self.repo.revparse_single(rev) { - return Ok(CommitOid::from_git2(commit.id())); - } - - Err(GitError::InvalidOid(format!("cannot resolve: {}", rev))) - } -} - -fn limit_check(commits: &[CommitMeta], limit: usize) -> bool { - limit > 0 && commits.len() >= limit -} diff --git a/libs/git/commit/types.rs b/libs/git/commit/types.rs deleted file mode 100644 index adcc695..0000000 --- a/libs/git/commit/types.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! Serializable types for the commit domain. - -use crate::GitError; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct CommitOid(pub String); - -impl CommitOid { - pub fn new(hex: &str) -> Self { - Self(hex.to_lowercase()) - } - - pub fn from_oid(oid: git2::Oid) -> Self { - Self(oid.to_string()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn to_oid(&self) -> Result { - git2::Oid::from_str(&self.0).map_err(|_| GitError::InvalidOid(self.0.clone())) - } - - pub fn from_git2(oid: git2::Oid) -> Self { - Self(oid.to_string()) - } -} - -impl std::fmt::Display for CommitOid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl AsRef for CommitOid { - fn as_ref(&self) -> &str { - &self.0 - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitSignature { - pub name: String, - pub email: String, - pub time_secs: i64, - pub offset_minutes: i32, -} - -impl CommitSignature { - pub fn from_git2(sig: git2::Signature<'_>) -> Self { - Self { - name: sig.name().unwrap_or("").to_string(), - email: sig.email().unwrap_or("").to_string(), - time_secs: sig.when().seconds(), - offset_minutes: sig.when().offset_minutes(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitMeta { - pub oid: CommitOid, - pub message: String, - pub summary: String, - pub author: CommitSignature, - pub committer: CommitSignature, - pub tree_id: CommitOid, - pub parent_ids: Vec, - pub encoding: Option, -} - -impl CommitMeta { - pub fn from_git2(commit: &git2::Commit<'_>) -> Self { - Self { - oid: CommitOid::from_git2(commit.id()), - message: commit.message().unwrap_or("").to_string(), - summary: commit.summary().unwrap_or("").to_string(), - author: CommitSignature::from_git2(commit.author()), - committer: CommitSignature::from_git2(commit.committer()), - tree_id: CommitOid::from_git2(commit.tree_id()), - parent_ids: commit.parent_ids().map(CommitOid::from_git2).collect(), - encoding: commit.message_encoding().map(String::from), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitDiffStats { - pub oid: String, - pub files_changed: usize, - pub insertions: usize, - pub deletions: usize, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitDiffFile { - pub path: Option, - pub status: String, - pub is_binary: bool, - pub size: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitDiffHunk { - pub old_start: u32, - pub old_lines: u32, - pub new_start: u32, - pub new_lines: u32, - pub header: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitBlameHunk { - pub commit_oid: CommitOid, - pub final_start_line: u32, - pub final_lines: u32, - pub orig_start_line: u32, - pub orig_lines: u32, - pub boundary: bool, - pub orig_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitBlameLine { - pub commit_oid: CommitOid, - pub line_no: u32, - pub content: String, - pub orig_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitRefInfo { - pub name: String, - pub target: CommitOid, - pub is_remote: bool, - pub is_tag: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CommitReflogEntry { - pub oid_new: CommitOid, - pub oid_old: CommitOid, - pub committer_name: String, - pub committer_email: String, - pub time_secs: i64, - pub offset_minutes: i32, - pub message: Option, - pub ref_name: String, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] -pub struct CommitSort(pub u32); - -impl CommitSort { - pub const TOPOLOGICAL: Self = Self(git2::Sort::TOPOLOGICAL.bits()); - pub const TIME: Self = Self(git2::Sort::TIME.bits()); - pub const REVERSE: Self = Self(git2::Sort::REVERSE.bits()); - - pub fn to_git2(self) -> git2::Sort { - git2::Sort::from_bits(self.0).unwrap_or(git2::Sort::NONE) - } -} diff --git a/libs/git/config/mod.rs b/libs/git/config/mod.rs deleted file mode 100644 index d761bf0..0000000 --- a/libs/git/config/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Config domain — repository and global config read/write operations. -pub mod ops; -pub mod types; diff --git a/libs/git/config/ops.rs b/libs/git/config/ops.rs deleted file mode 100644 index cebfa25..0000000 --- a/libs/git/config/ops.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Config operations. - -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; - -use crate::config::types::{ConfigEntry, ConfigSnapshot}; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - /// Open the repository-level config. - pub fn config(&self) -> GitResult { - let cfg = self - .repo() - .config() - .map_err(|e| GitError::ConfigError(e.to_string()))?; - Ok(GitConfig { - inner: Rc::new(RefCell::new(cfg)), - }) - } - - pub fn config_get(&self, key: &str) -> GitResult> { - let cfg = self.config()?; - match cfg.get_str(key) { - Ok(v) => Ok(Some(v)), - Err(e) => { - // git2 returns an error for not-found keys. - if e.code() == git2::ErrorCode::NotFound { - Ok(None) - } else { - Err(GitError::ConfigError(e.to_string())) - } - } - } - } - - pub fn config_set(&self, key: &str, value: &str) -> GitResult<()> { - let cfg = self.config()?; - cfg.set(key, value) - .map_err(|e| GitError::ConfigError(e.to_string())) - } - - pub fn config_delete(&self, key: &str) -> GitResult<()> { - let cfg = self.config()?; - cfg.delete(key) - .map_err(|e| GitError::ConfigError(e.to_string())) - } - - /// List all config entries. Optionally filter by key prefix (e.g. "user"). - pub fn config_entries(&self, prefix: Option<&str>) -> GitResult { - let cfg = self.config()?; - let mut entries = Vec::new(); - - let glob = prefix.filter(|p| !p.is_empty()); - - let binding = cfg.inner.borrow(); - let _ = binding - .entries(glob) - .map_err(|e: git2::Error| GitError::ConfigError(e.to_string()))? - .for_each(|entry| { - let name = entry.name().unwrap_or("").to_string(); - let value = entry.value().unwrap_or("").to_string(); - entries.push(ConfigEntry { name, value }); - }); - - Ok(ConfigSnapshot { entries }) - } - - pub fn config_has(&self, key: &str) -> GitResult { - let cfg = self.config()?; - Ok(cfg.get_str(key).is_ok()) - } - - pub fn config_get_family(&self, prefix: &str) -> GitResult> { - let snapshot = self.config_entries(Some(prefix))?; - Ok(snapshot - .entries - .into_iter() - .map(|e| (e.name, e.value)) - .collect()) - } -} - -/// A wrapper around git2::Config providing a cleaner API. -pub struct GitConfig { - inner: Rc>, -} - -impl GitConfig { - fn get_str(&self, key: &str) -> Result { - self.inner.borrow().get_str(key).map(String::from) - } - - fn set(&self, key: &str, value: &str) -> Result<(), git2::Error> { - self.inner.borrow_mut().set_str(key, value) - } - - fn delete(&self, key: &str) -> Result<(), git2::Error> { - self.inner.borrow_mut().remove(key) - } -} diff --git a/libs/git/config/types.rs b/libs/git/config/types.rs deleted file mode 100644 index ed1e846..0000000 --- a/libs/git/config/types.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Serializable types for the config domain. - -use serde::{Deserialize, Serialize}; - -/// A single config entry. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConfigEntry { - pub name: String, - pub value: String, -} - -/// A snapshot of config entries matching a query. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ConfigSnapshot { - pub entries: Vec, -} diff --git a/libs/git/description/mod.rs b/libs/git/description/mod.rs deleted file mode 100644 index 4a69a0c..0000000 --- a/libs/git/description/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Description domain — repository description file read/write (used by GitWeb). - -use std::path::PathBuf; - -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - /// Path to the description file. - fn description_path(&self) -> PathBuf { - PathBuf::from(self.repo().path()).join("description") - } - - /// Read the repository description. - /// Returns "Unnamed repository" if the file does not exist. - pub fn description_get(&self) -> GitResult { - let path = self.description_path(); - if !path.exists() { - return Ok("Unnamed repository".to_string()); - } - let content = - std::fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?; - Ok(content.trim().to_string()) - } - - /// Write the repository description. - pub fn description_set(&self, description: &str) -> GitResult<()> { - let path = self.description_path(); - std::fs::write(&path, description.trim()).map_err(|e| GitError::IoError(e.to_string())) - } - - pub fn description_exists(&self) -> bool { - self.description_path().exists() - } - - /// Reset description to the default "Unnamed repository". - pub fn description_reset(&self) -> GitResult<()> { - self.description_set("Unnamed repository") - } -} diff --git a/libs/git/diff/mod.rs b/libs/git/diff/mod.rs deleted file mode 100644 index e742ef8..0000000 --- a/libs/git/diff/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Diff domain — all diff-related operations on a GitDomain. -pub mod ops; -pub mod types; diff --git a/libs/git/diff/ops.rs b/libs/git/diff/ops.rs deleted file mode 100644 index d423b98..0000000 --- a/libs/git/diff/ops.rs +++ /dev/null @@ -1,533 +0,0 @@ -//! Diff operations. - -use std::cell::RefCell; - -use crate::commit::types::CommitOid; -use crate::diff::types::{ - DiffDelta, DiffHunk, DiffLine, DiffOptions, DiffResult, DiffStats, SideBySideChangeType, - SideBySideDiffResult, SideBySideFile, SideBySideLine, -}; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn diff_tree_to_tree( - &self, - old_tree: Option<&CommitOid>, - new_tree: Option<&CommitOid>, - opts: Option, - ) -> GitResult { - let old_tree = match old_tree { - Some(oid) => { - let o = oid - .to_oid() - .map_err(|_| GitError::InvalidOid(oid.to_string()))?; - let obj = self - .repo() - .find_object(o, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - Some( - obj.peel_to_tree() - .map_err(|e| GitError::Internal(e.to_string()))?, - ) - } - None => None, - }; - let new_tree = match new_tree { - Some(oid) => { - let o = oid - .to_oid() - .map_err(|_| GitError::InvalidOid(oid.to_string()))?; - let obj = self - .repo() - .find_object(o, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - Some( - obj.peel_to_tree() - .map_err(|e| GitError::Internal(e.to_string()))?, - ) - } - None => None, - }; - - let mut git_opts = opts - .map(|o| o.to_git2()) - .unwrap_or_else(git2::DiffOptions::new); - - // git2 requires at least one tree to be Some - let diff = match (old_tree.as_ref(), new_tree.as_ref()) { - (Some(old), Some(new)) => { - self.repo() - .diff_tree_to_tree(Some(old), Some(new), Some(&mut git_opts)) - } - (Some(old), None) => { - self.repo() - .diff_tree_to_tree(Some(old), None, Some(&mut git_opts)) - } - (None, Some(new)) => { - self.repo() - .diff_tree_to_tree(None, Some(new), Some(&mut git_opts)) - } - (None, None) => { - return Err(GitError::Internal( - "Both old_tree and new_tree are None".into(), - )); - } - } - .map_err(|e| GitError::Internal(e.to_string()))?; - - build_diff_result(&diff) - } - - pub fn diff_commit_to_workdir( - &self, - commit: &CommitOid, - opts: Option, - ) -> GitResult { - let oid = commit - .to_oid() - .map_err(|_| GitError::InvalidOid(commit.to_string()))?; - - let commit = self - .repo() - .find_commit(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let tree = self - .repo() - .find_tree(commit.tree_id()) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut git_opts = opts - .map(|o| o.to_git2()) - .unwrap_or_else(git2::DiffOptions::new); - - let diff = self - .repo() - .diff_tree_to_workdir(Some(&tree), Some(&mut git_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - - build_diff_result(&diff) - } - - pub fn diff_commit_to_index( - &self, - commit: &CommitOid, - opts: Option, - ) -> GitResult { - let oid = commit - .to_oid() - .map_err(|_| GitError::InvalidOid(commit.to_string()))?; - - let commit = self - .repo() - .find_commit(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let tree = self - .repo() - .find_tree(commit.tree_id()) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Get the index as a tree for comparison. - let mut index = self - .repo() - .index() - .map_err(|e| GitError::Internal(e.to_string()))?; - let index_tree_oid = index - .write_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - let index_tree = self - .repo() - .find_tree(index_tree_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut git_opts = opts - .map(|o| o.to_git2()) - .unwrap_or_else(git2::DiffOptions::new); - - let diff = self - .repo() - .diff_tree_to_tree(Some(&tree), Some(&index_tree), Some(&mut git_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - - build_diff_result(&diff) - } - - pub fn diff_workdir_to_index(&self, opts: Option) -> GitResult { - let mut git_opts = opts - .map(|o| o.to_git2()) - .unwrap_or_else(git2::DiffOptions::new); - - let diff = self - .repo() - .diff_tree_to_workdir(None, Some(&mut git_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - - build_diff_result(&diff) - } - - pub fn diff_index_to_tree( - &self, - tree: &CommitOid, - opts: Option, - ) -> GitResult { - let oid = tree - .to_oid() - .map_err(|_| GitError::InvalidOid(tree.to_string()))?; - - let tree = self - .repo() - .find_tree(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut git_opts = opts - .map(|o| o.to_git2()) - .unwrap_or_else(git2::DiffOptions::new); - - let diff = self - .repo() - .diff_tree_to_tree(Some(&tree), None, Some(&mut git_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - - build_diff_result(&diff) - } - - pub fn diff_stats(&self, old_tree: &CommitOid, new_tree: &CommitOid) -> GitResult { - let old_oid = old_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(old_tree.to_string()))?; - let new_oid = new_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(new_tree.to_string()))?; - - let old_obj = self - .repo() - .find_object(old_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - let new_obj = self - .repo() - .find_object(new_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let old_tree = old_obj - .peel_to_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - let new_tree = new_obj - .peel_to_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - - let diff = self - .repo() - .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let stats = diff - .stats() - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(DiffStats::from_git2(&stats)) - } - - pub fn diff_patch_id(&self, old_tree: &CommitOid, new_tree: &CommitOid) -> GitResult { - let old_oid = old_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(old_tree.to_string()))?; - let new_oid = new_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(new_tree.to_string()))?; - - let old_obj = self - .repo() - .find_object(old_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - let new_obj = self - .repo() - .find_object(new_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let old_tree = old_obj - .peel_to_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - let new_tree = new_obj - .peel_to_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - - let diff = self - .repo() - .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let patch_id = diff - .patchid(None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(patch_id.to_string()) - } -} - -fn build_diff_result(diff: &git2::Diff<'_>) -> GitResult { - let stats = diff - .stats() - .map_err(|e| GitError::Internal(e.to_string()))?; - - let delta_count = diff.deltas().len(); - let deltas: RefCell> = RefCell::new(Vec::with_capacity(delta_count)); - let delta_hunks: RefCell> = RefCell::new(Vec::new()); - let delta_lines: RefCell> = RefCell::new(Vec::new()); - let counter: RefCell = RefCell::new(0); - - let mut file_cb = |_delta: git2::DiffDelta<'_>, _progress: f32| -> bool { - let count = *counter.borrow(); - if count > 0 { - let prev_idx = count - 1; - let hunks = delta_hunks.take(); - let lines = delta_lines.take(); - if let Some(prev_delta) = diff.get_delta(prev_idx) { - deltas - .borrow_mut() - .push(DiffDelta::from_git2(&prev_delta, hunks, lines)); - } - } - *counter.borrow_mut() = count + 1; - true - }; - - let mut hunk_cb = |_delta: git2::DiffDelta<'_>, hunk: git2::DiffHunk<'_>| -> bool { - delta_hunks.borrow_mut().push(DiffHunk::from_git2(&hunk)); - true - }; - - let mut line_cb = |_delta: git2::DiffDelta<'_>, - _hunk: Option>, - line: git2::DiffLine<'_>| - -> bool { - delta_lines.borrow_mut().push(DiffLine::from_git2(&line)); - true - }; - - diff.foreach(&mut file_cb, None, Some(&mut hunk_cb), Some(&mut line_cb)) - .map_err(|e| GitError::Internal(e.to_string()))?; - { - let count = *counter.borrow(); - if count > 0 { - let last_idx = count - 1; - let hunks = delta_hunks.take(); - let lines = delta_lines.take(); - if let Some(last_delta) = diff.get_delta(last_idx) { - deltas - .borrow_mut() - .push(DiffDelta::from_git2(&last_delta, hunks, lines)); - } - } - } - - Ok(DiffResult { - stats: DiffStats::from_git2(&stats), - deltas: deltas.into_inner(), - }) -} - -/// The algorithm walks the unified diff line-by-line and produces rows where: -/// - **Added** lines appear only on the right side. -/// - **Deleted** lines appear only on the left side. -/// - When a deletion is immediately followed by an addition they are rendered as a -/// **Modified** pair (two separate rows). -/// - **Context** lines appear on both sides with the same content. -/// - **Empty** filler rows are inserted so that left and right line numbers stay aligned. -pub fn diff_to_side_by_side(diff: &DiffResult) -> SideBySideDiffResult { - let mut files: Vec = Vec::with_capacity(diff.deltas.len()); - let mut total_additions = 0usize; - let mut total_deletions = 0usize; - - for delta in &diff.deltas { - let (path, is_binary, is_rename) = - if delta.status == crate::diff::types::DiffDeltaStatus::Renamed { - ( - delta - .new_file - .path - .clone() - .or_else(|| delta.old_file.path.clone()) - .unwrap_or_default(), - delta.new_file.is_binary || delta.old_file.is_binary, - true, - ) - } else { - ( - delta - .new_file - .path - .clone() - .or_else(|| delta.old_file.path.clone()) - .unwrap_or_default(), - delta.new_file.is_binary, - false, - ) - }; - - if is_binary { - files.push(SideBySideFile { - path, - additions: 0, - deletions: 0, - is_binary: true, - is_rename, - lines: vec![], - }); - continue; - } - - let lines = build_side_by_side_lines(&delta.lines); - let additions = lines - .iter() - .filter(|l| matches!(l.change_type, SideBySideChangeType::Added)) - .count(); - let deletions = lines - .iter() - .filter(|l| matches!(l.change_type, SideBySideChangeType::Removed)) - .count(); - - total_additions += additions; - total_deletions += deletions; - - files.push(SideBySideFile { - path, - additions, - deletions, - is_binary: false, - is_rename, - lines, - }); - } - - SideBySideDiffResult { - files, - total_additions, - total_deletions, - } -} - -fn build_side_by_side_lines(unified: &[DiffLine]) -> Vec { - let mut result: Vec = Vec::with_capacity(unified.len() * 2); - let mut i = 0; - - while i < unified.len() { - let line = &unified[i]; - - match line.origin { - '+' => { - // Collect a run of consecutive additions. - let mut added_lines: Vec<&DiffLine> = vec![line]; - while i + 1 < unified.len() && unified[i + 1].origin == '+' { - i += 1; - added_lines.push(&unified[i]); - } - - // Peek at the previous deletion run to pair with additions. - let mut deleted_lines: Vec<&DiffLine> = vec![]; - // Backtrack to find deletions right before this run. - let Some(j_base) = i.checked_sub(added_lines.len()) else { - // Additions at start of diff — no preceding deletions to pair with. - // Emit all as unpaired Added below. - for k in 0..added_lines.len() { - let add = added_lines[k]; - result.push(SideBySideLine { - left_line_no: None, - right_line_no: add.new_lineno, - left_content: String::new(), - right_content: add.content.clone(), - change_type: SideBySideChangeType::Added, - }); - } - i += 1; - continue; - }; - let mut j = j_base; - while j > 0 && unified[j].origin == '-' { - j -= 1; - } - let del_start = if j == 0 && unified[0].origin == '-' { - 0 - } else { - j + 1 - }; - for k in del_start..i { - if unified[k].origin == '-' { - deleted_lines.push(&unified[k]); - } - } - - // If we have paired deletions, emit them as Modified pairs. - let pairs = deleted_lines.len().min(added_lines.len()); - for k in 0..pairs { - let del = deleted_lines[k]; - let add = added_lines[k]; - result.push(SideBySideLine { - left_line_no: del.old_lineno, - right_line_no: add.new_lineno, - left_content: del.content.clone(), - right_content: add.content.clone(), - change_type: SideBySideChangeType::Modified, - }); - } - - // Remaining unpaired additions. - for k in pairs..added_lines.len() { - let add = added_lines[k]; - result.push(SideBySideLine { - left_line_no: None, - right_line_no: add.new_lineno, - left_content: String::new(), - right_content: add.content.clone(), - change_type: SideBySideChangeType::Added, - }); - } - - // Remaining unpaired deletions (only possible if deletions > additions). - for k in pairs..deleted_lines.len() { - let del = deleted_lines[k]; - result.push(SideBySideLine { - left_line_no: del.old_lineno, - right_line_no: None, - left_content: del.content.clone(), - right_content: String::new(), - change_type: SideBySideChangeType::Removed, - }); - } - } - '-' => { - // Collect a run of consecutive deletions. - let mut deleted_lines: Vec<&DiffLine> = vec![line]; - while i + 1 < unified.len() && unified[i + 1].origin == '-' { - i += 1; - deleted_lines.push(&unified[i]); - } - - // Emit each deletion (unless already paired above). - // We defer pairing to the addition-handling block above, - // so here we just emit unpaired deletions. - for del in &deleted_lines { - result.push(SideBySideLine { - left_line_no: del.old_lineno, - right_line_no: None, - left_content: del.content.clone(), - right_content: String::new(), - change_type: SideBySideChangeType::Removed, - }); - } - } - _ => { - // Context line — appears on both sides. - result.push(SideBySideLine { - left_line_no: line.old_lineno, - right_line_no: line.new_lineno, - left_content: line.content.clone(), - right_content: line.content.clone(), - change_type: SideBySideChangeType::Unchanged, - }); - } - } - - i += 1; - } - - result -} diff --git a/libs/git/diff/types.rs b/libs/git/diff/types.rs deleted file mode 100644 index f3b5ede..0000000 --- a/libs/git/diff/types.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Serializable types for the diff domain. - -use serde::{Deserialize, Serialize}; - -use crate::commit::types::CommitOid; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum DiffDeltaStatus { - Unmodified, - Added, - Deleted, - Modified, - Renamed, - Copied, - Ignored, - Untracked, - Typechange, - Unreadable, - Conflicted, -} - -impl DiffDeltaStatus { - pub fn from_git2(status: git2::Delta) -> Self { - match status { - git2::Delta::Unmodified => Self::Unmodified, - git2::Delta::Added => Self::Added, - git2::Delta::Deleted => Self::Deleted, - git2::Delta::Modified => Self::Modified, - git2::Delta::Renamed => Self::Renamed, - git2::Delta::Copied => Self::Copied, - git2::Delta::Ignored => Self::Ignored, - git2::Delta::Untracked => Self::Untracked, - git2::Delta::Typechange => Self::Typechange, - git2::Delta::Unreadable => Self::Unreadable, - git2::Delta::Conflicted => Self::Conflicted, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffFile { - pub oid: Option, - pub path: Option, - pub size: u64, - pub is_binary: bool, -} - -impl DiffFile { - pub fn from_git2(file: &git2::DiffFile<'_>) -> Self { - Self { - oid: if file.is_valid_id() { - Some(CommitOid::from_git2(file.id())) - } else { - None - }, - path: file.path().map(|p| p.to_string_lossy().to_string()), - size: file.size(), - is_binary: file.is_binary(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffHunk { - pub old_start: u32, - pub old_lines: u32, - pub new_start: u32, - pub new_lines: u32, - pub header: String, -} - -impl DiffHunk { - pub fn from_git2(hunk: &git2::DiffHunk<'_>) -> Self { - Self { - old_start: hunk.old_start(), - old_lines: hunk.old_lines(), - new_start: hunk.new_start(), - new_lines: hunk.new_lines(), - header: String::from_utf8_lossy(hunk.header()).to_string(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffLine { - pub content: String, - pub origin: char, - pub old_lineno: Option, - pub new_lineno: Option, - pub num_lines: u32, - pub content_offset: i64, -} - -impl DiffLine { - pub fn from_git2(line: &git2::DiffLine<'_>) -> Self { - Self { - content: String::from_utf8_lossy(line.content()).to_string(), - origin: line.origin(), - old_lineno: line.old_lineno(), - new_lineno: line.new_lineno(), - num_lines: line.num_lines(), - content_offset: line.content_offset(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffDelta { - pub status: DiffDeltaStatus, - pub old_file: DiffFile, - pub new_file: DiffFile, - pub nfiles: u16, - pub hunks: Vec, - pub lines: Vec, -} - -impl DiffDelta { - pub fn from_git2( - delta: &git2::DiffDelta<'_>, - hunks: Vec, - lines: Vec, - ) -> Self { - Self { - status: DiffDeltaStatus::from_git2(delta.status()), - old_file: DiffFile::from_git2(&delta.old_file()), - new_file: DiffFile::from_git2(&delta.new_file()), - nfiles: delta.nfiles(), - hunks, - lines, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffStats { - pub files_changed: usize, - pub insertions: usize, - pub deletions: usize, -} - -impl DiffStats { - pub fn from_git2(stats: &git2::DiffStats) -> Self { - Self { - files_changed: stats.files_changed(), - insertions: stats.insertions(), - deletions: stats.deletions(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiffResult { - pub stats: DiffStats, - pub deltas: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum SideBySideChangeType { - /// Both sides show the same context line. - Unchanged, - /// Line was added (only on the right / new side). - Added, - /// Line was removed (only on the left / old side). - Removed, - /// A modified region — left shows old line, right shows new line. - Modified, - /// Empty row used to align paired add/remove lines. - Empty, -} - -impl SideBySideChangeType {} - -/// One row in the side-by-side output. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SideBySideLine { - /// Line number in the old / left file. `None` for added-only rows. - pub left_line_no: Option, - /// Line number in the new / right file. `None` for deleted-only rows. - pub right_line_no: Option, - /// Content displayed on the left side. - pub left_content: String, - /// Content displayed on the right side. - pub right_content: String, - /// How this row should be rendered. - pub change_type: SideBySideChangeType, -} - -/// A single file in a side-by-side diff. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SideBySideFile { - /// Path of the file (prefer the new path for renames). - pub path: String, - /// Number of additions in this file. - pub additions: usize, - /// Number of deletions in this file. - pub deletions: usize, - /// Whether this file is binary. - pub is_binary: bool, - /// Whether this file was renamed. - pub is_rename: bool, - /// All rows in the side-by-side view. - pub lines: Vec, -} - -/// The complete side-by-side diff result for all files. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SideBySideDiffResult { - pub files: Vec, - pub total_additions: usize, - pub total_deletions: usize, -} - -#[derive(Debug, Clone, Default)] -pub struct DiffOptions { - context_lines: u32, - pathspec: Vec, - include_untracked: bool, - include_ignored: bool, - include_unmodified: bool, - ignore_whitespace: bool, - force_text: bool, - skip_binary_check: bool, - reverse: bool, -} - -impl DiffOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn context_lines(mut self, lines: u32) -> Self { - self.context_lines = lines; - self - } - - pub fn pathspec(mut self, path: &str) -> Self { - self.pathspec.push(path.to_string()); - self - } - - pub fn include_untracked(mut self) -> Self { - self.include_untracked = true; - self - } - - pub fn include_ignored(mut self) -> Self { - self.include_ignored = true; - self - } - - pub fn include_unmodified(mut self) -> Self { - self.include_unmodified = true; - self - } - - pub fn ignore_whitespace(mut self) -> Self { - self.ignore_whitespace = true; - self - } - - pub fn force_text(mut self) -> Self { - self.force_text = true; - self - } - - pub fn skip_binary_check(mut self) -> Self { - self.skip_binary_check = true; - self - } - - pub fn reverse(mut self) -> Self { - self.reverse = true; - self - } - - pub fn to_git2(&self) -> git2::DiffOptions { - let mut opts = git2::DiffOptions::new(); - if self.context_lines > 0 { - opts.context_lines(self.context_lines); - } - for p in &self.pathspec { - opts.pathspec(p); - } - if self.include_untracked { - opts.include_untracked(true); - } - if self.include_ignored { - opts.include_ignored(true); - } - if self.include_unmodified { - opts.include_unmodified(true); - } - if self.ignore_whitespace { - opts.ignore_whitespace(true); - } - if self.force_text { - opts.force_text(true); - } - if self.skip_binary_check { - opts.skip_binary_check(true); - } - if self.reverse { - opts.reverse(true); - } - opts - } -} diff --git a/libs/git/domain.rs b/libs/git/domain.rs deleted file mode 100644 index c5619c6..0000000 --- a/libs/git/domain.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::path::Path; -use std::sync::Arc; - -use crate::GitError; -use git2::Repository; -use models::repos::repo; - -#[derive(Clone)] -pub struct GitDomain { - pub(crate) repo: Arc, -} - -// SAFETY: git2's Repository uses internal locking for thread-safe operations. -// We additionally enforce exclusive access via Arc::get_mut in repo_mut(). -// All mutable access is gated through the synchronous methods of HookMetaDataSync, -// which are called from a single blocking thread per sync cycle. -#[allow(unsafe_code)] -unsafe impl Send for GitDomain {} -#[allow(unsafe_code)] -unsafe impl Sync for GitDomain {} - -impl GitDomain { - pub fn from_model(model: repo::Model) -> crate::GitResult { - let repo = - Repository::open(model.storage_path).map_err(|e| GitError::Internal(e.to_string()))?; - Ok(Self { - repo: Arc::new(repo), - }) - } - pub fn open>(path: P) -> crate::GitResult { - let repo = Repository::open(path).map_err(|e| GitError::Internal(e.to_string()))?; - Ok(Self { - repo: Arc::new(repo), - }) - } - - pub fn open_bare>(path: P) -> crate::GitResult { - let repo = Repository::open_bare(path).map_err(|e| GitError::Internal(e.to_string()))?; - Ok(Self { - repo: Arc::new(repo), - }) - } - - pub fn init_bare>(path: P) -> crate::GitResult { - let repo = Repository::init_bare(path).map_err(|e| GitError::Internal(e.to_string()))?; - Ok(Self { - repo: Arc::new(repo), - }) - } - pub fn repo(&self) -> &Repository { - &self.repo - } - - pub fn repo_mut(&mut self) -> crate::GitResult<&mut Repository> { - Arc::get_mut(&mut self.repo) - .ok_or_else(|| GitError::Internal("GitDomain requires exclusive access".to_string())) - } -} diff --git a/libs/git/error.rs b/libs/git/error.rs deleted file mode 100644 index 0a1d9bb..0000000 --- a/libs/git/error.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Git domain error types. - -use std::fmt; - -/// Result type alias for Git operations. -pub type GitResult = Result; - -/// Git domain errors. -#[derive(Debug, Clone)] -pub enum GitError { - /// Repository is not found or not accessible. - NotFound(String), - /// Object not found. - ObjectNotFound(String), - /// Reference not found. - RefNotFound(String), - /// Invalid reference name. - InvalidRefName(String), - /// Invalid object id. - InvalidOid(String), - /// Branch already exists. - BranchExists(String), - /// Tag already exists. - TagExists(String), - /// Branch is protected. - BranchProtected(String), - /// Merge conflict. - MergeConflict(String), - /// Hook execution failed. - HookFailed(String), - /// LFS operation failed. - LfsError(String), - /// Config error. - ConfigError(String), - /// I/O error. - IoError(String), - /// Authentication failed. - AuthFailed(String), - /// Permission denied. - PermissionDenied(String), - /// Locked resource. - Locked(String), - /// Internal error. - Internal(String), -} - -impl fmt::Display for GitError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - GitError::NotFound(s) => write!(f, "not found: {}", s), - GitError::ObjectNotFound(s) => write!(f, "object not found: {}", s), - GitError::RefNotFound(s) => write!(f, "ref not found: {}", s), - GitError::InvalidRefName(s) => write!(f, "invalid ref name: {}", s), - GitError::InvalidOid(s) => write!(f, "invalid oid: {}", s), - GitError::BranchExists(s) => write!(f, "branch already exists: {}", s), - GitError::TagExists(s) => write!(f, "tag already exists: {}", s), - GitError::BranchProtected(s) => write!(f, "branch is protected: {}", s), - GitError::MergeConflict(s) => write!(f, "merge conflict: {}", s), - GitError::HookFailed(s) => write!(f, "hook failed: {}", s), - GitError::LfsError(s) => write!(f, "lfs error: {}", s), - GitError::ConfigError(s) => write!(f, "config error: {}", s), - GitError::IoError(s) => write!(f, "io error: {}", s), - GitError::AuthFailed(s) => write!(f, "auth failed: {}", s), - GitError::PermissionDenied(s) => write!(f, "permission denied: {}", s), - GitError::Locked(s) => write!(f, "locked: {}", s), - GitError::Internal(s) => write!(f, "internal error: {}", s), - } - } -} - -impl std::error::Error for GitError {} - -impl From for GitError { - fn from(e: git2::Error) -> Self { - match e.code() { - git2::ErrorCode::NotFound => GitError::NotFound(e.message().to_string()), - git2::ErrorCode::Exists => GitError::BranchExists(e.message().to_string()), - git2::ErrorCode::InvalidSpec => GitError::InvalidRefName(e.message().to_string()), - git2::ErrorCode::MergeConflict => GitError::MergeConflict(e.message().to_string()), - git2::ErrorCode::Auth => GitError::AuthFailed(e.message().to_string()), - git2::ErrorCode::Invalid => GitError::InvalidOid(e.message().to_string()), - git2::ErrorCode::Locked => GitError::Locked(e.message().to_string()), - _ => GitError::Internal(e.message().to_string()), - } - } -} - -impl From for GitError { - fn from(e: std::io::Error) -> Self { - GitError::IoError(e.to_string()) - } -} - -impl From for GitError { - fn from(e: sea_orm::DbErr) -> Self { - GitError::Internal(format!("db error: {}", e)) - } -} diff --git a/libs/git/hook/embed.rs b/libs/git/hook/embed.rs deleted file mode 100644 index c3c2abf..0000000 --- a/libs/git/hook/embed.rs +++ /dev/null @@ -1,11 +0,0 @@ -use models::TagEmbedInput; - -/// Trait for tag embedding — implemented by agent's EmbedService. -/// Defined here to avoid git → agent dependency. -#[async_trait::async_trait] -pub trait TagEmbedder: Send + Sync { - async fn embed_tags_batch( - &self, - tags: Vec, - ) -> Result<(), Box>; -} diff --git a/libs/git/hook/mod.rs b/libs/git/hook/mod.rs deleted file mode 100644 index a5d0770..0000000 --- a/libs/git/hook/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::sync::Arc; - -use config::AppConfig; -use db::cache::AppCache; -use db::database::AppDatabase; -use deadpool_redis::cluster::Pool as RedisPool; -use tokio_util::sync::CancellationToken; - -pub mod embed; -pub mod pool; -pub mod sync; -pub mod webhook_dispatch; - -pub use embed::TagEmbedder; -pub use pool::types::{HookTask, TaskType}; -pub use pool::{HookWorker, PoolConfig, RedisConsumer}; - -/// Hook service that manages the Redis-backed task queue worker. -/// Multiple gitserver pods can run concurrently — the worker acquires a -/// per-repo Redis lock before processing each task. -#[derive(Clone)] -pub struct HookService { - pub(crate) db: AppDatabase, - pub(crate) cache: AppCache, - pub(crate) redis_pool: RedisPool, - pub(crate) config: AppConfig, - pub(crate) tag_embedder: Option>, -} - -impl HookService { - pub fn new(db: AppDatabase, cache: AppCache, redis_pool: RedisPool, config: AppConfig) -> Self { - Self { - db, - cache, - redis_pool, - config, - tag_embedder: None, - } - } - - /// Set a tag embedder (typically from the agent crate). - pub fn with_tag_embedder(mut self, embedder: Arc) -> Self { - self.tag_embedder = Some(embedder); - self - } - - /// Start the background worker and return a cancellation token. - pub async fn start_worker(&self) -> CancellationToken { - let pool_config = PoolConfig::from_env(&self.config); - pool::start_worker( - self.db.clone(), - self.cache.clone(), - self.redis_pool.clone(), - pool_config, - self.tag_embedder.clone(), - ) - } -} diff --git a/libs/git/hook/pool/mod.rs b/libs/git/hook/pool/mod.rs deleted file mode 100644 index 07267de..0000000 --- a/libs/git/hook/pool/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -pub mod redis; -pub mod types; -pub mod worker; - -pub use redis::RedisConsumer; -pub use types::{HookTask, PoolConfig, TaskType}; -pub use worker::HookWorker; - -use crate::hook::embed::TagEmbedder; -use db::cache::AppCache; -use db::database::AppDatabase; -use deadpool_redis::cluster::Pool as RedisPool; -use std::sync::Arc; -use tokio_util::sync::CancellationToken; - -/// Start the hook worker background task. -/// Returns a handle to the cancellation token so the caller can shut it down. -pub fn start_worker( - db: AppDatabase, - cache: AppCache, - redis_pool: RedisPool, - config: PoolConfig, - tag_embedder: Option>, -) -> CancellationToken { - let consumer = RedisConsumer::new( - redis_pool.clone(), - config.redis_list_prefix.clone(), - config.redis_block_timeout_secs, - ); - - let http_client = Arc::new(reqwest::Client::new()); - let max_retries = config.redis_max_retries as u32; - let worker = HookWorker::new(db, cache, consumer, http_client, max_retries, tag_embedder); - - let cancel = CancellationToken::new(); - let cancel_clone = cancel.clone(); - - tokio::spawn(async move { - worker.run(cancel_clone).await; - }); - - cancel -} diff --git a/libs/git/hook/pool/redis.rs b/libs/git/hook/pool/redis.rs deleted file mode 100644 index ed0241a..0000000 --- a/libs/git/hook/pool/redis.rs +++ /dev/null @@ -1,280 +0,0 @@ -use crate::error::GitError; -use crate::hook::pool::types::HookTask; -use deadpool_redis::cluster::Connection as RedisConn; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::time::Duration; - -/// NATS consumer function type: returns (task, ack_fn) pairs. -pub type NatsHookConsumeFn = Arc< - dyn Fn( - String, - usize, - ) -> Pin< - Box< - dyn Future< - Output = anyhow::Result< - Vec<( - Vec, - Box< - dyn Fn() -> Pin< - Box> + Send>, - > + Send, - >, - )>, - >, - > + Send, - >, - > + Send - + Sync, ->; - -/// Redis List consumer using BLMOVE for atomic move-from-queue-to-work pattern. -pub struct RedisConsumer { - pool: deadpool_redis::cluster::Pool, - /// Hash-tag-prefixed key prefix, e.g. "{hook}". - prefix: String, - block_timeout_secs: u64, - /// Optional NATS consume function for JetStream integration. - nats_consume: Option, -} - -const POOL_GET_TIMEOUT: Duration = Duration::from_secs(5); - -impl RedisConsumer { - pub fn new( - pool: deadpool_redis::cluster::Pool, - mut prefix: String, - block_timeout_secs: u64, - ) -> Self { - // Redis Cluster requires hash tags ({...}) for multi-key commands - // like BLMOVE and Lua scripts to ensure keys hash to the same slot. - if !prefix.contains('{') { - prefix = format!("{{{}}}", prefix); - } - Self { - pool, - prefix, - block_timeout_secs, - nats_consume: None, - } - } - - pub fn with_nats( - pool: deadpool_redis::cluster::Pool, - mut prefix: String, - block_timeout_secs: u64, - nats_consume: NatsHookConsumeFn, - ) -> Self { - if !prefix.contains('{') { - prefix = format!("{{{}}}", prefix); - } - Self { - pool, - prefix, - block_timeout_secs, - nats_consume: Some(nats_consume), - } - } - - /// Atomically moves a task from the main queue to the work queue using BLMOVE or NATS. - /// Blocks up to `block_timeout_secs` waiting for a task. - /// - /// Returns `Some((HookTask, task_json))` where `task_json` is the raw JSON string - /// needed for LREM on ACK. Returns `None` if the blocking timed out. - pub async fn next(&self, task_type: &str) -> Result, GitError> { - // Try NATS first if available - if let Some(nats_consume) = &self.nats_consume { - match self.next_nats(task_type, nats_consume).await { - Ok(Some(result)) => return Ok(Some(result)), - Ok(None) => {} // No messages, fall through to Redis - Err(e) => { - tracing::warn!(error = %e, "NATS consume failed, falling back to Redis"); - } - } - } - - // Fallback to Redis List - self.next_redis(task_type).await - } - - async fn next_nats( - &self, - task_type: &str, - nats_consume: &NatsHookConsumeFn, - ) -> Result, GitError> { - let subject = format!("queue.hook.{}", task_type); - let messages = nats_consume(subject, 1) - .await - .map_err(|e| GitError::Internal(format!("NATS consume failed: {}", e)))?; - - if messages.is_empty() { - return Ok(None); - } - - let (data, ack_fn) = messages.into_iter().next().unwrap(); - - match serde_json::from_slice::(&data) { - Ok(task) => { - let task_json = String::from_utf8_lossy(&data).to_string(); - tracing::debug!( - "task dequeued from NATS task_id={} task_type={}", - task.id, - task.task_type - ); - - // Store ack_fn for later use - we'll need to refactor to support async ack - // For now, we'll ack immediately after processing in the worker - Ok(Some((task, task_json))) - } - Err(e) => { - tracing::warn!("malformed task JSON from NATS, discarding error={}", e); - let _ = ack_fn().await; - Ok(None) - } - } - } - - async fn next_redis(&self, task_type: &str) -> Result, GitError> { - let queue_key = format!("{}:{}", self.prefix, task_type); - let work_key = format!("{}:{}:work", self.prefix, task_type); - - let redis = tokio::time::timeout(POOL_GET_TIMEOUT, self.pool.get()) - .await - .map_err(|_| GitError::Internal("redis pool get timed out".into()))? - .map_err(|e| GitError::Internal(format!("redis pool get failed: {}", e)))?; - - let mut conn: RedisConn = redis; - - // BLMOVE source destination timeout - let task_json: Option = redis::cmd("BLMOVE") - .arg(&queue_key) - .arg(&work_key) - .arg("RIGHT") - .arg("LEFT") - .arg(self.block_timeout_secs) - .query_async(&mut conn) - .await - .map_err(|e| GitError::Internal(format!("BLMOVE failed: {}", e)))?; - - match task_json { - Some(json) => { - match serde_json::from_str::(&json) { - Ok(task) => { - tracing::debug!( - "task dequeued task_id={} task_type={} queue={}", - task.id, - task.task_type, - queue_key - ); - Ok(Some((task, json))) - } - Err(e) => { - // Malformed task — remove from work queue and discard - tracing::warn!( - "malformed task JSON, discarding error={} queue={}", - e, - work_key - ); - let _ = self.ack_raw(&work_key, &json).await; - Ok(None) - } - } - } - None => { - // Timed out, no task available - Ok(None) - } - } - } - - /// Acknowledge a task: remove it from the work queue (LREM). - pub async fn ack(&self, work_key: &str, task_json: &str) -> Result<(), GitError> { - self.ack_raw(work_key, task_json).await - } - - async fn ack_raw(&self, work_key: &str, task_json: &str) -> Result<(), GitError> { - let redis = tokio::time::timeout(POOL_GET_TIMEOUT, self.pool.get()) - .await - .map_err(|_| GitError::Internal("redis pool get timed out".into()))? - .map_err(|e| GitError::Internal(format!("redis pool get failed: {}", e)))?; - - let mut conn: RedisConn = redis; - - let _: i64 = redis::cmd("LREM") - .arg(work_key) - .arg(-1) - .arg(task_json) - .query_async(&mut conn) - .await - .map_err(|e| GitError::Internal(format!("LREM failed: {}", e)))?; - - Ok(()) - } - - /// Negative acknowledge (retry): remove from work queue and push back to main queue. - pub async fn nak( - &self, - work_key: &str, - queue_key: &str, - task_json: &str, - ) -> Result<(), GitError> { - self.nak_with_retry(work_key, queue_key, task_json, task_json) - .await - } - - /// Negative acknowledge with a different (updated) task JSON — used to - /// requeue with an incremented retry_count. - /// Uses a Lua script for atomic LREM + LPUSH to prevent task loss on crash. - pub async fn nak_with_retry( - &self, - work_key: &str, - queue_key: &str, - old_task_json: &str, - new_task_json: &str, - ) -> Result<(), GitError> { - let redis = tokio::time::timeout(POOL_GET_TIMEOUT, self.pool.get()) - .await - .map_err(|_| GitError::Internal("redis pool get timed out".into()))? - .map_err(|e| GitError::Internal(format!("redis pool get failed: {}", e)))?; - - let mut conn: RedisConn = redis; - - // Atomic: remove from work queue AND push to retry queue in one script. - // If the process crashes mid-script, either both happen or neither — no lost tasks. - let script = r#" - redis.call("LREM", KEYS[1], 1, ARGV[1]) - redis.call("LPUSH", KEYS[2], ARGV[2]) - return 1 - "#; - - let _: i32 = redis::Script::new(script) - .key(work_key) - .key(queue_key) - .arg(old_task_json) - .arg(new_task_json) - .invoke_async(&mut conn) - .await - .map_err(|e| GitError::Internal(format!("nak script failed: {}", e)))?; - - tracing::warn!("task nack'd and requeued queue={}", queue_key); - - Ok(()) - } - - pub fn prefix(&self) -> &str { - &self.prefix - } -} - -impl Clone for RedisConsumer { - fn clone(&self) -> Self { - Self { - pool: self.pool.clone(), - prefix: self.prefix.clone(), - block_timeout_secs: self.block_timeout_secs, - nats_consume: self.nats_consume.clone(), - } - } -} diff --git a/libs/git/hook/pool/types.rs b/libs/git/hook/pool/types.rs deleted file mode 100644 index 1c6d7de..0000000 --- a/libs/git/hook/pool/types.rs +++ /dev/null @@ -1,32 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub use config::hook::PoolConfig; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HookTask { - pub id: String, - pub repo_id: String, - pub task_type: TaskType, - pub payload: serde_json::Value, - pub created_at: chrono::DateTime, - #[serde(default)] - pub retry_count: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum TaskType { - Sync, - Fsck, - Gc, -} - -impl std::fmt::Display for TaskType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TaskType::Sync => write!(f, "sync"), - TaskType::Fsck => write!(f, "fsck"), - TaskType::Gc => write!(f, "gc"), - } - } -} diff --git a/libs/git/hook/pool/worker.rs b/libs/git/hook/pool/worker.rs deleted file mode 100644 index 088eea3..0000000 --- a/libs/git/hook/pool/worker.rs +++ /dev/null @@ -1,454 +0,0 @@ -use crate::error::GitError; -use crate::hook::embed::TagEmbedder; -use crate::hook::pool::redis::RedisConsumer; -use crate::hook::pool::types::{HookTask, TaskType}; -use crate::hook::sync::HookMetaDataSync; -use db::cache::AppCache; -use db::database::AppDatabase; -use metrics::counter; -use models::EntityTrait; -use models::repos::repo_tag; -use sea_orm::{ColumnTrait, QueryFilter}; -use std::sync::Arc; -use std::time::Duration; - -use tokio_util::sync::CancellationToken; - -/// Git zero OID for new branch/tag creation webhook events. -const ZERO_OID: &str = "0000000000000000000000000000000000000000"; - -/// Single-threaded worker that sequentially consumes tasks from Redis queues. -/// K8s can scale replicas for concurrency — each replica runs one worker. -/// Per-repo Redis locking is managed inside HookMetaDataSync methods. -#[derive(Clone)] -pub struct HookWorker { - db: AppDatabase, - cache: AppCache, - consumer: RedisConsumer, - http_client: Arc, - max_retries: u32, - tag_embedder: Option>, -} - -impl HookWorker { - pub fn new( - db: AppDatabase, - cache: AppCache, - consumer: RedisConsumer, - http_client: Arc, - max_retries: u32, - tag_embedder: Option>, - ) -> Self { - Self { - db, - cache, - consumer, - http_client, - max_retries, - tag_embedder, - } - } - - /// Run the worker loop. Blocks until cancelled. - pub async fn run(&self, cancel: CancellationToken) { - tracing::info!("hook worker started"); - - let task_types = [TaskType::Sync, TaskType::Fsck, TaskType::Gc]; - let mut redis_backoff_ms: u64 = 1000; - - loop { - // Check cancellation at top of loop to avoid unnecessary work - if cancel.is_cancelled() { - tracing::info!("hook worker shutdown signal received"); - break; - } - - for task_type in &task_types { - let result = self.consumer.next(&task_type.to_string()).await; - - let (task, task_json) = match result { - Ok(Some(pair)) => { - // Reset backoff on successful dequeue - redis_backoff_ms = 1000; - pair - } - Ok(None) => continue, - Err(e) => { - tracing::warn!("failed to dequeue task: {}", e); - tokio::time::sleep(Duration::from_millis(redis_backoff_ms)).await; - // Exponential backoff, cap at 32s - redis_backoff_ms = (redis_backoff_ms * 2).min(32_000); - break; - } - }; - - let queue_key = format!("{}:{}", self.consumer.prefix(), task_type); - let work_key = format!("{}:work", queue_key); - - self.process_task(&task, &task_json, &work_key, &queue_key) - .await; - } - } - - tracing::info!("hook worker stopped"); - } - - async fn process_task( - &self, - task: &HookTask, - task_json: &str, - work_key: &str, - queue_key: &str, - ) { - tracing::info!( - "task started task_id={} task_type={} repo_id={}", - task.id, - task.task_type, - task.repo_id - ); - - counter!("hook_tasks_total", "task_type" => task.task_type.to_string()).increment(1); - - let result = match task.task_type { - TaskType::Sync => self.run_sync(&task.repo_id).await, - TaskType::Fsck => self.run_fsck(&task.repo_id).await, - TaskType::Gc => self.run_gc(&task.repo_id).await, - }; - - match result { - Ok(()) => { - counter!("hook_tasks_success_total", "task_type" => task.task_type.to_string()) - .increment(1); - if let Err(e) = self.consumer.ack(work_key, task_json).await { - tracing::warn!("failed to ack task: {}", e); - } - tracing::info!("task completed task_id={}", task.id); - } - Err(e) => { - let is_locked = matches!(e, crate::GitError::Locked(_)); - - if is_locked { - counter!("hook_tasks_locked_total").increment(1); - // Another worker holds the lock — requeue without counting as retry. - tracing::info!( - "repo locked by another worker, requeueing task_id={}", - task.id - ); - if let Err(nak_err) = self.consumer.nak(work_key, queue_key, task_json).await { - tracing::warn!("failed to requeue locked task: {}", nak_err); - } - } else { - counter!("hook_tasks_failed_total", "task_type" => task.task_type.to_string()) - .increment(1); - tracing::warn!( - "task failed task_id={} task_type={} repo_id={} error={}", - task.id, - task.task_type, - task.repo_id, - e - ); - - if task.retry_count >= self.max_retries { - counter!("hook_tasks_exhausted_total").increment(1); - tracing::warn!( - "task exhausted retries, discarding task_id={} retry_count={}", - task.id, - task.retry_count - ); - let _ = self.consumer.ack(work_key, task_json).await; - } else { - counter!("hook_tasks_retried_total").increment(1); - let mut task = task.clone(); - task.retry_count += 1; - let retry_json = - serde_json::to_string(&task).unwrap_or_else(|_| task_json.to_string()); - let _ = self - .consumer - .nak_with_retry(work_key, queue_key, task_json, &retry_json) - .await; - } - } - } - } - } - - async fn run_sync(&self, repo_id: &str) -> Result<(), GitError> { - let repo_uuid = models::Uuid::parse_str(repo_id) - .map_err(|_| GitError::Internal("invalid repo_id uuid".into()))?; - - let repo = models::repos::repo::Entity::find_by_id(repo_uuid) - .one(self.db.reader()) - .await - .map_err(GitError::from)? - .ok_or_else(|| GitError::NotFound(format!("repo {} not found", repo_id)))?; - - if !std::path::Path::new(&repo.storage_path).exists() { - return Err(GitError::NotFound(format!( - "storage path does not exist: {}", - repo.storage_path - ))); - } - - // Build sync once and reuse for before_tips + sync + after_tips - // (avoids opening git2::Repository 3 times) - let db_for_sync = self.db.clone(); - let cache_for_sync = self.cache.clone(); - let repo_for_sync = repo.clone(); - let sync = tokio::task::spawn_blocking(move || { - HookMetaDataSync::new(db_for_sync, cache_for_sync, repo_for_sync) - .map_err(|e| GitError::Internal(e.to_string())) - }) - .await - .map_err(|e| GitError::Internal(format!("spawn_blocking join error: {}", e)))? - .map_err(GitError::from)?; - - // Capture before tips for webhook diff (read-only, no lock needed) - let before_tips = tokio::task::spawn_blocking({ - let sync = sync.clone(); - move || Ok::<_, GitError>((sync.list_branch_tips(), sync.list_tag_tips())) - }) - .await - .map_err(|e| GitError::Internal(format!("spawn_blocking join error: {}", e)))? - .map_err(GitError::from)?; - - // Run full sync (internally acquires/releases per-repo lock) - let sync_clone = sync.clone(); - tokio::task::spawn_blocking(move || { - let result = - tokio::runtime::Handle::current().block_on(async { sync_clone.sync().await }); - match result { - Ok(()) => Ok::<(), GitError>(()), - Err(e) => Err(GitError::Internal(e.to_string())), - } - }) - .await - .map_err(|e| GitError::Internal(format!("spawn_blocking join error: {}", e))) - .and_then(|r| r.map_err(GitError::from))?; - - // Capture after tips for webhook diff (read-only, no lock needed) - let after_tips = tokio::task::spawn_blocking({ - let sync = sync.clone(); - move || Ok::<_, GitError>((sync.list_branch_tips(), sync.list_tag_tips())) - }) - .await - .map_err(|e| GitError::Internal(format!("spawn_blocking join error: {}", e)))? - .map_err(GitError::from)?; - - let (before_branch_tips, before_tag_tips) = before_tips; - let (after_branch_tips, after_tag_tips) = after_tips; - let project = repo.project; - - // Resolve namespace for webhook URL construction - let namespace = models::projects::Project::find_by_id(project) - .one(self.db.reader()) - .await - .inspect_err(|e| tracing::warn!(error = %e, project = %project, "hook sync: failed to resolve project namespace")) - .ok() - .flatten() - .map(|p| p.name) - .unwrap_or_else(|| { - tracing::warn!(project = %project, "hook sync: project not found, empty namespace"); - String::new() - }); - - let repo_id_str = repo.id.to_string(); - let repo_name = repo.repo_name.clone(); - let default_branch = repo.default_branch.clone(); - let http_client = self.http_client.clone(); - let db = self.db.clone(); - - // Dispatch branch webhooks and collect handles - let mut handles = Vec::new(); - let mut branch_changes: u64 = 0; - for (branch, after_oid) in after_branch_tips { - let before_oid = before_branch_tips - .iter() - .find(|(n, _)| n == &branch) - .map(|(_, o)| o.as_str()); - let changed = before_oid.map(|o| o != after_oid.as_str()).unwrap_or(true); - if changed { - branch_changes += 1; - let before_oid = before_oid.map_or(ZERO_OID, |v| v).to_string(); - let branch_name = branch.clone(); - let h = tokio::spawn({ - let http_client = http_client.clone(); - let db = db.clone(); - let repo_id_str = repo_id_str.clone(); - let namespace = namespace.clone(); - let repo_name = repo_name.clone(); - let default_branch = default_branch.clone(); - async move { - crate::hook::webhook_dispatch::dispatch_repo_webhooks( - &db, - &http_client, - &repo_id_str, - &namespace, - &repo_name, - &default_branch, - "", - "", - crate::hook::webhook_dispatch::WebhookEventKind::Push { - r#ref: format!("refs/heads/{}", branch_name), - before: before_oid, - after: after_oid, - commits: vec![], - }, - ) - .await; - } - }); - handles.push(h); - } - } - - // Dispatch tag webhooks and collect handles - let mut tag_changes: u64 = 0; - let mut changed_tag_names: Vec = Vec::new(); - for (tag, after_oid) in after_tag_tips { - let before_oid = before_tag_tips - .iter() - .find(|(n, _)| n == &tag) - .map(|(_, o)| o.as_str()); - let is_new = before_oid.is_none(); - let was_updated = before_oid.map(|o| o != after_oid.as_str()).unwrap_or(false); - if is_new || was_updated { - tag_changes += 1; - changed_tag_names.push(tag.clone()); - let before_oid = before_oid.map_or(ZERO_OID, |v| v).to_string(); - let tag_name = tag.clone(); - let h = tokio::spawn({ - let http_client = http_client.clone(); - let db = db.clone(); - let repo_id_str = repo_id_str.clone(); - let namespace = namespace.clone(); - let repo_name = repo_name.clone(); - let default_branch = default_branch.clone(); - async move { - crate::hook::webhook_dispatch::dispatch_repo_webhooks( - &db, - &http_client, - &repo_id_str, - &namespace, - &repo_name, - &default_branch, - "", - "", - crate::hook::webhook_dispatch::WebhookEventKind::TagPush { - r#ref: format!("refs/tags/{}", tag_name), - before: before_oid, - after: after_oid, - }, - ) - .await; - } - }); - handles.push(h); - } - } - - // Wait for all webhooks to complete before returning - for h in handles { - let _ = h.await; - } - - counter!("hook_sync_branches_changed_total").increment(branch_changes); - counter!("hook_sync_tags_changed_total").increment(tag_changes); - - // Embed changed tags into Qdrant for semantic search (non-blocking, fire-and-forget) - if let Some(ref embedder) = self.tag_embedder { - if !changed_tag_names.is_empty() { - let ed = embedder.clone(); - let db = self.db.clone(); - let repo_uuid = repo_uuid; - let repo_name = repo_name.clone(); - let project_id = project.to_string(); - let tag_names = changed_tag_names.clone(); - tokio::spawn(async move { - let tags = repo_tag::Entity::find() - .filter(repo_tag::Column::Repo.eq(repo_uuid)) - .filter(repo_tag::Column::Name.is_in(tag_names)) - .all(db.reader()) - .await; - - match tags { - Ok(rows) => { - let count = rows.len(); - let inputs: Vec = rows - .into_iter() - .map(|t| models::TagEmbedInput { - repo_id: repo_uuid.to_string(), - repo_name: repo_name.clone(), - project_id: project_id.clone(), - name: t.name, - description: t.description, - }) - .collect(); - if !inputs.is_empty() { - if let Err(e) = ed.embed_tags_batch(inputs).await { - tracing::warn!(error = %e, "failed to embed changed tags"); - } else { - tracing::debug!(count, "embedded changed tags into Qdrant"); - } - } - } - Err(e) => { - tracing::warn!(error = %e, "failed to query changed tags for embedding"); - } - } - }); - } - } - - Ok(()) - } - - async fn run_fsck(&self, repo_id: &str) -> Result<(), GitError> { - let repo_uuid = models::Uuid::parse_str(repo_id) - .map_err(|_| GitError::Internal("invalid repo_id uuid".into()))?; - - let repo = models::repos::repo::Entity::find_by_id(repo_uuid) - .one(self.db.reader()) - .await - .map_err(GitError::from)? - .ok_or_else(|| GitError::NotFound(format!("repo {} not found", repo_id)))?; - - if !std::path::Path::new(&repo.storage_path).exists() { - return Err(GitError::NotFound(format!( - "storage path does not exist: {}", - repo.storage_path - ))); - } - - let db = self.db.clone(); - let cache = self.cache.clone(); - let sync = HookMetaDataSync::new(db, cache, repo)?; - sync.fsck_only().await?; - - Ok(()) - } - - async fn run_gc(&self, repo_id: &str) -> Result<(), GitError> { - let repo_uuid = models::Uuid::parse_str(repo_id) - .map_err(|_| GitError::Internal("invalid repo_id uuid".into()))?; - - let repo = models::repos::repo::Entity::find_by_id(repo_uuid) - .one(self.db.reader()) - .await - .map_err(GitError::from)? - .ok_or_else(|| GitError::NotFound(format!("repo {} not found", repo_id)))?; - - if !std::path::Path::new(&repo.storage_path).exists() { - return Err(GitError::NotFound(format!( - "storage path does not exist: {}", - repo.storage_path - ))); - } - - let db = self.db.clone(); - let cache = self.cache.clone(); - let sync = HookMetaDataSync::new(db, cache, repo)?; - sync.gc_only().await?; - - Ok(()) - } -} diff --git a/libs/git/hook/sync/branch.rs b/libs/git/hook/sync/branch.rs deleted file mode 100644 index 94f6837..0000000 --- a/libs/git/hook/sync/branch.rs +++ /dev/null @@ -1,2 +0,0 @@ -// sync_refs has been moved to commit.rs to consolidate sync helpers. -// Keeping this module stub to avoid breaking existing module references. diff --git a/libs/git/hook/sync/commit.rs b/libs/git/hook/sync/commit.rs deleted file mode 100644 index 8df02b6..0000000 --- a/libs/git/hook/sync/commit.rs +++ /dev/null @@ -1,500 +0,0 @@ -use crate::GitError; -use crate::hook::sync::HookMetaDataSync; -use db::database::AppTransaction; -use models::repos::RepoCollaborator; -use models::repos::RepoCommit; -use models::repos::repo_collaborator; -use models::repos::repo_commit; -use models::users::user_email; -use sea_orm::*; -use sea_query::OnConflict; -use std::collections::{HashMap, HashSet}; - -/// Owned branch data collected from git2 (no git2 types after this). -#[derive(Debug, Clone)] -pub(crate) struct BranchTip { - pub name: String, - pub shorthand: String, - pub target_oid: String, - pub is_branch: bool, - pub is_remote: bool, - pub upstream: Option, -} - -/// Owned tag data collected from git2 (no git2 types after this). -/// Consumed by sync_tags via collect_tag_refs(). -#[derive(Debug, Clone)] -pub(crate) struct TagTip { - pub name: String, - pub target_oid: String, - pub description: Option, - pub tagger_name: String, - pub tagger_email: String, -} - -/// Owned commit data collected from git2 (no git2 types after this). -#[derive(Debug)] -struct CommitData { - oid: String, - author_name: String, - author_email: String, - committer_name: String, - committer_email: String, - message: String, - parent_ids: Vec, -} - -impl HookMetaDataSync { - /// Collect all git2 branch/tag data into owned structs. - /// This is sync and must be called from a `spawn_blocking` context. - pub(crate) fn collect_git_refs(&self) -> Result<(Vec, Vec), GitError> { - let mut branches = Vec::new(); - let mut tags = Vec::new(); - - let references = self - .domain - .repo() - .references() - .map_err(|e| GitError::Internal(e.to_string()))?; - - for ref_result in references { - let reference = match ref_result { - Ok(r) => r, - Err(e) => { - tracing::warn!("failed to read reference error={}", e); - continue; - } - }; - - let name = match reference.name() { - Some(n) => n.to_string(), - None => continue, - }; - - let target_oid = match reference.target() { - Some(oid) => oid.to_string(), - None => continue, - }; - - let shorthand = reference.shorthand().unwrap_or("").to_string(); - let is_branch = reference.is_branch(); - let is_remote = reference.is_remote(); - - if reference.is_tag() { - tags.push(TagTip { - name: name.strip_prefix("refs/tags/").unwrap_or(&name).to_string(), - target_oid, - description: None, - tagger_name: String::new(), - tagger_email: String::new(), - }); - } else if is_branch && !is_remote { - // Try to get upstream branch name from the reference's upstream target - let upstream: Option = if reference.target().is_some() { - if let Ok(branch) = self - .domain - .repo() - .find_branch(&name, git2::BranchType::Local) - { - if let Ok(upstream_ref) = branch.upstream() { - if let Some(upstream_name) = upstream_ref.name().ok().flatten() { - Some(upstream_name.to_string()) - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None - }; - - branches.push(BranchTip { - name, - shorthand, - target_oid, - is_branch, - is_remote, - upstream, - }); - } - } - - Ok((branches, tags)) - } - - pub async fn sync_refs(&self, txn: &AppTransaction) -> Result<(), GitError> { - let repo_id = self.repo.id; - let now = chrono::Utc::now(); - - let existing: Vec = - models::repos::repo_branch::Entity::find() - .filter(models::repos::repo_branch::Column::Repo.eq(repo_id)) - .all(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to query branches: {}", e)))?; - let mut existing_names: HashSet = existing.iter().map(|r| r.name.clone()).collect(); - - let (branches, _) = self.collect_git_refs()?; - - // Preferred default branch names, in priority order. - // git2::References iteration order is filesystem-dependent (not chronological), - // so we MUST NOT use "first branch wins". - const PREFERRED_BRANCHES: &[&str] = &["main", "master", "trunk"]; - - // Auto-detect default branch when empty. - // Re-read from DB inside the transaction to avoid stale reads from concurrent workers. - let mut auto_detected_branch: Option = None; - let current_default: Option = models::repos::repo::Entity::find_by_id(self.repo.id) - .one(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to re-read repo: {}", e)))? - .map(|r| r.default_branch) - .filter(|b| !b.is_empty()); - - if current_default.is_none() { - // Prefer known branch names over first-come - for preferred in PREFERRED_BRANCHES { - if branches - .iter() - .any(|b| b.shorthand == *preferred && b.is_branch && !b.is_remote) - { - auto_detected_branch = Some(ToString::to_string(preferred)); - break; - } - } - // Fallback: first local branch - if auto_detected_branch.is_none() { - if let Some(first) = branches.iter().find(|b| b.is_branch && !b.is_remote) { - auto_detected_branch = Some(first.shorthand.clone()); - } - } - } - - for branch in &branches { - if existing_names.contains(&branch.name) { - existing_names.remove(&branch.name); - models::repos::repo_branch::Entity::update_many() - .filter(models::repos::repo_branch::Column::Repo.eq(repo_id)) - .filter(models::repos::repo_branch::Column::Name.eq(&branch.name)) - .col_expr( - models::repos::repo_branch::Column::Oid, - sea_orm::prelude::Expr::value(&branch.target_oid), - ) - .col_expr( - models::repos::repo_branch::Column::Upstream, - sea_orm::prelude::Expr::value(branch.upstream.clone()), - ) - // head is NOT set here — set below in a single pass to avoid N+1 writes - .col_expr( - models::repos::repo_branch::Column::UpdatedAt, - sea_orm::prelude::Expr::value(now), - ) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to update branch: {}", e)))?; - } else { - let new_branch = models::repos::repo_branch::ActiveModel { - repo: Set(repo_id), - name: Set(branch.name.clone()), - oid: Set(branch.target_oid.clone()), - upstream: Set(branch.upstream.clone()), - // head defaults to false — will be set below if this is the default branch - head: Set(false), - created_at: Set(now), - updated_at: Set(now), - ..Default::default() - }; - new_branch - .insert(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to insert branch: {}", e)))?; - } - } - - if !existing_names.is_empty() { - models::repos::repo_branch::Entity::delete_many() - .filter(models::repos::repo_branch::Column::Repo.eq(repo_id)) - .filter(models::repos::repo_branch::Column::Name.is_in(existing_names)) - .exec(txn) - .await - .map_err(|e| { - GitError::IoError(format!("failed to delete stale branches: {}", e)) - })?; - } - - // Persist auto-detected default branch and update head flags. - // Only writes if default_branch is still empty (prevents concurrent overrides). - if let Some(ref branch_name) = auto_detected_branch { - let updated = models::repos::repo::Entity::update_many() - .filter(models::repos::repo::Column::Id.eq(repo_id)) - .filter(models::repos::repo::Column::DefaultBranch.eq("")) - .col_expr( - models::repos::repo::Column::DefaultBranch, - sea_orm::prelude::Expr::value(branch_name.clone()), - ) - .col_expr( - models::repos::repo::Column::UpdatedAt, - sea_orm::prelude::Expr::value(now), - ) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to set default branch: {}", e)))?; - - if updated.rows_affected > 0 { - models::repos::repo_branch::Entity::update_many() - .filter(models::repos::repo_branch::Column::Repo.eq(repo_id)) - .col_expr( - models::repos::repo_branch::Column::Head, - sea_orm::prelude::Expr::value(false), - ) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to clear head flags: {}", e)))?; - - models::repos::repo_branch::Entity::update_many() - .filter(models::repos::repo_branch::Column::Repo.eq(repo_id)) - .filter(models::repos::repo_branch::Column::Name.eq(branch_name)) - .col_expr( - models::repos::repo_branch::Column::Head, - sea_orm::prelude::Expr::value(true), - ) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to set head flag: {}", e)))?; - } else { - tracing::debug!( - repo_id = %repo_id, - attempted = %branch_name, - "default_branch already set by another worker, skipping" - ); - } - } - - Ok(()) - } - - pub async fn sync_commits(&self, txn: &AppTransaction) -> Result<(), GitError> { - let repo_id = self.repo.id; - let repo = self.domain.repo(); - - if repo.is_empty().unwrap_or(true) { - return Ok(()); - } - - let existing_oids: Vec = RepoCommit::find() - .filter(repo_commit::Column::Repo.eq(repo_id)) - .select_only() - .column(repo_commit::Column::Oid) - .into_tuple() - .all(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to query commits: {}", e)))?; - let existing_set: HashSet = existing_oids.into_iter().collect(); - - let branch_names = self.list_branch_names(); - - let mut new_oid_list: Vec<(git2::Oid, String)> = Vec::new(); - for ref_name in &branch_names { - let mut revwalk = repo - .revwalk() - .map_err(|e| GitError::Internal(e.to_string()))?; - revwalk - .push_ref(ref_name) - .map_err(|e| GitError::Internal(e.to_string()))?; - revwalk - .set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME) - .map_err(|e| GitError::Internal(e.to_string()))?; - for oid_result in revwalk { - let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; - let oid_str = oid.to_string(); - if !existing_set.contains(&oid_str) - && !new_oid_list.iter().any(|(_, s)| s == &oid_str) - { - new_oid_list.push((oid, oid_str)); - } - } - } - - if new_oid_list.is_empty() { - return Ok(()); - } - - let mut author_emails: Vec = Vec::with_capacity(new_oid_list.len()); - let mut committer_emails: Vec = Vec::with_capacity(new_oid_list.len()); - let mut commits_data: Vec = Vec::with_capacity(new_oid_list.len()); - - for (oid, oid_str) in &new_oid_list { - let commit = repo - .find_commit(*oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let author = commit.author(); - let committer = commit.committer(); - - let a_email = author.email().unwrap_or("").to_string(); - let c_email = committer.email().unwrap_or("").to_string(); - - author_emails.push(a_email.clone()); - committer_emails.push(c_email.clone()); - - commits_data.push(CommitData { - oid: oid_str.clone(), - author_name: author.name().unwrap_or("").to_string(), - author_email: a_email, - committer_name: committer.name().unwrap_or("").to_string(), - committer_email: c_email, - message: commit.message().unwrap_or("").to_string(), - parent_ids: commit.parent_ids().map(|p| p.to_string()).collect(), - }); - } - - let user_map = self - .resolve_user_ids(&author_emails, &committer_emails, txn) - .await?; - - let all_emails: Vec<&str> = author_emails - .iter() - .chain(committer_emails.iter()) - .map(|s| s.as_str()) - .collect(); - self.ensure_collaborators(&all_emails, &user_map, txn) - .await?; - - let now = chrono::Utc::now(); - let mut batch = Vec::with_capacity(100); - - for data in commits_data { - let author_uid = user_map.get(&data.author_email).copied(); - let committer_uid = user_map.get(&data.committer_email).copied(); - - batch.push(repo_commit::ActiveModel { - repo: Set(repo_id), - oid: Set(data.oid), - author_name: Set(data.author_name), - author_email: Set(data.author_email), - author: Set(author_uid), - commiter_name: Set(data.committer_name), - commiter_email: Set(data.committer_email), - commiter: Set(committer_uid), - message: Set(data.message), - parent: Set(serde_json::json!(data.parent_ids)), - created_at: Set(now), - ..Default::default() - }); - - if batch.len() >= 100 { - RepoCommit::insert_many(std::mem::take(&mut batch)) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to insert commits: {}", e)))?; - } - } - - if !batch.is_empty() { - RepoCommit::insert_many(batch) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to insert commits: {}", e)))?; - } - - Ok(()) - } - - async fn resolve_user_ids( - &self, - author_emails: &[String], - committer_emails: &[String], - txn: &AppTransaction, - ) -> Result, GitError> { - let mut emails: Vec<&str> = - Vec::with_capacity(author_emails.len() + committer_emails.len()); - for e in author_emails { - emails.push(e.as_str()); - } - for e in committer_emails { - emails.push(e.as_str()); - } - - let rows: Vec<(String, models::UserId)> = user_email::Entity::find() - .filter(user_email::Column::Email.is_in(emails)) - .select_only() - .column(user_email::Column::Email) - .column(user_email::Column::User) - .into_tuple() - .all(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to query user emails: {}", e)))?; - - let mut map = HashMap::new(); - for (email, uid) in rows { - map.insert(email, uid); - } - Ok(map) - } - - async fn ensure_collaborators( - &self, - emails: &[&str], - user_map: &HashMap, - txn: &AppTransaction, - ) -> Result<(), GitError> { - let repo_id = self.repo.id; - - let existing: Vec<(models::UserId,)> = RepoCollaborator::find() - .filter(repo_collaborator::Column::Repo.eq(repo_id)) - .select_only() - .column(repo_collaborator::Column::User) - .into_tuple() - .all(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to query collaborators: {}", e)))?; - let existing_set: HashSet = - existing.into_iter().map(|(uid,)| uid).collect(); - - let now = chrono::Utc::now(); - - for &email in emails { - if let Some(&uid) = user_map.get(email) { - if !existing_set.contains(&uid) { - let new_collab = repo_collaborator::ActiveModel { - repo: Set(repo_id), - user: Set(uid), - scope: Set("read".to_string()), - created_at: Set(now), - ..Default::default() - }; - let _ = RepoCollaborator::insert(new_collab) - .on_conflict( - OnConflict::columns([ - repo_collaborator::Column::Repo, - repo_collaborator::Column::User, - ]) - .do_nothing() - .to_owned(), - ) - .exec(txn) - .await; - } - } - } - - Ok(()) - } - - fn list_branch_names(&self) -> Vec { - let mut names = Vec::new(); - if let Ok(refs) = self.domain.repo().references() { - for r in refs.flatten() { - if r.is_branch() && !r.is_remote() { - if let Some(name) = r.name() { - names.push(name.to_string()); - } - } - } - } - names - } -} diff --git a/libs/git/hook/sync/fsck.rs b/libs/git/hook/sync/fsck.rs deleted file mode 100644 index 800f9ba..0000000 --- a/libs/git/hook/sync/fsck.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::GitError; -use crate::hook::sync::HookMetaDataSync; -use db::database::AppTransaction; -use models::system::notify; -use sea_orm::*; -use std::collections::HashMap; -use std::process::Command; - -impl HookMetaDataSync { - pub async fn run_fsck_and_rollback_if_corrupt( - &self, - txn: &AppTransaction, - ) -> Result<(), GitError> { - let snapshot = self.snapshot_refs(); - let storage_path = self.repo.storage_path.clone(); - - let fsck_errors = tokio::task::spawn_blocking(move || { - let output = Command::new("git") - .arg("-C") - .arg(&storage_path) - .arg("fsck") - .arg("--full") - .output() - .map_err(|e| GitError::IoError(format!("git fsck failed: {}", e)))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - tracing::warn!( - "git fsck failed code={:?} stdout={} stderr={}", - output.status.code(), - stdout, - stderr - ); - return Ok(Some(format!("{}\n{}", stdout, stderr))); - } - Ok::, GitError>(None) - }) - .await - .map_err(|e| GitError::Internal(format!("spawn_blocking join error: {}", e)))??; - - if let Some(errors) = fsck_errors { - self.rollback_refs(&snapshot).await; - - let notification = notify::ActiveModel { - user: Set(self.repo.created_by), - title: Set(format!( - "Repository sync rollback triggered: {}", - self.repo.repo_name - )), - description: Set(Some("Repository integrity check failed".to_string())), - content: Set(format!( - "Repository {} sync failed and has been rolled back.\nError: {}", - self.repo.repo_name, errors - )), - kind: Set(1), - created_at: Set(chrono::Utc::now()), - ..Default::default() - }; - - notify::Entity::insert(notification) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to insert notification: {}", e)))?; - - return Err(GitError::Internal(format!( - "repository corruption detected: {}", - errors - ))); - } - - Ok(()) - } - - fn snapshot_refs(&self) -> HashMap { - let mut snapshot = HashMap::new(); - let repo = self.domain.repo(); - - if let Ok(refs) = repo.references() { - for r in refs.flatten() { - let name = match r.name() { - Some(n) => n.to_string(), - None => continue, - }; - let oid = match r.target() { - Some(o) => o.to_string(), - None => continue, - }; - if name.starts_with("refs/heads/") || name.starts_with("refs/tags/") { - snapshot.insert(name, oid); - } - } - } - snapshot - } - - async fn rollback_refs(&self, snapshot: &HashMap) { - let storage_path = self.repo.storage_path.clone(); - let refs: Vec<(String, String)> = snapshot - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - - let _ = tokio::task::spawn_blocking(move || { - for (ref_name, oid) in &refs { - // git update-ref -m — no old-sha in rollback - let status = Command::new("git") - .arg("-C") - .arg(&storage_path) - .arg("update-ref") - .arg("-m") - .arg("rollback: integrity check failed") - .arg(ref_name) - .arg(oid) - .status(); - - match status { - Ok(s) if s.success() => { - tracing::info!("rolled back ref ref_name={} oid={}", ref_name, oid); - } - Ok(s) => { - tracing::error!( - "failed to rollback ref ref_name={} code={:?}", - ref_name, - s.code() - ); - } - Err(e) => { - tracing::error!("failed to rollback ref ref_name={} error={}", ref_name, e); - } - } - } - }) - .await; - } -} diff --git a/libs/git/hook/sync/gc.rs b/libs/git/hook/sync/gc.rs deleted file mode 100644 index 0723142..0000000 --- a/libs/git/hook/sync/gc.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::GitError; -use crate::hook::sync::HookMetaDataSync; -use std::process::Command; - -impl HookMetaDataSync { - pub async fn run_gc(&self) -> Result<(), GitError> { - let storage_path = self.repo.storage_path.clone(); - - tokio::task::spawn_blocking(move || { - let status = Command::new("git") - .arg("-C") - .arg(&storage_path) - .arg("gc") - .arg("--auto") - .arg("--quiet") - .status() - .map_err(|e| GitError::IoError(format!("git gc failed: {}", e)))?; - - if !status.success() { - // git gc --auto exits non-zero when there's nothing to collect, - // or when another gc is already running — both are benign. - tracing::warn!(code = ?status.code(), "git gc exited with non-zero status"); - } - - Ok::<(), GitError>(()) - }) - .await - .map_err(|e| GitError::Internal(format!("spawn_blocking join error: {}", e)))? - } -} diff --git a/libs/git/hook/sync/lfs.rs b/libs/git/hook/sync/lfs.rs deleted file mode 100644 index 64f8dd7..0000000 --- a/libs/git/hook/sync/lfs.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::GitError; -use crate::hook::sync::HookMetaDataSync; -use db::database::AppTransaction; -use models::repos::repo_lfs_object; -use sea_orm::*; -use std::collections::HashSet; - -impl HookMetaDataSync { - pub async fn sync_lfs_objects(&self, txn: &AppTransaction) -> Result<(), GitError> { - let repo_id = self.repo.id; - - let existing: Vec = repo_lfs_object::Entity::find() - .filter(repo_lfs_object::Column::Repo.eq(repo_id)) - .all(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to query lfs objects: {}", e)))?; - - let mut existing_oids: HashSet = existing.into_iter().map(|o| o.oid).collect(); - - let local_oids = self.domain.lfs_object_list()?; - let now = chrono::Utc::now(); - - let mut new_objects = Vec::new(); - - for oid in local_oids { - let oid_str = oid.as_str().to_string(); - - if existing_oids.contains(&oid_str) { - existing_oids.remove(&oid_str); - continue; - } - - let path = match self.domain.lfs_object_path(&oid) { - Ok(p) => p, - Err(e) => { - tracing::warn!("invalid LFS OID in local objects directory error={}", e); - continue; - } - }; - let size = if let Ok(meta) = std::fs::metadata(&path) { - meta.len() as i64 - } else { - continue; - }; - - let storage_path = path.to_string_lossy().to_string(); - - new_objects.push(repo_lfs_object::ActiveModel { - repo: Set(repo_id), - oid: Set(oid_str), - size: Set(size), - storage_path: Set(storage_path), - uploaded_by: Set(None), - uploaded_at: Set(now), - ..Default::default() - }); - } - - if !new_objects.is_empty() { - // Insert in batches - for chunk in new_objects.chunks(100) { - repo_lfs_object::Entity::insert_many(chunk.to_vec()) - .exec(txn) - .await - .map_err(|e| { - GitError::IoError(format!("failed to insert lfs objects: {}", e)) - })?; - } - } - - // Remove objects that no longer exist on disk - if !existing_oids.is_empty() { - repo_lfs_object::Entity::delete_many() - .filter(repo_lfs_object::Column::Repo.eq(repo_id)) - .filter(repo_lfs_object::Column::Oid.is_in(existing_oids)) - .exec(txn) - .await - .map_err(|e| { - GitError::IoError(format!("failed to delete stale lfs objects: {}", e)) - })?; - } - - Ok(()) - } -} diff --git a/libs/git/hook/sync/lock.rs b/libs/git/hook/sync/lock.rs deleted file mode 100644 index bf7bafc..0000000 --- a/libs/git/hook/sync/lock.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::GitError; -use crate::hook::sync::HookMetaDataSync; - -impl HookMetaDataSync { - const LOCK_TTL_SECS: u64 = 300; - - /// Try to acquire an exclusive lock for this repo. - /// Returns the lock value if acquired, which must be passed to `release_lock`. - pub async fn acquire_lock(&self) -> Result { - let lock_key = format!("git:repo:lock:{}", self.repo.id); - let lock_value = format!("{}:{}", uuid::Uuid::new_v4(), std::process::id()); - - let mut conn = self - .cache - .conn() - .await - .map_err(|e| GitError::IoError(format!("failed to get redis connection: {}", e)))?; - - let result: bool = redis::cmd("SET") - .arg(&lock_key) - .arg(&lock_value) - .arg("NX") - .arg("EX") - .arg(Self::LOCK_TTL_SECS) - .query_async(&mut conn) - .await - .map_err(|e| GitError::IoError(format!("failed to acquire lock: {}", e)))?; - - if result { - Ok(lock_value) - } else { - Err(GitError::Locked(format!( - "repository {} is locked by another process", - self.repo.id - ))) - } - } - - /// Release the lock, but only if we still own it (value matches). - pub async fn release_lock(&self, lock_value: &str) -> Result<(), GitError> { - let lock_key = format!("git:repo:lock:{}", self.repo.id); - - let mut conn = self - .cache - .conn() - .await - .map_err(|e| GitError::IoError(format!("failed to get redis connection: {}", e)))?; - - let script = r#" - if redis.call("get", KEYS[1]) == ARGV[1] then - return redis.call("del", KEYS[1]) - else - return 0 - end - "#; - - let _: i32 = redis::Script::new(script) - .key(&lock_key) - .arg(lock_value) - .invoke_async(&mut conn) - .await - .map_err(|e| GitError::IoError(format!("failed to release lock: {}", e)))?; - - Ok(()) - } -} diff --git a/libs/git/hook/sync/mod.rs b/libs/git/hook/sync/mod.rs deleted file mode 100644 index 8ce746e..0000000 --- a/libs/git/hook/sync/mod.rs +++ /dev/null @@ -1,681 +0,0 @@ -pub mod branch; -pub mod commit; -pub mod fsck; -pub mod gc; -pub mod lfs; -pub mod lock; -pub mod tag; - -use db::cache::AppCache; -use db::database::AppDatabase; -use models::ActiveModelTrait; -use models::RepoId; -use models::projects::project_skill::ActiveModel as SkillActiveModel; -use models::projects::project_skill::{Column as SkillCol, Entity as SkillEntity}; -use models::repos::repo::Model as RepoModel; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set}; -use std::collections::HashMap; -use std::path::Path; - -use crate::GitDomain; - -// ── Skill discovery (local, no service crate dependency) ──────────────────────── - -use sha1::Digest; - -const SKILL_ROOTS: &[(&str, &str)] = &[(".claude/skills", "claude"), (".codex/skills", "codex")]; -const ROOT_SKILL_SYSTEM: &str = "root"; - -fn should_descend_dir(name: &str) -> bool { - name != ".git" -} - -/// Recursively scan supported skill locations for files named `SKILL.md`. -/// Root-level skill packs keep the legacy slug `{short_repo_id}/{skill_dir}`. -/// System skills use `{short_repo_id}/{system}/{relative_skill_dir}`. -fn scan_skills_from_dir( - base: &Path, - repo_id: &RepoId, - commit_sha: &str, -) -> Result, std::io::Error> { - let repo_id_prefix = &repo_id.to_string()[..8]; - let mut discovered = Vec::new(); - - for (root, system) in SKILL_ROOTS { - let root_path = base.join(root); - if root_path.exists() { - scan_skill_root_from_dir( - &root_path, - repo_id_prefix, - system, - root, - commit_sha, - &mut discovered, - ); - } - } - scan_root_skill_pack_from_dir(base, repo_id_prefix, commit_sha, &mut discovered); - - Ok(discovered) -} - -fn scan_skill_root_from_dir( - root_path: &Path, - repo_id_prefix: &str, - system: &str, - root: &str, - commit_sha: &str, - discovered: &mut Vec, -) { - let mut stack = vec![root_path.to_path_buf()]; - while let Some(dir) = stack.pop() { - let entries = match std::fs::read_dir(&dir) { - Ok(e) => e, - Err(_) => continue, - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - stack.push(path); - continue; - } - if !is_skill_file_name(&path) { - continue; - } - let Some(parent) = path.parent() else { - continue; - }; - let relative_skill_dir = parent - .strip_prefix(root_path) - .ok() - .and_then(path_to_slug) - .filter(|s| !s.is_empty()); - let Some(relative_skill_dir) = relative_skill_dir else { - continue; - }; - let slug = format!("{}/{}/{}", repo_id_prefix, system, relative_skill_dir); - if let Ok(raw) = std::fs::read(&path) { - let blob_hash = git_blob_hash(&raw); - let mut skill = parse_skill_content(&slug, &raw); - skill.commit_sha = Some(commit_sha.to_string()); - skill.blob_hash = Some(blob_hash); - skill.metadata = enrich_metadata( - skill.metadata, - system, - Some(&format!("{}/{}/SKILL.md", root, relative_skill_dir)), - ); - discovered.push(skill); - } - } - } -} - -fn scan_root_skill_pack_from_dir( - base: &Path, - repo_id_prefix: &str, - commit_sha: &str, - discovered: &mut Vec, -) { - let entries = match std::fs::read_dir(base) { - Ok(e) => e, - Err(_) => return, - }; - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) else { - continue; - }; - if dir_name == ".git" || dir_name == ".claude" || dir_name == ".codex" { - continue; - } - let skill_file = path.join("SKILL.md"); - if !skill_file.exists() { - continue; - } - let relative_skill_dir = slugify_segment(dir_name); - if relative_skill_dir.is_empty() { - continue; - } - let slug = format!("{}/{}", repo_id_prefix, relative_skill_dir); - if let Ok(raw) = std::fs::read(&skill_file) { - let blob_hash = git_blob_hash(&raw); - let mut skill = parse_skill_content(&slug, &raw); - skill.commit_sha = Some(commit_sha.to_string()); - skill.blob_hash = Some(blob_hash); - skill.metadata = enrich_metadata( - skill.metadata, - ROOT_SKILL_SYSTEM, - Some(&format!("{}/SKILL.md", relative_skill_dir)), - ); - discovered.push(skill); - } - } -} - -fn git_blob_hash(content: &[u8]) -> String { - let size = content.len(); - let header = format!("blob {}\0", size); - let mut hasher = sha1::Sha1::new(); - hasher.update(header.as_bytes()); - hasher.update(content); - hex::encode(hasher.finalize()) -} - -fn parse_frontmatter(frontmatter: Option<&str>) -> serde_json::Value { - frontmatter - .and_then(|fm| serde_json::from_str(fm).ok()) - .or_else(|| frontmatter.and_then(|fm| serde_yaml::from_str(fm).ok())) - .unwrap_or_default() -} - -fn parse_skill_content(slug: &str, raw: &[u8]) -> DiscoveredSkill { - let content = String::from_utf8_lossy(raw); - let (frontmatter, body) = extract_frontmatter(&content); - let metadata = parse_frontmatter(frontmatter); - - let name = metadata - .get("name") - .and_then(|v| v.as_str()) - .map(String::from) - .unwrap_or_else(|| slug.replace('-', " ").replace('_', " ")); - - let description = metadata - .get("description") - .and_then(|v| v.as_str()) - .map(String::from); - - DiscoveredSkill { - slug: slug.to_string(), - name, - description, - content: body.trim().to_string(), - metadata, - commit_sha: None, - blob_hash: None, - } -} - -struct DiscoveredSkill { - slug: String, - name: String, - description: Option, - content: String, - metadata: serde_json::Value, - commit_sha: Option, - blob_hash: Option, -} - -fn is_skill_file_name(path: &Path) -> bool { - path.file_name() - .and_then(|n| n.to_str()) - .is_some_and(|name| name.eq_ignore_ascii_case("SKILL.md")) -} - -fn path_to_slug(path: &Path) -> Option { - let parts: Vec = path - .components() - .filter_map(|c| c.as_os_str().to_str()) - .map(slugify_segment) - .filter(|s| !s.is_empty()) - .collect(); - (!parts.is_empty()).then(|| parts.join("/")) -} - -fn slugify_segment(input: &str) -> String { - let mut out = String::with_capacity(input.len()); - let mut last_dash = false; - for ch in input.chars() { - let ch = ch.to_ascii_lowercase(); - if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' { - out.push(ch); - last_dash = false; - } else if !last_dash { - out.push('-'); - last_dash = true; - } - } - out.trim_matches('-').to_string() -} - -fn enrich_metadata( - mut metadata: serde_json::Value, - system: &str, - relative_path: Option<&str>, -) -> serde_json::Value { - if !metadata.is_object() { - metadata = serde_json::json!({}); - } - if let Some(obj) = metadata.as_object_mut() { - obj.entry("system") - .or_insert_with(|| serde_json::Value::String(system.to_string())); - if let Some(relative_path) = relative_path { - obj.entry("path") - .or_insert_with(|| serde_json::Value::String(relative_path.to_string())); - } - } - metadata -} - -fn extract_frontmatter(raw: &str) -> (Option<&str>, &str) { - let trimmed = raw.trim_start(); - if !trimmed.starts_with("---") { - return (None, trimmed); - } - if let Some(end) = trimmed[3..].find("---") { - let fm = &trimmed[3..end + 3]; - let rest = trimmed[3 + end + 3..].trim_start(); - (Some(fm), rest) - } else { - (None, trimmed) - } -} - -/// Scan git tree objects for `SKILL.md` files (works for bare repos). -fn scan_skills_from_tree( - git_repo: &git2::Repository, - repo_id: &RepoId, - commit_sha: &str, -) -> Result, String> { - let repo_id_prefix = &repo_id.to_string()[..8]; - let head = git_repo.head().map_err(|e| format!("no HEAD: {e}"))?; - let tree = head.peel_to_tree().map_err(|e| format!("no tree: {e}"))?; - - let mut discovered = Vec::new(); - let mut stack: Vec<(git2::Tree<'_>, String)> = vec![(tree, String::new())]; - - while let Some((current_tree, prefix)) = stack.pop() { - for entry in current_tree.iter() { - let name = match entry.name() { - Some(n) => n, - None => continue, - }; - let entry_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - - match entry.kind() { - Some(git2::ObjectType::Tree) => { - if should_descend_dir(name) { - if let Ok(subtree) = - entry.to_object(git_repo).and_then(|o| o.peel_to_tree()) - { - stack.push((subtree, entry_path)); - } - } - } - Some(git2::ObjectType::Blob) if name.eq_ignore_ascii_case("SKILL.md") => { - let Some((system, relative_skill_dir, legacy_slug)) = - skill_location_from_path(&entry_path) - else { - continue; - }; - - let slug = if legacy_slug { - format!("{}/{}", repo_id_prefix, relative_skill_dir) - } else { - format!("{}/{}/{}", repo_id_prefix, system, relative_skill_dir) - }; - if let Ok(blob) = entry.to_object(git_repo).and_then(|o| o.peel_to_blob()) { - let raw = blob.content(); - let blob_hash = git_blob_hash(raw); - let mut skill = parse_skill_content(&slug, raw); - skill.commit_sha = Some(commit_sha.to_string()); - skill.blob_hash = Some(blob_hash); - skill.metadata = enrich_metadata(skill.metadata, system, Some(&entry_path)); - discovered.push(skill); - } - } - _ => {} - } - } - } - - Ok(discovered) -} - -fn skill_location_from_path(path: &str) -> Option<(&'static str, String, bool)> { - let normalized = path.replace('\\', "/"); - for (root, system) in SKILL_ROOTS { - let prefix = format!("{}/", root); - let suffix = "/SKILL.md"; - if normalized.starts_with(&prefix) && normalized.ends_with(suffix) { - let relative = &normalized[prefix.len()..normalized.len() - suffix.len()]; - let slug = relative - .split('/') - .map(slugify_segment) - .filter(|s| !s.is_empty()) - .collect::>() - .join("/"); - if !slug.is_empty() { - return Some((*system, slug, false)); - } - } - } - - let suffix = "/SKILL.md"; - if normalized.ends_with(suffix) && !normalized.starts_with('.') { - let relative = &normalized[..normalized.len() - suffix.len()]; - if !relative.contains('/') { - let slug = slugify_segment(relative); - if !slug.is_empty() { - return Some((ROOT_SKILL_SYSTEM, slug, true)); - } - } - } - - None -} - -#[derive(Clone)] -pub struct HookMetaDataSync { - pub db: AppDatabase, - pub cache: AppCache, - pub repo: RepoModel, - pub domain: GitDomain, -} - -impl HookMetaDataSync { - pub fn new(db: AppDatabase, cache: AppCache, repo: RepoModel) -> Result { - let domain = GitDomain::from_model(repo.clone())?; - Ok(Self { - db, - cache, - repo, - domain, - }) - } - - /// Full sync with lock. Caller (worker) manages locking. - pub async fn sync(&self) -> Result<(), crate::GitError> { - let lock_value = self.acquire_lock().await?; - - let res = self.sync_work().await; - - if let Err(ref e) = res { - tracing::error!("sync failed error={}", e); - } - - let _ = self.release_lock(&lock_value).await; - res - } - - /// Fsck only with lock. Caller manages locking. - pub async fn fsck_only(&self) -> Result<(), crate::GitError> { - let lock_value = self.acquire_lock().await?; - - let res = self.fsck_work().await; - - let _ = self.release_lock(&lock_value).await; - res - } - - /// GC only with lock. Caller manages locking. - pub async fn gc_only(&self) -> Result<(), crate::GitError> { - let lock_value = self.acquire_lock().await?; - - let res = self.gc_work().await; - - let _ = self.release_lock(&lock_value).await; - res - } - - /// Full sync pipeline (no locking — caller is responsible). - async fn sync_work(&self) -> Result<(), crate::GitError> { - let mut txn = - self.db.begin().await.map_err(|e| { - crate::GitError::IoError(format!("failed to begin transaction: {}", e)) - })?; - - self.sync_refs(&mut txn).await?; - self.sync_commits(&mut txn).await?; - self.sync_tags(&mut txn).await?; - self.sync_lfs_objects(&mut txn).await?; - self.run_fsck_and_rollback_if_corrupt(&mut txn).await?; - - txn.commit().await.map_err(|e| { - crate::GitError::IoError(format!("failed to commit transaction: {}", e)) - })?; - - self.run_gc().await?; - self.sync_skills().await; - - Ok(()) - } - - /// Fsck only work (no locking — caller is responsible). - async fn fsck_work(&self) -> Result<(), crate::GitError> { - let mut txn = - self.db.begin().await.map_err(|e| { - crate::GitError::IoError(format!("failed to begin transaction: {}", e)) - })?; - - self.run_fsck_and_rollback_if_corrupt(&mut txn).await?; - - txn.commit().await.map_err(|e| { - crate::GitError::IoError(format!("failed to commit transaction: {}", e)) - })?; - - Ok(()) - } - - /// GC only work (no locking — caller is responsible). - async fn gc_work(&self) -> Result<(), crate::GitError> { - self.run_gc().await - } - - /// Returns a list of (branch_name, oid) for all local branches. - pub fn list_branch_tips(&self) -> Vec<(String, String)> { - let repo = self.domain.repo(); - let mut tips = Vec::new(); - if let Ok(refs) = repo.references() { - for ref_result in refs { - if let Ok(r) = ref_result { - if r.is_branch() && !r.is_remote() { - if let Some(name) = r.name() { - let branch = name.strip_prefix("refs/heads/").unwrap_or(name); - if let Some(target) = r.target() { - tips.push((branch.to_string(), target.to_string())); - } - } - } - } - } - } - tips - } - - /// Returns a list of (tag_name, oid) for all tags. - pub fn list_tag_tips(&self) -> Vec<(String, String)> { - let repo = self.domain.repo(); - let mut tips = Vec::new(); - if let Ok(refs) = repo.references() { - for ref_result in refs { - if let Ok(r) = ref_result { - if r.is_tag() { - if let Some(name) = r.name() { - let tag = name.strip_prefix("refs/tags/").unwrap_or(name); - if let Some(target) = r.target() { - tips.push((tag.to_string(), target.to_string())); - } - } - } - } - } - } - tips - } - - /// Scan the repository for `SKILL.md` files and sync skills to the project. - /// Best-effort — failures are logged but do not fail the sync. - pub async fn sync_skills(&self) { - let project_uid = self.repo.project; - let git_repo = self.domain.repo(); - - let commit_sha = git_repo - .head() - .ok() - .and_then(|h| h.target()) - .map(|oid| oid.to_string()) - .unwrap_or_default(); - - let repo_id = self.repo.id; - let is_bare = git_repo.is_bare() || git_repo.workdir().is_none(); - - let discovered = if is_bare { - // Bare repo: scan git tree objects directly - let git_repo_ref = self.domain.repo(); - match scan_skills_from_tree(git_repo_ref, &repo_id, &commit_sha) { - Ok(d) => d, - Err(e) => { - tracing::warn!("failed to scan skills from tree error={}", e); - return; - } - } - } else { - // Normal repo: walk filesystem - let repo_root = match git_repo.workdir() { - Some(path) => path.to_path_buf(), - None => { - tracing::warn!("workdir not available for non-bare repo"); - return; - } - }; - match tokio::task::spawn_blocking(move || { - scan_skills_from_dir(&repo_root, &repo_id, &commit_sha) - }) - .await - { - Ok(Ok(d)) => d, - Ok(Err(e)) => { - tracing::warn!("failed to scan skills directory error={}", e); - return; - } - Err(e) => { - tracing::warn!("spawn_blocking join error error={}", e); - return; - } - } - }; - - if discovered.is_empty() { - return; - } - - let now = chrono::Utc::now(); - let mut created = 0i64; - let mut updated = 0i64; - let mut removed = 0i64; - - let existing: Vec<_> = match SkillEntity::find() - .filter(SkillCol::ProjectUuid.eq(project_uid)) - .filter(SkillCol::Source.eq("repo")) - .filter(SkillCol::RepoId.eq(self.repo.id)) - .all(&self.db) - .await - { - Ok(e) => e, - Err(e) => { - tracing::warn!("failed to query existing skills error={}", e); - return; - } - }; - - // Deduplicate by stable slug. Blob hash changes when content changes and must not be the - // upsert key because project_skill has a unique (project_uuid, slug) constraint. - let mut deduped: std::collections::HashMap = - std::collections::HashMap::new(); - for skill in discovered { - match deduped.get(&skill.slug) { - Some(existing) => { - if skill.commit_sha.as_ref().unwrap_or(&String::new()) - > existing.commit_sha.as_ref().unwrap_or(&String::new()) - { - deduped.insert(skill.slug.clone(), skill); - } - } - None => { - deduped.insert(skill.slug.clone(), skill); - } - } - } - - let existing_by_slug: HashMap<_, _> = - existing.into_iter().map(|s| (s.slug.clone(), s)).collect(); - - let mut seen_keys = std::collections::HashSet::new(); - - for (key, skill) in deduped { - seen_keys.insert(key.clone()); - let json_meta = serde_json::to_value(&skill.metadata).unwrap_or_default(); - - if let Some(existing_skill) = existing_by_slug.get(&key) { - if existing_skill.content != skill.content - || existing_skill.metadata != json_meta - || existing_skill.commit_sha != skill.commit_sha - || existing_skill.blob_hash != skill.blob_hash - || existing_skill.name != skill.name - || existing_skill.description != skill.description - { - let mut active: SkillActiveModel = existing_skill.clone().into(); - active.name = Set(skill.name); - active.description = Set(skill.description); - active.content = Set(skill.content); - active.metadata = Set(json_meta); - active.commit_sha = Set(skill.commit_sha); - active.blob_hash = Set(skill.blob_hash); - active.updated_at = Set(now); - if active.update(&self.db).await.is_ok() { - updated += 1; - } - } - } else { - let active = SkillActiveModel { - id: Set(0), - project_uuid: Set(project_uid), - slug: Set(skill.slug.clone()), - name: Set(skill.name), - description: Set(skill.description), - source: Set("repo".to_string()), - repo_id: Set(Some(self.repo.id)), - commit_sha: Set(skill.commit_sha), - blob_hash: Set(skill.blob_hash), - content: Set(skill.content), - metadata: Set(json_meta), - enabled: Set(true), - created_by: Set(None), - created_at: Set(now), - updated_at: Set(now), - }; - if SkillEntity::insert(active).exec(&self.db).await.is_ok() { - created += 1; - } - } - } - - for (key, old_skill) in existing_by_slug { - if !seen_keys.contains(&key) { - if SkillEntity::delete_by_id(old_skill.id) - .exec(&self.db) - .await - .is_ok() - { - removed += 1; - } - } - } - - if created > 0 || updated > 0 || removed > 0 { - tracing::info!( - "skills synced created={} updated={} removed={}", - created, - updated, - removed - ); - } - } -} diff --git a/libs/git/hook/sync/tag.rs b/libs/git/hook/sync/tag.rs deleted file mode 100644 index d7b9bf4..0000000 --- a/libs/git/hook/sync/tag.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::GitError; -use crate::hook::sync::HookMetaDataSync; -use crate::hook::sync::commit::TagTip; -use db::database::AppTransaction; -use models::repos::repo_tag; -use sea_orm::prelude::Expr; -use sea_orm::*; -use std::collections::HashSet; - -impl HookMetaDataSync { - /// Collect all tag metadata from git2 into owned structs. - /// This is sync and must be called from a `spawn_blocking` context. - pub(crate) fn collect_tag_refs(&self) -> Result, GitError> { - let repo = self.domain.repo(); - let tag_names = repo - .tag_names(None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut tags = Vec::new(); - for tag_name in tag_names.iter().flatten() { - let full_ref = format!("refs/tags/{}", tag_name); - let reference = match repo.find_reference(&full_ref) { - Ok(r) => r, - Err(_) => continue, - }; - - let target_oid = match reference.target() { - Some(oid) => oid.to_string(), - None => continue, - }; - - let (description, tagger_name, tagger_email) = if reference.is_tag() { - if let Ok(tag) = reference.peel_to_tag() { - let description = tag.message().map(|s| s.to_string()); - if let Some(tagger) = tag.tagger() { - ( - description, - tagger.name().unwrap_or("").to_string(), - tagger.email().unwrap_or("").to_string(), - ) - } else { - (description, String::new(), String::new()) - } - } else { - (None, String::new(), String::new()) - } - } else { - (None, String::new(), String::new()) - }; - - tags.push(TagTip { - name: tag_name.to_string(), - target_oid, - description, - tagger_name, - tagger_email, - }); - } - - Ok(tags) - } - - pub async fn sync_tags(&self, txn: &AppTransaction) -> Result<(), GitError> { - let repo_id = self.repo.id; - - let existing: Vec = repo_tag::Entity::find() - .filter(repo_tag::Column::Repo.eq(repo_id)) - .all(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to query tags: {}", e)))?; - let mut existing_names: HashSet = existing.iter().map(|t| t.name.clone()).collect(); - - let tags = self.collect_tag_refs()?; - - for tag in tags { - if existing_names.contains(&tag.name) { - existing_names.remove(&tag.name); - repo_tag::Entity::update_many() - .filter(repo_tag::Column::Repo.eq(repo_id)) - .filter(repo_tag::Column::Name.eq(&tag.name)) - .col_expr(repo_tag::Column::Oid, Expr::value(&tag.target_oid)) - .col_expr( - repo_tag::Column::Description, - Expr::value(tag.description.clone()), - ) - .col_expr(repo_tag::Column::TaggerName, Expr::value(&tag.tagger_name)) - .col_expr( - repo_tag::Column::TaggerEmail, - Expr::value(&tag.tagger_email), - ) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to update tag: {}", e)))?; - } else { - let new_tag = repo_tag::ActiveModel { - repo: Set(repo_id), - name: Set(tag.name.clone()), - oid: Set(tag.target_oid), - color: Set(None), - description: Set(tag.description.clone()), - created_at: Set(chrono::Utc::now()), - tagger_name: Set(tag.tagger_name.clone()), - tagger_email: Set(tag.tagger_email.clone()), - tagger: Set(None), - ..Default::default() - }; - new_tag - .insert(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to insert tag: {}", e)))?; - } - } - - if !existing_names.is_empty() { - repo_tag::Entity::delete_many() - .filter(repo_tag::Column::Repo.eq(repo_id)) - .filter(repo_tag::Column::Name.is_in(existing_names)) - .exec(txn) - .await - .map_err(|e| GitError::IoError(format!("failed to delete stale tags: {}", e)))?; - } - - Ok(()) - } -} diff --git a/libs/git/hook/webhook_dispatch.rs b/libs/git/hook/webhook_dispatch.rs deleted file mode 100644 index fa083dc..0000000 --- a/libs/git/hook/webhook_dispatch.rs +++ /dev/null @@ -1,441 +0,0 @@ -use db::database::AppDatabase; -use serde::Deserialize; -use sha2::{Digest, Sha256}; -use std::time::Duration; -use tokio::time::timeout; - -/// Compute HMAC-SHA256 of `body` with `secret`, returning "sha256=" or None if secret is empty. -pub fn sign_payload(body: &[u8], secret: &str) -> Option { - if secret.is_empty() { - return None; - } - - // HMAC-SHA256: inner = SHA256(k XOR ipad || text), outer = SHA256(k XOR opad || inner) - const IPAD: u8 = 0x36; - const OPAD: u8 = 0x5c; - const BLOCK_SIZE: usize = 64; // SHA256 block size - - // Pad or hash key to 64 bytes - let key = if secret.len() > BLOCK_SIZE { - Sha256::digest(secret.as_bytes()).to_vec() - } else { - secret.as_bytes().to_vec() - }; - let mut key_block = vec![0u8; BLOCK_SIZE]; - key_block[..key.len()].copy_from_slice(&key); - - // k_ipad = key_block XOR ipad, k_opad = key_block XOR opad - let mut k_ipad = [0u8; BLOCK_SIZE]; - let mut k_opad = [0u8; BLOCK_SIZE]; - for i in 0..BLOCK_SIZE { - k_ipad[i] = key_block[i] ^ IPAD; - k_opad[i] = key_block[i] ^ OPAD; - } - - // inner = SHA256(k_ipad || body) - let mut inner_hasher = Sha256::new(); - inner_hasher.update(&k_ipad); - inner_hasher.update(body); - let inner = inner_hasher.finalize(); - - // outer = SHA256(k_opad || inner) - let mut outer_hasher = Sha256::new(); - outer_hasher.update(&k_opad); - outer_hasher.update(inner); - let result = outer_hasher.finalize(); - - Some(format!( - "sha256={}", - result - .iter() - .map(|b| format!("{:02x}", b)) - .collect::() - )) -} - -#[derive(Debug, Clone, Default, Deserialize)] -pub struct WebhookEvents { - pub push: bool, - pub tag_push: bool, - pub pull_request: bool, - pub issue_comment: bool, - pub release: bool, -} - -impl From for WebhookEvents { - fn from(v: serde_json::Value) -> Self { - Self { - push: v.get("push").and_then(|v| v.as_bool()).unwrap_or(false), - tag_push: v.get("tag_push").and_then(|v| v.as_bool()).unwrap_or(false), - pull_request: v - .get("pull_request") - .and_then(|v| v.as_bool()) - .unwrap_or(false), - issue_comment: v - .get("issue_comment") - .and_then(|v| v.as_bool()) - .unwrap_or(false), - release: v.get("release").and_then(|v| v.as_bool()).unwrap_or(false), - } - } -} - -#[derive(Debug, serde::Serialize)] -pub struct PushPayload { - #[serde(rename = "ref")] - pub r#ref: String, - pub before: String, - pub after: String, - pub repository: RepositoryPayload, - pub pusher: PusherPayload, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub commits: Vec, -} - -#[derive(Debug, serde::Serialize)] -pub struct TagPushPayload { - #[serde(rename = "ref")] - pub r#ref: String, - pub before: String, - pub after: String, - pub repository: RepositoryPayload, - pub pusher: PusherPayload, -} - -#[derive(Debug, serde::Serialize)] -pub struct RepositoryPayload { - pub id: String, - pub name: String, - pub full_name: String, - pub namespace: String, - pub default_branch: String, -} - -#[derive(Debug, serde::Serialize)] -pub struct PusherPayload { - pub name: String, - pub email: String, -} - -#[derive(Debug, serde::Serialize)] -pub struct CommitPayload { - pub id: String, - pub message: String, - pub author: AuthorPayload, -} - -#[derive(Debug, serde::Serialize)] -pub struct AuthorPayload { - pub name: String, - pub email: String, -} - -#[derive(Debug)] -pub enum DispatchError { - Timeout, - ConnectionFailed, - RequestFailed(String), - HttpError(u16), -} - -impl std::fmt::Display for DispatchError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DispatchError::Timeout => write!(f, "timeout"), - DispatchError::ConnectionFailed => write!(f, "connection failed"), - DispatchError::RequestFailed(s) => write!(f, "request failed: {}", s), - DispatchError::HttpError(code) => write!(f, "http error: {}", code), - } - } -} - -pub async fn deliver( - client: &reqwest::Client, - url: &str, - secret: Option<&str>, - content_type: &str, - body: &[u8], -) -> Result<(), DispatchError> { - let mut req = client - .post(url) - .header("Content-Type", content_type) - .header("User-Agent", "Code-Git-Hook/1.0") - .timeout(Duration::from_secs(10)) - .body(body.to_vec()); - - if let Some(secret) = secret { - if let Some(sig) = sign_payload(body, secret) { - req = req.header("X-Hub-Signature-256", sig); - } - } - - let resp = req.send().await.map_err(|e| { - if e.is_timeout() { - DispatchError::Timeout - } else if e.is_connect() { - DispatchError::ConnectionFailed - } else { - DispatchError::RequestFailed(e.to_string()) - } - })?; - - if resp.status().is_success() { - Ok(()) - } else { - Err(DispatchError::HttpError(resp.status().as_u16())) - } -} - -pub struct CommitDispatch { - pub id: String, - pub message: String, - pub author_name: String, - pub author_email: String, -} - -pub enum WebhookEventKind { - Push { - r#ref: String, - before: String, - after: String, - commits: Vec, - }, - TagPush { - r#ref: String, - before: String, - after: String, - }, -} - -/// Dispatch webhooks for a repository after a push or tag event. -/// Queries active webhooks from the DB and sends HTTP POST requests. -pub async fn dispatch_repo_webhooks( - db: &AppDatabase, - http: &reqwest::Client, - repo_uuid: &str, - namespace: &str, - repo_name: &str, - default_branch: &str, - pusher_name: &str, - pusher_email: &str, - event: WebhookEventKind, -) { - use models::repos::repo_webhook::{Column as RwCol, Entity as RepoWebhookEntity}; - use models::{ColumnTrait, EntityTrait, QueryFilter, Uuid}; - - let webhooks: Vec<::Model> = match RepoWebhookEntity::find() - .filter(RwCol::Repo.eq(Uuid::parse_str(repo_uuid).ok())) - .all(db.reader()) - .await - { - Ok(ws) => ws, - Err(e) => { - tracing::error!("failed to query webhooks repo={} error={}", repo_uuid, e); - return; - } - }; - - if webhooks.is_empty() { - return; - } - - for webhook in webhooks { - let event_config: WebhookEvents = - serde_json::from_value(webhook.event.clone()).unwrap_or_default(); - let content_type = webhook - .event - .get("content_type") - .and_then(|v: &serde_json::Value| v.as_str()) - .unwrap_or("application/json"); - let url = webhook.url.as_deref().unwrap_or(""); - - if url.is_empty() { - continue; - } - - let secret = webhook.secret_key.as_deref(); - - match &event { - WebhookEventKind::Push { - r#ref, - before, - after, - commits, - } => { - if !event_config.push { - continue; - } - let payload = PushPayload { - r#ref: r#ref.clone(), - before: before.clone(), - after: after.clone(), - repository: RepositoryPayload { - id: repo_uuid.to_owned(), - name: repo_name.to_owned(), - full_name: format!("{}/{}", namespace, repo_name), - namespace: namespace.to_owned(), - default_branch: default_branch.to_owned(), - }, - pusher: PusherPayload { - name: pusher_name.to_owned(), - email: pusher_email.to_owned(), - }, - commits: commits - .iter() - .map(|c| CommitPayload { - id: c.id.clone(), - message: c.message.clone(), - author: AuthorPayload { - name: c.author_name.clone(), - email: c.author_email.clone(), - }, - }) - .collect(), - }; - - let body = match serde_json::to_vec(&payload) { - Ok(b) => b, - Err(e) => { - tracing::error!("failed to serialize push payload error={}", e); - continue; - } - }; - - let webhook_id = webhook.id; - match timeout( - Duration::from_secs(10), - deliver(http, url, secret, content_type, &body), - ) - .await - { - Ok(Ok(())) => { - tracing::info!( - "push webhook delivered webhook_id={} url={}", - webhook_id, - url - ); - let _ = touch_webhook(db, webhook_id, true).await; - } - Ok(Err(e)) => { - tracing::warn!( - "push webhook delivery failed webhook_id={} url={} error={}", - webhook_id, - url, - e - ); - let _ = touch_webhook(db, webhook_id, false).await; - } - Err(_) => { - tracing::warn!( - "push webhook timed out webhook_id={} url={}", - webhook_id, - url - ); - let _ = touch_webhook(db, webhook_id, false).await; - } - } - } - WebhookEventKind::TagPush { - r#ref, - before, - after, - } => { - if !event_config.tag_push { - continue; - } - let payload = TagPushPayload { - r#ref: r#ref.clone(), - before: before.clone(), - after: after.clone(), - repository: RepositoryPayload { - id: repo_uuid.to_owned(), - name: repo_name.to_owned(), - full_name: format!("{}/{}", namespace, repo_name), - namespace: namespace.to_owned(), - default_branch: default_branch.to_owned(), - }, - pusher: PusherPayload { - name: pusher_name.to_owned(), - email: pusher_email.to_owned(), - }, - }; - - let body = match serde_json::to_vec(&payload) { - Ok(b) => b, - Err(e) => { - tracing::error!("failed to serialize tag payload error={}", e); - continue; - } - }; - - let webhook_id = webhook.id; - match timeout( - Duration::from_secs(10), - deliver(http, url, secret, content_type, &body), - ) - .await - { - Ok(Ok(())) => { - tracing::info!( - "tag webhook delivered webhook_id={} url={}", - webhook_id, - url - ); - let _ = touch_webhook(db, webhook_id, true).await; - } - Ok(Err(e)) => { - tracing::warn!( - "tag webhook delivery failed webhook_id={} url={} error={}", - webhook_id, - url, - e - ); - let _ = touch_webhook(db, webhook_id, false).await; - } - Err(_) => { - tracing::warn!( - "tag webhook timed out webhook_id={} url={}", - webhook_id, - url - ); - let _ = touch_webhook(db, webhook_id, false).await; - } - } - } - } - } -} - -async fn touch_webhook( - db: &AppDatabase, - webhook_id: i64, - success: bool, -) -> Result<(), sea_orm::DbErr> { - use models::repos::repo_webhook::{Column as RwCol, Entity as RepoWebhookEntity}; - use models::{ColumnTrait, EntityTrait, QueryFilter}; - use sea_orm::{ExprTrait, sea_query::Expr}; - - let result: Result = if success { - RepoWebhookEntity::update_many() - .filter(RwCol::Id.eq(webhook_id)) - .col_expr( - RwCol::LastDeliveredAt, - Expr::value(Some(chrono::Utc::now())), - ) - .col_expr(RwCol::TouchCount, Expr::col(RwCol::TouchCount).add(1)) - .exec(db.writer()) - .await - } else { - RepoWebhookEntity::update_many() - .filter(RwCol::Id.eq(webhook_id)) - .col_expr(RwCol::TouchCount, Expr::col(RwCol::TouchCount).add(1)) - .exec(db.writer()) - .await - }; - - if let Err(e) = result { - tracing::warn!("failed to update webhook touch error={}", e); - return Err(e); - } - Ok(()) -} diff --git a/libs/git/http/auth.rs b/libs/git/http/auth.rs deleted file mode 100644 index 8d8258a..0000000 --- a/libs/git/http/auth.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::http::utils::extract_basic_credentials; -use crate::ssh::authz::SshAuthService; -use actix_web::{Error, HttpRequest}; -use argon2::Argon2; -use argon2::password_hash::{PasswordHash, PasswordVerifier}; -use db::database::AppDatabase; -use models::repos::repo; -use models::users::{user, user_token}; -use sea_orm::sqlx::types::chrono; -use sea_orm::*; - -pub async fn verify_access_token( - db: &AppDatabase, - username: &str, - access_key: &str, -) -> Result { - let user = user::Entity::find() - .filter(user::Column::Username.eq(username)) - .one(db.reader()) - .await - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid username or access key"))? - .ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid username or access key"))?; - - let tokens = user_token::Entity::find() - .filter(user_token::Column::User.eq(user.uid)) - .filter(user_token::Column::IsRevoked.eq(false)) - .all(db.reader()) - .await - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid username or access key"))? - .into_iter() - .filter(|token| { - token - .expires_at - .map(|expires_at| expires_at >= chrono::Utc::now()) - .unwrap_or(true) - }); - - for token in tokens { - let Ok(hash) = PasswordHash::new(&token.token_hash) else { - tracing::warn!(token_id = token.id, "invalid stored access key hash"); - continue; - }; - if Argon2::default() - .verify_password(access_key.as_bytes(), &hash) - .is_ok() - { - return Ok(user); - } - } - - Err(actix_web::error::ErrorUnauthorized( - "Invalid username or access key", - )) -} - -pub async fn authorize_repo_access( - req: &HttpRequest, - db: &AppDatabase, - repo: &repo::Model, - is_write: bool, -) -> Result<(), Error> { - if !is_write && !repo.is_private { - return Ok(()); - } - - let (username, access_key) = extract_basic_credentials(req)?; - let user = verify_access_token(db, &username, &access_key).await?; - let authz = SshAuthService::new(db.clone()); - - let can_access = authz.check_repo_permission(&user, repo, is_write).await; - if !can_access { - return Err(actix_web::error::ErrorForbidden( - "No permission for repository", - )); - } - - Ok(()) -} diff --git a/libs/git/http/handler.rs b/libs/git/http/handler.rs deleted file mode 100644 index 9b0aa7d..0000000 --- a/libs/git/http/handler.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::ssh::branch_protect::check_branch_protection; -use crate::ssh::ref_update::RefUpdate; -use actix_web::{Error, HttpResponse, web}; -use async_stream::stream; -use futures_util::Stream; -use futures_util::StreamExt; -use models::repos::{repo, repo_branch_protect}; -use sea_orm::*; -use std::path::PathBuf; -use std::pin::Pin; -use std::time::{Duration, Instant}; -use tokio::io::AsyncWriteExt; - -use db::database::AppDatabase; - -type ByteStream = Pin, std::io::Error>>>>; - -const PRE_PACK_LIMIT: usize = 1_048_576; -const GIT_OPERATION_TIMEOUT: Duration = Duration::from_secs(30); - -pub fn is_valid_oid(oid: &str) -> bool { - oid.len() == 40 && oid.chars().all(|c| c.is_ascii_hexdigit()) -} - -/// Validate a Git LFS OID (hex-encoded SHA-256 hash, 64 chars per LFS spec). -pub fn is_valid_lfs_oid(oid: &str) -> bool { - oid.len() == 64 && oid.chars().all(|c| c.is_ascii_hexdigit()) -} - -pub struct GitHttpHandler { - storage_path: PathBuf, - repo: repo::Model, - db: AppDatabase, -} - -impl GitHttpHandler { - pub fn new(storage_path: PathBuf, repo: repo::Model, db: AppDatabase) -> Self { - Self { - storage_path, - repo, - db, - } - } - - pub async fn upload_pack(&self, payload: web::Payload) -> Result { - self.handle_git_rpc("upload-pack", payload).await - } - - pub async fn receive_pack(&self, payload: web::Payload) -> Result { - self.handle_git_rpc("receive-pack", payload).await - } - - pub async fn info_refs(&self, service: &str) -> Result { - let git_cmd = match service { - "git-upload-pack" => "upload-pack", - "git-receive-pack" => "receive-pack", - _ => { - return Err(actix_web::error::ErrorBadRequest("Invalid service")); - } - }; - - let output = tokio::time::timeout(GIT_OPERATION_TIMEOUT, async { - tokio::process::Command::new("git") - .arg(git_cmd) - .arg("--stateless-rpc") - .arg("--advertise-refs") - .arg(&self.storage_path) - .output() - .await - }) - .await - .map_err(|_| actix_web::error::ErrorInternalServerError("Git info-refs timeout"))? - .map_err(|e| { - actix_web::error::ErrorInternalServerError(format!("Failed to execute git: {}", e)) - })?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(actix_web::error::ErrorInternalServerError(format!( - "Git command failed: {}", - stderr - ))); - } - - let mut response_body = Vec::new(); - let header = format!("# service={}\n", service); - write_pkt_line(&mut response_body, header.as_bytes()); - write_flush_pkt(&mut response_body); - response_body.extend_from_slice(&output.stdout); - - Ok(HttpResponse::Ok() - .content_type(format!("application/x-{}-advertisement", service)) - .insert_header(("Cache-Control", "no-cache")) - .body(response_body)) - } - - async fn handle_git_rpc( - &self, - service: &str, - mut payload: web::Payload, - ) -> Result { - let started = Instant::now(); - tracing::info!( - "git_rpc_started service={} repo={} repo_id={}", - service, - self.repo.repo_name, - self.repo.id.to_string() - ); - let mut child = tokio::process::Command::new("git") - .arg(service) - .arg("--stateless-rpc") - .arg(&self.storage_path) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .kill_on_drop(true) - .spawn() - .map_err(|e| { - actix_web::error::ErrorInternalServerError(format!("Failed to spawn git: {}", e)) - })?; - - let stream = stream! { - while let Some(chunk) = payload.next().await { - match chunk { - Ok(bytes) => { yield Ok(bytes.to_vec()); } - Err(e) => { yield Err(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())); } - } - } - }; - let mut stream: ByteStream = Box::pin(stream); - - if service == "receive-pack" { - let branch_protects = repo_branch_protect::Entity::find() - .filter(repo_branch_protect::Column::Repo.eq(self.repo.id)) - .all(self.db.reader()) - .await - .map_err(|e| actix_web::error::ErrorInternalServerError(e.to_string()))?; - - let mut pre_pack: Vec = Vec::with_capacity(65536); - - while let Some(chunk) = stream.next().await { - let bytes = match chunk { - Ok(b) => b, - Err(e) => return Err(Error::from(e)), - }; - - // Reject oversized pre-PACK data to prevent memory exhaustion - if pre_pack.len() + bytes.len() > PRE_PACK_LIMIT { - tracing::warn!( - "git_rpc_payload_too_large service={} repo={} repo_id={}", - service, - self.repo.repo_name, - self.repo.id.to_string() - ); - return Err(actix_web::error::ErrorPayloadTooLarge(format!( - "Ref negotiation exceeds {} byte limit", - PRE_PACK_LIMIT - ))); - } - - if let Some(pos) = bytes.windows(4).position(|w| w == b"0000") { - let end = pos + 4; - pre_pack.extend_from_slice(&bytes[..end]); - - let refs = RefUpdate::parse_ref_updates(&pre_pack) - .map_err(actix_web::error::ErrorBadRequest)?; - if let Some(msg) = refs - .iter() - .find_map(|r#ref| check_branch_protection(&branch_protects, r#ref)) - { - tracing::warn!( - "branch_protection_violation repo={} repo_id={} message={}", - self.repo.repo_name, - self.repo.id.to_string(), - msg - ); - return Err(actix_web::error::ErrorForbidden(msg)); - } - - let remaining: ByteStream = Box::pin(stream! { - yield Ok(pre_pack); - if end < bytes.len() { - yield Ok(bytes[end..].to_vec()); - } - while let Some(chunk) = stream.next().await { - yield chunk; - } - }); - stream = remaining; - break; - } else { - pre_pack.extend_from_slice(&bytes); - } - } - } - - if let Some(mut stdin) = child.stdin.take() { - let write_task = actix_web::rt::spawn(async move { - while let Some(chunk) = stream.next().await { - match chunk { - Ok(bytes) => { - if let Err(e) = stdin.write_all(&bytes).await { - return Err(e); - } - } - Err(e) => { - return Err(std::io::Error::new(std::io::ErrorKind::Other, e)); - } - } - } - drop(stdin); - Ok::<_, std::io::Error>(()) - }); - - let write_result = tokio::time::timeout(GIT_OPERATION_TIMEOUT, write_task) - .await - .map_err(|_| actix_web::error::ErrorInternalServerError("Git stdin write timeout"))? - .map_err(|e| { - actix_web::error::ErrorInternalServerError(format!("Write error: {}", e)) - })?; - - if let Err(e) = write_result { - return Err(actix_web::error::ErrorInternalServerError(format!( - "Failed to write to git: {}", - e - ))); - } - } - - let output = tokio::time::timeout(GIT_OPERATION_TIMEOUT, child.wait_with_output()) - .await - .map_err(|_| actix_web::error::ErrorInternalServerError("Git operation timeout"))? - .map_err(|e| { - actix_web::error::ErrorInternalServerError(format!("Git wait failed: {}", e)) - })?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let ms = started.elapsed().as_millis() as u64; - tracing::error!( - "git_rpc_failed service={} repo={} repo_id={} duration_ms={} stderr={}", - service, - self.repo.repo_name, - self.repo.id.to_string(), - ms, - stderr.to_string() - ); - return Err(actix_web::error::ErrorInternalServerError(format!( - "Git command failed: {}", - stderr - ))); - } - - let ms = started.elapsed().as_millis() as u64; - tracing::info!( - "git_rpc_completed service={} repo={} repo_id={} duration_ms={} bytes_out={}", - service, - self.repo.repo_name, - self.repo.id.to_string(), - ms, - output.stdout.len() - ); - - Ok(HttpResponse::Ok() - .content_type(format!("application/x-git-{}-result", service)) - .insert_header(("Cache-Control", "no-cache")) - .body(output.stdout)) - } -} - -fn write_pkt_line(buf: &mut Vec, data: &[u8]) { - let len = data.len() + 4; - buf.extend_from_slice(format!("{:04x}", len).as_bytes()); - buf.extend_from_slice(data); -} - -fn write_flush_pkt(buf: &mut Vec) { - buf.extend_from_slice(b"0000"); -} diff --git a/libs/git/http/lfs.rs b/libs/git/http/lfs.rs deleted file mode 100644 index 9e9748b..0000000 --- a/libs/git/http/lfs.rs +++ /dev/null @@ -1,629 +0,0 @@ -use crate::error::GitError; -use crate::http::handler::is_valid_lfs_oid; -use actix_web::{HttpResponse, web}; -use db::cache::AppCache; -use db::database::AppDatabase; -use models::repos::{repo, repo_lfs_lock, repo_lfs_object}; -use sea_orm::sqlx::types::chrono; -use sea_orm::*; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::PathBuf; - -const LFS_AUTH_TOKEN_EXPIRY: u64 = 3600; -/// Maximum LFS object size in bytes (50 GiB, matching GitHub/Gitea default). -const LFS_MAX_OBJECT_SIZE: i64 = 50 * 1024 * 1024 * 1024; - -#[derive(Deserialize, Serialize)] -pub struct BatchRequest { - pub operation: String, - pub objects: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub transfers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub r#ref: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub hash_algo: Option, -} - -#[derive(Deserialize, Serialize)] -pub struct LfsRef { - pub name: String, -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct LfsObjectReq { - pub oid: String, - pub size: i64, -} - -#[derive(Serialize)] -pub struct BatchResponse { - pub transfer: String, - pub objects: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub hash_algo: Option, -} - -#[derive(Serialize)] -pub struct LfsObjectResponse { - pub oid: String, - pub size: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub authenticated: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub actions: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -#[derive(Serialize)] -pub struct LfsAction { - pub href: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub header: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub expires_in: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub expires_at: Option, -} - -#[derive(Serialize)] -pub struct LfsError { - pub code: i32, - pub message: String, -} - -#[derive(Deserialize)] -pub struct CreateLockRequest { - pub oid: String, -} - -#[derive(Serialize)] -pub struct LockResponse { - pub path: String, - pub locked_by: uuid::Uuid, - pub locked_at: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub unlocked_at: Option, -} - -pub struct LfsHandler { - pub storage_path: PathBuf, - pub model: repo::Model, - pub db: AppDatabase, -} - -impl LfsHandler { - pub fn new(storage_path: PathBuf, model: repo::Model, db: AppDatabase) -> Self { - Self { - storage_path, - model, - db, - } - } - - fn get_lfs_storage_path(&self) -> PathBuf { - self.storage_path.join(".lfs") - } - - fn get_object_path(&self, oid: &str) -> PathBuf { - let prefix = &oid[..2]; - self.get_lfs_storage_path() - .join("objects") - .join(prefix) - .join(oid) - } - - pub async fn batch( - &self, - req: BatchRequest, - base_url: &str, - ) -> Result { - let operation = req.operation.as_str(); - - if operation != "upload" && operation != "download" { - return Err(GitError::InvalidOid(format!( - "Invalid operation: {}", - operation - ))); - } - - for obj in &req.objects { - if obj.size > LFS_MAX_OBJECT_SIZE { - return Err(GitError::InvalidOid(format!( - "Object size {} exceeds maximum allowed size {}", - obj.size, LFS_MAX_OBJECT_SIZE - ))); - } - } - - let oids: Vec<&str> = req.objects.iter().map(|o| o.oid.as_str()).collect(); - - // Single batch query for all OIDs - let existing: Vec = repo_lfs_object::Entity::find() - .filter(repo_lfs_object::Column::Oid.is_in(oids.clone())) - .filter(repo_lfs_object::Column::Repo.eq(self.model.id)) - .all(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))?; - - let existing_map: HashMap<&str, &repo_lfs_object::Model> = - existing.iter().map(|m| (m.oid.as_str(), m)).collect(); - - let mut response_objects = Vec::with_capacity(req.objects.len()); - - for obj in req.objects { - let existing = existing_map.get(obj.oid.as_str()); - - let mut actions = HashMap::new(); - - match operation { - "upload" => { - if existing.is_none() { - let upload_url = format!( - "{}/{}/{}.git/info/lfs/objects/{}", - base_url, self.model.project, self.model.repo_name, obj.oid - ); - - let token = uuid::Uuid::now_v7().to_string(); - let mut headers = HashMap::new(); - headers.insert("authorization".to_string(), format!("Bearer {}", token)); - - actions.insert( - "upload".to_string(), - LfsAction { - href: upload_url, - header: Some(headers), - expires_in: Some(LFS_AUTH_TOKEN_EXPIRY as i64), - expires_at: None, - }, - ); - } - } - "download" => match existing { - Some(_) => { - let download_url = format!( - "{}/{}/{}.git/info/lfs/objects/{}", - base_url, self.model.project, self.model.repo_name, obj.oid - ); - - let token = uuid::Uuid::now_v7().to_string(); - let mut headers = HashMap::new(); - headers.insert("authorization".to_string(), format!("Bearer {}", token)); - - actions.insert( - "download".to_string(), - LfsAction { - href: download_url, - header: Some(headers), - expires_in: Some(LFS_AUTH_TOKEN_EXPIRY as i64), - expires_at: None, - }, - ); - } - None => { - response_objects.push(LfsObjectResponse { - oid: obj.oid, - size: obj.size, - authenticated: None, - actions: None, - error: Some(LfsError { - code: 404, - message: "Object does not exist".to_string(), - }), - }); - continue; - } - }, - _ => {} - } - - response_objects.push(LfsObjectResponse { - oid: obj.oid, - size: obj.size, - authenticated: Some(true), - actions: if actions.is_empty() { - None - } else { - Some(actions) - }, - error: None, - }); - } - - Ok(BatchResponse { - transfer: "basic".to_string(), - objects: response_objects, - hash_algo: req.hash_algo, - }) - } - - /// Batch API with authentication: stores generated tokens in Redis for later validation. - pub async fn batch_with_auth( - &self, - req: BatchRequest, - base_url: &str, - user_uid: uuid::Uuid, - cache: &AppCache, - ) -> Result { - let operation = req.operation.as_str(); - - if operation != "upload" && operation != "download" { - return Err(GitError::InvalidOid(format!( - "Invalid operation: {}", - operation - ))); - } - - for obj in &req.objects { - if obj.size > LFS_MAX_OBJECT_SIZE { - return Err(GitError::InvalidOid(format!( - "Object size {} exceeds maximum allowed size {}", - obj.size, LFS_MAX_OBJECT_SIZE - ))); - } - } - - let oids: Vec<&str> = req.objects.iter().map(|o| o.oid.as_str()).collect(); - - let existing: Vec = repo_lfs_object::Entity::find() - .filter(repo_lfs_object::Column::Oid.is_in(oids.clone())) - .filter(repo_lfs_object::Column::Repo.eq(self.model.id)) - .all(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))?; - - let existing_map: HashMap<&str, &repo_lfs_object::Model> = - existing.iter().map(|m| (m.oid.as_str(), m)).collect(); - - let mut response_objects = Vec::with_capacity(req.objects.len()); - - for obj in req.objects { - let existing = existing_map.get(obj.oid.as_str()); - - let mut actions = HashMap::new(); - - match operation { - "upload" => { - if existing.is_none() { - let upload_url = format!( - "{}/{}/{}.git/info/lfs/objects/{}", - base_url, self.model.project, self.model.repo_name, obj.oid - ); - - let token = uuid::Uuid::now_v7().to_string(); - crate::http::lfs_routes::store_lfs_token( - cache, - &token, - self.model.id, - user_uid, - "upload", - ) - .await; - - let mut headers = HashMap::new(); - headers.insert("authorization".to_string(), format!("Bearer {}", token)); - - actions.insert( - "upload".to_string(), - LfsAction { - href: upload_url, - header: Some(headers), - expires_in: Some(LFS_AUTH_TOKEN_EXPIRY as i64), - expires_at: None, - }, - ); - } - } - "download" => match existing { - Some(_) => { - let download_url = format!( - "{}/{}/{}.git/info/lfs/objects/{}", - base_url, self.model.project, self.model.repo_name, obj.oid - ); - - let token = uuid::Uuid::now_v7().to_string(); - crate::http::lfs_routes::store_lfs_token( - cache, - &token, - self.model.id, - user_uid, - "download", - ) - .await; - - let mut headers = HashMap::new(); - headers.insert("authorization".to_string(), format!("Bearer {}", token)); - - actions.insert( - "download".to_string(), - LfsAction { - href: download_url, - header: Some(headers), - expires_in: Some(LFS_AUTH_TOKEN_EXPIRY as i64), - expires_at: None, - }, - ); - } - None => { - response_objects.push(LfsObjectResponse { - oid: obj.oid, - size: obj.size, - authenticated: None, - actions: None, - error: Some(LfsError { - code: 404, - message: "Object does not exist".to_string(), - }), - }); - continue; - } - }, - _ => {} - } - - response_objects.push(LfsObjectResponse { - oid: obj.oid, - size: obj.size, - authenticated: Some(true), - actions: if actions.is_empty() { - None - } else { - Some(actions) - }, - error: None, - }); - } - - Ok(BatchResponse { - transfer: "basic".to_string(), - objects: response_objects, - hash_algo: req.hash_algo, - }) - } - - pub async fn upload_object( - &self, - oid: &str, - payload: web::Payload, - ) -> Result { - if !is_valid_lfs_oid(oid) { - return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid))); - } - - let object_path = self.get_object_path(oid); - if let Some(parent) = object_path.parent() { - tokio::fs::create_dir_all(parent) - .await - .map_err(|e| GitError::Internal(format!("Failed to create directory: {}", e)))?; - } - - let temp_path = object_path.with_extension("tmp"); - let mut file = tokio::fs::File::create(&temp_path) - .await - .map_err(|e| GitError::Internal(format!("Failed to create temp file: {}", e)))?; - - use futures_util::stream::StreamExt; - use sha2::Digest; - use tokio::io::AsyncWriteExt; - - let mut payload = payload; - let mut size = 0i64; - let mut hasher = sha2::Sha256::new(); - - while let Some(chunk) = payload.next().await { - let chunk = chunk.map_err(|e| GitError::Internal(format!("Payload error: {}", e)))?; - size += chunk.len() as i64; - // Hard limit: abort if we exceed the max LFS object size. - // This prevents unbounded disk usage from a malicious or misbehaving client. - if size > LFS_MAX_OBJECT_SIZE { - let _ = tokio::fs::remove_file(&temp_path).await; - return Err(GitError::InvalidOid(format!( - "Object size exceeds maximum allowed size {}", - LFS_MAX_OBJECT_SIZE - ))); - } - hasher.update(&chunk); - if let Err(e) = file.write_all(&chunk).await { - let _ = tokio::fs::remove_file(&temp_path).await; - return Err(GitError::Internal(format!("Failed to write file: {}", e))); - } - } - - file.flush() - .await - .map_err(|e| GitError::Internal(format!("Failed to flush file: {}", e)))?; - drop(file); - - let hash_bytes = hasher.finalize(); - let calculated_oid = hex::encode(hash_bytes.as_slice()); - - if calculated_oid != oid { - let _ = tokio::fs::remove_file(&temp_path).await; - return Err(GitError::InvalidOid(format!( - "OID mismatch: expected {}, got {}", - oid, calculated_oid - ))); - } - - if let Err(e) = tokio::fs::rename(&temp_path, &object_path).await { - let _ = tokio::fs::remove_file(&temp_path).await; - return Err(GitError::Internal(format!("Failed to move file: {}", e))); - } - - let now = chrono::Utc::now(); - let new_object = repo_lfs_object::ActiveModel { - id: Set(0i64), - oid: Set(oid.to_string()), - repo: Set(self.model.id), - size: Set(size), - storage_path: Set(object_path.to_string_lossy().to_string()), - uploaded_by: Set(None), - uploaded_at: Set(now), - }; - - new_object - .insert(self.db.writer()) - .await - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(HttpResponse::Ok().finish()) - } - - pub async fn download_object(&self, oid: &str) -> Result { - if !is_valid_lfs_oid(oid) { - return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid))); - } - - let obj = repo_lfs_object::Entity::find() - .filter(repo_lfs_object::Column::Oid.eq(oid)) - .filter(repo_lfs_object::Column::Repo.eq(self.model.id)) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - .ok_or_else(|| GitError::NotFound("Object not found".to_string()))?; - - // Security: verify the stored path is within the expected LFS storage directory - let expected_base = self.get_lfs_storage_path(); - let obj_path = PathBuf::from(&obj.storage_path); - if !obj_path.starts_with(&expected_base) { - tracing::error!( - "LFS object path outside storage directory: {}", - obj.storage_path - ); - return Err(GitError::AuthFailed("Invalid object path".to_string())); - } - - let file = tokio::fs::File::open(&obj_path) - .await - .map_err(|e| GitError::Internal(format!("Failed to open file: {}", e)))?; - - use actix_web::body::BodyStream; - use futures_util::stream; - use tokio::io::AsyncReadExt; - - let chunk_size: usize = 65536; - - let stream = stream::unfold(file, move |mut file| async move { - let mut buffer = vec![0u8; chunk_size]; - match file.read(&mut buffer).await { - Ok(0) => None, - Ok(n) => { - buffer.truncate(n); - Some(( - Ok::<_, std::io::Error>(actix_web::web::Bytes::from(buffer)), - file, - )) - } - Err(e) => Some((Err(e), file)), - } - }); - - Ok(HttpResponse::Ok() - .content_type("application/octet-stream") - .insert_header(("Content-Length", obj.size.to_string())) - .body(BodyStream::new(stream))) - } - - pub async fn lock_object( - &self, - oid: &str, - user_uid: uuid::Uuid, - ) -> Result { - use sea_orm::ActiveModelTrait; - - if !is_valid_lfs_oid(oid) { - return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid))); - } - - let now = chrono::Utc::now(); - - let am = repo_lfs_lock::ActiveModel { - repo: Set(self.model.id), - path: Set(oid.to_string()), - lock_type: Set("upload".to_string()), - locked_by: Set(user_uid), - locked_at: Set(now), - unlocked_at: Set(None), - }; - - match am.insert(self.db.writer()).await { - Ok(model) => Ok(LockResponse { - path: model.path, - locked_by: model.locked_by, - locked_at: model.locked_at.to_rfc3339(), - unlocked_at: model.unlocked_at.map(|t| t.to_rfc3339()), - }), - Err(e) => { - let err_msg = format!("{}", e); - if err_msg.contains("duplicate key") || err_msg.contains("23505") { - return Err(GitError::Locked("Already locked".to_string())); - } - Err(GitError::Internal(format!("DB error: {}", e))) - } - } - } - - pub async fn unlock_object(&self, lock_id: &str, user_uid: uuid::Uuid) -> Result<(), GitError> { - let existing = repo_lfs_lock::Entity::find() - .filter(repo_lfs_lock::Column::Repo.eq(self.model.id)) - .filter(repo_lfs_lock::Column::Path.eq(lock_id.to_string())) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - .ok_or_else(|| GitError::NotFound("Lock not found".to_string()))?; - - if existing.locked_by != user_uid && existing.locked_by != self.model.created_by { - return Err(GitError::PermissionDenied( - "Not allowed to unlock".to_string(), - )); - } - - let now = chrono::Utc::now(); - let mut am: repo_lfs_lock::ActiveModel = existing.into(); - am.unlocked_at = Set(Some(now)); - let _: repo_lfs_lock::Model = am - .update(self.db.writer()) - .await - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(()) - } - - pub async fn list_locks(&self, maybe_oid: Option<&str>) -> Result, GitError> { - let mut q = - repo_lfs_lock::Entity::find().filter(repo_lfs_lock::Column::Repo.eq(self.model.id)); - if let Some(oid) = maybe_oid { - q = q.filter(repo_lfs_lock::Column::Path.eq(oid.to_string())); - } - let rows: Vec = q - .all(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(rows - .into_iter() - .map(|r| LockResponse { - path: r.path, - locked_by: r.locked_by, - locked_at: r.locked_at.to_rfc3339(), - unlocked_at: r.unlocked_at.map(|t| t.to_rfc3339()), - }) - .collect()) - } - - pub async fn get_lock(&self, path: &str) -> Result { - let r = repo_lfs_lock::Entity::find() - .filter(repo_lfs_lock::Column::Repo.eq(self.model.id)) - .filter(repo_lfs_lock::Column::Path.eq(path.to_string())) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - .ok_or_else(|| GitError::NotFound("Lock not found".to_string()))?; - Ok(LockResponse { - path: r.path, - locked_by: r.locked_by, - locked_at: r.locked_at.to_rfc3339(), - unlocked_at: r.unlocked_at.map(|t| t.to_rfc3339()), - }) - } -} diff --git a/libs/git/http/lfs_routes.rs b/libs/git/http/lfs_routes.rs deleted file mode 100644 index 8a9a17a..0000000 --- a/libs/git/http/lfs_routes.rs +++ /dev/null @@ -1,458 +0,0 @@ -use crate::error::GitError; -use crate::http::HttpAppState; -use crate::http::auth::authorize_repo_access; -use crate::http::auth::verify_access_token; -use crate::http::handler::is_valid_lfs_oid; -use crate::http::lfs::{BatchRequest, CreateLockRequest, LfsHandler}; -use crate::http::utils::{extract_basic_credentials, get_repo_model}; -use crate::ssh::authz::SshAuthService; -use crate::ssh::push_queue::{ - PushQueueEvent, PushQueueLease, PushQueueWaitError, wait_for_push_queue_slot, -}; -use actix_web::{Error, HttpRequest, HttpResponse, web}; -use argon2::Argon2; -use argon2::password_hash::{PasswordHash, PasswordVerifier}; -use models::repos::repo; -use models::users::{user, user_token}; -use sea_orm::prelude::*; -use std::path::PathBuf; - -fn base_url(req: &HttpRequest) -> String { - let conn_info = req.connection_info(); - format!("{}://{}", conn_info.scheme(), conn_info.host()) -} - -fn bearer_token(req: &HttpRequest) -> Result { - let auth_header = req - .headers() - .get("authorization") - .ok_or_else(|| actix_web::error::ErrorUnauthorized("Missing authorization header"))? - .to_str() - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid authorization header"))?; - - if let Some(token) = auth_header.strip_prefix("Bearer ") { - Ok(token.to_string()) - } else { - Err(actix_web::error::ErrorUnauthorized( - "Invalid authorization format", - )) - } -} - -/// Derive the acting user from the authenticated bearer token. -async fn user_uid(req: &HttpRequest, db: &db::database::AppDatabase) -> Result { - if let Ok((username, access_key)) = extract_basic_credentials(req) { - return verify_access_token(db, &username, &access_key) - .await - .map(|user| user.uid); - } - - let token = bearer_token(req)?; - find_user_by_bearer_token(&token, db).await -} - -/// Store LFS batch-generated token in Redis with TTL. -/// Key format: `lfs:token:{token}` → `{repo_id}:{user_uid}:{operation}` -pub async fn store_lfs_token( - cache: &db::cache::AppCache, - token: &str, - repo_id: uuid::Uuid, - user_uid: uuid::Uuid, - operation: &str, -) { - if let Ok(mut conn) = cache.conn().await { - use redis::AsyncCommands; - let value = format!("{}:{}:{}", repo_id, user_uid, operation); - let _: () = conn - .set_ex(format!("lfs:token:{}", token), value, 3600_u64) - .await - .map_err(|e| tracing::warn!(error = %e, "failed to store lfs token")) - .unwrap_or(()); - } -} - -/// Validate a bearer token for LFS upload/download. -/// Checks two sources: -/// 1. LFS batch-generated token (stored in Redis under `lfs:token:{token}`) -/// 2. Regular user access token (stored in user_token table) -/// -/// Returns (user_uid, repo_id, operation) for batch tokens, or -/// (user_uid, None, None) for regular access tokens. -async fn validate_lfs_token( - token: &str, - cache: &db::cache::AppCache, - db: &db::database::AppDatabase, - expected_repo_id: uuid::Uuid, - expected_operation: &str, -) -> Result { - // First: check if it's a LFS batch token in Redis - if let Ok(mut conn) = cache.conn().await { - use redis::AsyncCommands; - let stored: Option = conn - .get::(format!("lfs:token:{}", token)) - .await - .ok(); - if let Some(value) = stored { - let parts: Vec<&str> = value.split(':').collect(); - if parts.len() == 3 { - let repo_id = uuid::Uuid::parse_str(parts[0]) - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid batch token"))?; - let user_uid = uuid::Uuid::parse_str(parts[1]) - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid batch token"))?; - let operation = parts[2]; - - if repo_id != expected_repo_id { - return Err(actix_web::error::ErrorUnauthorized( - "Token not valid for this repo", - )); - } - if operation != expected_operation { - return Err(actix_web::error::ErrorUnauthorized( - "Token not valid for this operation", - )); - } - - // Consume the token (one-time use) - let _: Result<(), redis::RedisError> = - conn.del(format!("lfs:token:{}", token)).await; - - return Ok(user_uid); - } - } - } - - // Second: check if it's a regular user access token. - find_user_by_bearer_token(token, db).await -} - -async fn find_user_by_bearer_token( - token: &str, - db: &db::database::AppDatabase, -) -> Result { - let tokens = user_token::Entity::find() - .filter(user_token::Column::IsRevoked.eq(false)) - .all(db.reader()) - .await - .map_err(|_| actix_web::error::ErrorUnauthorized("Authentication failed"))?; - - for token_model in tokens { - if token_model - .expires_at - .map(|expires_at| expires_at < chrono::Utc::now()) - .unwrap_or(false) - { - continue; - } - - let Ok(hash) = PasswordHash::new(&token_model.token_hash) else { - tracing::warn!( - token_id = token_model.id, - "invalid stored bearer token hash" - ); - continue; - }; - if Argon2::default() - .verify_password(token.as_bytes(), &hash) - .is_ok() - { - return Ok(token_model.user); - } - } - - Err(actix_web::error::ErrorUnauthorized("Invalid token")) -} - -async fn authorize_user_repo_access( - db: &db::database::AppDatabase, - user_uid: uuid::Uuid, - repo: &repo::Model, - is_write: bool, -) -> Result<(), Error> { - let user = user::Entity::find() - .filter(user::Column::Uid.eq(user_uid)) - .one(db.reader()) - .await - .map_err(|_| actix_web::error::ErrorUnauthorized("Authentication failed"))? - .ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid token user"))?; - - let authz = SshAuthService::new(db.clone()); - if authz.check_repo_permission(&user, repo, is_write).await { - Ok(()) - } else { - Err(actix_web::error::ErrorForbidden( - "No permission for repository", - )) - } -} - -async fn acquire_lfs_write_queue( - state: &HttpAppState, - repo: &repo::Model, - operation: &'static str, -) -> Result { - match wait_for_push_queue_slot(state.sync.clone(), repo.id, |event, request_id| { - let request_id = request_id.to_string(); - match event { - PushQueueEvent::Waiting(position) => { - tracing::info!( - repo = %repo.repo_name, - repo_id = %repo.id, - request_id = %request_id, - operation = operation, - position = position.position, - total = position.total, - "lfs_write_queue_waiting" - ); - } - PushQueueEvent::Acquired => { - tracing::info!( - repo = %repo.repo_name, - repo_id = %repo.id, - request_id = %request_id, - operation = operation, - "lfs_write_queue_acquired" - ); - } - } - }) - .await - { - Ok(lease) => Ok(lease), - Err(PushQueueWaitError::Join(e)) => { - tracing::error!( - error = %e, - repo = %repo.repo_name, - repo_id = %repo.id, - operation = operation, - "lfs_write_queue_join_failed" - ); - Err(actix_web::error::ErrorServiceUnavailable( - "GitData: LFS write queue is temporarily unavailable. Please retry later.", - )) - } - Err(PushQueueWaitError::Lock(e)) => { - tracing::error!( - error = %e, - repo = %repo.repo_name, - repo_id = %repo.id, - operation = operation, - "lfs_write_queue_lock_failed" - ); - Err(actix_web::error::ErrorServiceUnavailable( - "GitData: LFS write queue lock failed. Please retry later.", - )) - } - Err(PushQueueWaitError::Timeout) => { - tracing::warn!( - repo = %repo.repo_name, - repo_id = %repo.id, - operation = operation, - "lfs_write_queue_timeout" - ); - Err(actix_web::error::ErrorServiceUnavailable( - "GitData: LFS write queue timed out. Please retry in a moment.", - )) - } - } -} - -pub async fn lfs_batch( - req: HttpRequest, - path: web::Path<(String, String)>, - body: web::Json, - state: web::Data, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let batch_req = body.into_inner(); - let is_write = batch_req.operation == "upload"; - - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - - // Auth check: private repos always require auth; upload always requires auth - if repo.is_private || is_write { - let uid = user_uid(&req, &state.db).await?; - authorize_repo_access(&req, &state.db, &repo, is_write).await?; - - let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); - let response = handler - .batch_with_auth(batch_req, &base_url(&req), uid, &state.cache) - .await - .map_err(|_| actix_web::error::ErrorInternalServerError("LFS batch failed"))?; - Ok(HttpResponse::Ok() - .content_type("application/vnd.git-lfs+json") - .json(response)) - } else { - // Public repo + download: allow anonymous - let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); - let response = handler - .batch(batch_req, &base_url(&req)) - .await - .map_err(|_| actix_web::error::ErrorInternalServerError("LFS batch failed"))?; - Ok(HttpResponse::Ok() - .content_type("application/vnd.git-lfs+json") - .json(response)) - } -} - -pub async fn lfs_upload( - req: HttpRequest, - path: web::Path<(String, String, String)>, - payload: web::Payload, - state: web::Data, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - - if !is_valid_lfs_oid(&oid) { - return Err(actix_web::error::ErrorBadRequest("Invalid OID format")); - } - - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - let token = bearer_token(&req)?; - - // Validate token (batch token or user access token) with write permission - let uid = validate_lfs_token(&token, &state.cache, &state.db, repo.id, "upload").await?; - authorize_user_repo_access(&state.db, uid, &repo, true).await?; - - let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); - let mut queue_lease = acquire_lfs_write_queue(&state, &handler.model, "upload").await?; - - let result = match handler.upload_object(&oid, payload).await { - Ok(response) => Ok(response), - Err(GitError::InvalidOid(_)) => Err(actix_web::error::ErrorBadRequest("Invalid OID")), - Err(GitError::AuthFailed(_)) => Err(actix_web::error::ErrorUnauthorized("Unauthorized")), - Err(_e) => Err(actix_web::error::ErrorInternalServerError("Upload failed")), - }; - queue_lease.release().await; - result -} - -pub async fn lfs_download( - req: HttpRequest, - path: web::Path<(String, String, String)>, - state: web::Data, -) -> Result { - let (namespace, repo_name, oid) = path.into_inner(); - - if !is_valid_lfs_oid(&oid) { - return Err(actix_web::error::ErrorBadRequest("Invalid OID format")); - } - - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - - // Auth check: private repos require auth; public repos allow anonymous - if repo.is_private { - let token = bearer_token(&req)?; - let uid = validate_lfs_token(&token, &state.cache, &state.db, repo.id, "download").await?; - authorize_user_repo_access(&state.db, uid, &repo, false).await?; - } - - let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); - - match handler.download_object(&oid).await { - Ok(response) => Ok(response), - Err(GitError::NotFound(_)) => Err(actix_web::error::ErrorNotFound("Object not found")), - Err(GitError::AuthFailed(_)) => Err(actix_web::error::ErrorUnauthorized("Unauthorized")), - Err(_e) => Err(actix_web::error::ErrorInternalServerError( - "Download failed", - )), - } -} - -pub async fn lfs_lock_create( - req: HttpRequest, - path: web::Path<(String, String)>, - body: web::Json, - state: web::Data, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - let uid = user_uid(&req, &state.db).await?; - authorize_repo_access(&req, &state.db, &repo, true).await?; - let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); - let mut queue_lease = acquire_lfs_write_queue(&state, &handler.model, "lock_create").await?; - - let result = match handler.lock_object(&body.oid, uid).await { - Ok(lock) => Ok(HttpResponse::Created().json(lock)), - Err(GitError::Locked(msg)) => Ok(HttpResponse::Conflict().body(msg)), - Err(_e) => Err(actix_web::error::ErrorInternalServerError("Lock failed")), - }; - queue_lease.release().await; - result -} - -pub async fn lfs_lock_list( - req: HttpRequest, - path: web::Path<(String, String)>, - query: web::Query>, - state: web::Data, -) -> Result { - let (namespace, repo_name) = path.into_inner(); - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - - // Auth check: private repos require auth for lock listing - if repo.is_private { - let uid = user_uid(&req, &state.db).await?; - authorize_user_repo_access(&state.db, uid, &repo, false).await?; - } - - let maybe_oid = query.get("oid").map(|s| s.as_str()); - let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); - - match handler.list_locks(maybe_oid).await { - Ok(list) => Ok(HttpResponse::Ok().json(list)), - Err(_e) => Err(actix_web::error::ErrorInternalServerError( - "Lock list failed", - )), - } -} - -pub async fn lfs_lock_get( - req: HttpRequest, - path: web::Path<(String, String, String)>, - state: web::Data, -) -> Result { - let (namespace, repo_name, lock_path) = path.into_inner(); - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - - // Auth check: private repos require auth for lock viewing - if repo.is_private { - let uid = user_uid(&req, &state.db).await?; - authorize_user_repo_access(&state.db, uid, &repo, false).await?; - } - - let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); - - match handler.get_lock(&lock_path).await { - Ok(lock) => Ok(HttpResponse::Ok().json(lock)), - Err(GitError::NotFound(_)) => Err(actix_web::error::ErrorNotFound("Lock not found")), - Err(_e) => Err(actix_web::error::ErrorInternalServerError( - "Lock get failed", - )), - } -} - -pub async fn lfs_lock_delete( - req: HttpRequest, - path: web::Path<(String, String, String)>, - state: web::Data, -) -> Result { - let (namespace, repo_name, lock_id) = path.into_inner(); - - let repo = get_repo_model(&namespace, &repo_name, &state.db).await?; - let uid = user_uid(&req, &state.db).await?; - authorize_repo_access(&req, &state.db, &repo, true).await?; - let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone()); - let mut queue_lease = acquire_lfs_write_queue(&state, &handler.model, "lock_delete").await?; - - let result = match handler.unlock_object(&lock_id, uid).await { - Ok(()) => Ok(HttpResponse::NoContent().finish()), - Err(GitError::PermissionDenied(_)) => Err(actix_web::error::ErrorForbidden("Not allowed")), - Err(GitError::NotFound(_)) => Err(actix_web::error::ErrorNotFound("Lock not found")), - Err(_e) => Err(actix_web::error::ErrorInternalServerError( - "Lock delete failed", - )), - }; - queue_lease.release().await; - result -} diff --git a/libs/git/http/mod.rs b/libs/git/http/mod.rs deleted file mode 100644 index b1db305..0000000 --- a/libs/git/http/mod.rs +++ /dev/null @@ -1,162 +0,0 @@ -use crate::hook::HookService; -use actix_web::{App, HttpResponse, HttpServer, web}; -use config::AppConfig; -use db::cache::AppCache; -use db::database::AppDatabase; -use sea_orm::ConnectionTrait; -use std::sync::Arc; - -pub mod auth; -pub mod handler; -pub mod lfs; -pub mod lfs_routes; -pub mod rate_limit; -pub mod routes; -pub mod utils; - -#[derive(Clone)] -pub struct HttpAppState { - pub db: AppDatabase, - pub cache: AppCache, - pub sync: crate::ssh::ReceiveSyncService, - pub rate_limiter: Arc, - pub config: AppConfig, -} - -async fn robots(state: web::Data) -> HttpResponse { - let sitemap_url = state - .config - .git_http_domain() - .map(|d| format!("{}/sitemap.xml", d.trim_end_matches('/'))) - .unwrap_or_default(); - - let body = if sitemap_url.is_empty() { - "User-agent: *\nDisallow: /\n".to_string() - } else { - format!("User-agent: *\nDisallow: /\n\nSitemap: {sitemap_url}\n") - }; - - HttpResponse::Ok() - .content_type("text/plain; charset=utf-8") - .body(body) -} - -async fn health(state: web::Data) -> HttpResponse { - let db_ok = state - .db - .query_one_raw(sea_orm::Statement::from_string( - sea_orm::DbBackend::Postgres, - "SELECT 1", - )) - .await - .is_ok(); - let cache_ok = state.cache.conn().await.is_ok(); - - if db_ok && cache_ok { - HttpResponse::Ok().json(serde_json::json!({ - "status": "ok", - "db": "ok", - "cache": "ok", - })) - } else { - HttpResponse::ServiceUnavailable().json(serde_json::json!({ - "status": "unhealthy", - "db": if db_ok { "ok" } else { "error" }, - "cache": if cache_ok { "ok" } else { "error" }, - })) - } -} - -pub fn git_http_cfg(cfg: &mut web::ServiceConfig) { - cfg.route("/robots.txt", web::get().to(robots)) - .route("/health", web::get().to(health)) - .route( - "/{namespace}/{repo_name}.git/info/refs", - web::get().to(routes::info_refs), - ) - .route( - "/{namespace}/{repo_name}.git/git-upload-pack", - web::post().to(routes::upload_pack), - ) - .route( - "/{namespace}/{repo_name}.git/git-receive-pack", - web::post().to(routes::receive_pack), - ) - .route( - "/{namespace}/{repo_name}.git/info/lfs/objects/batch", - web::post().to(lfs_routes::lfs_batch), - ) - .route( - "/{namespace}/{repo_name}.git/info/lfs/objects/{oid}", - web::put().to(lfs_routes::lfs_upload), - ) - .route( - "/{namespace}/{repo_name}.git/info/lfs/objects/{oid}", - web::get().to(lfs_routes::lfs_download), - ) - .route( - "/{namespace}/{repo_name}.git/info/lfs/locks", - web::post().to(lfs_routes::lfs_lock_create), - ) - .route( - "/{namespace}/{repo_name}.git/info/lfs/locks", - web::get().to(lfs_routes::lfs_lock_list), - ) - .route( - "/{namespace}/{repo_name}.git/info/lfs/locks/{id}", - web::get().to(lfs_routes::lfs_lock_get), - ) - .route( - "/{namespace}/{repo_name}.git/info/lfs/locks/{id}", - web::delete().to(lfs_routes::lfs_lock_delete), - ); -} - -pub async fn run_http(config: AppConfig) -> anyhow::Result<()> { - let (db, app_cache) = tokio::join!(AppDatabase::init(&config), AppCache::init(&config),); - let db = db?; - let app_cache = app_cache?; - - let redis_pool = app_cache.redis_pool().clone(); - let _hook = HookService::new( - db.clone(), - app_cache.clone(), - redis_pool.clone(), - config.clone(), - ); - - let sync = crate::ssh::ReceiveSyncService::new(redis_pool.clone()); - - let rate_limiter = Arc::new(rate_limit::RateLimiter::new( - rate_limit::RateLimitConfig::default(), - )); - let _cleanup = rate_limiter.clone().start_cleanup(); - - let state = HttpAppState { - db: db.clone(), - cache: app_cache.clone(), - sync, - rate_limiter, - config: config.clone(), - }; - - tracing::info!("Starting git HTTP server on 0.0.0.0:8021"); - - let server = HttpServer::new(move || { - App::new() - .app_data(web::Data::new(state.clone())) - .configure(git_http_cfg) - }) - .bind("0.0.0.0:8021")? - .run(); - - // Await the server. Actix-web handles Ctrl+C gracefully by default: - // workers finish in-flight requests then exit (graceful shutdown). - let result = server.await; - if let Err(e) = result { - tracing::error!("HTTP server error: {}", e); - } - - tracing::info!("Git HTTP server stopped"); - Ok(()) -} diff --git a/libs/git/http/rate_limit.rs b/libs/git/http/rate_limit.rs deleted file mode 100644 index fe9521f..0000000 --- a/libs/git/http/rate_limit.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! HTTP rate limiting for git operations. -//! -//! Uses a token-bucket approach with per-repo-write limits. -//! In K8s environments all traffic routes through the ingress so -//! per-IP limiting is meaningless — a fixed global key is used instead. -//! Cleanup runs every 5 minutes to prevent unbounded memory growth. - -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::RwLock; -use tokio::time::interval; - -#[derive(Debug, Clone)] -pub struct RateLimitConfig { - /// Requests allowed per window for read operations. - pub read_requests_per_window: u32, - /// Requests allowed per window for write operations. - pub write_requests_per_window: u32, - /// Window duration in seconds. - pub window_secs: u64, -} - -impl Default for RateLimitConfig { - fn default() -> Self { - Self { - read_requests_per_window: 120, - write_requests_per_window: 30, - window_secs: 60, - } - } -} - -#[derive(Debug)] -struct RateLimitBucket { - read_count: u32, - write_count: u32, - reset_time: Instant, -} - -#[derive(Clone, Copy)] -enum BucketOp { - Read, - Write, -} - -pub struct RateLimiter { - buckets: Arc>>, - config: RateLimitConfig, -} - -impl RateLimiter { - pub fn new(config: RateLimitConfig) -> Self { - Self { - buckets: Arc::new(RwLock::new(HashMap::new())), - config, - } - } - - pub async fn is_read_allowed(&self) -> bool { - self.is_allowed( - "global:read", - BucketOp::Read, - self.config.read_requests_per_window, - ) - .await - } - - pub async fn is_write_allowed(&self) -> bool { - self.is_allowed( - "global:write", - BucketOp::Write, - self.config.write_requests_per_window, - ) - .await - } - - pub async fn is_repo_write_allowed(&self, repo_path: &str) -> bool { - let key = format!("repo:write:{}", repo_path); - self.is_allowed(&key, BucketOp::Write, self.config.write_requests_per_window) - .await - } - - async fn is_allowed(&self, key: &str, op: BucketOp, limit: u32) -> bool { - let now = Instant::now(); - let mut buckets = self.buckets.write().await; - - let bucket = buckets - .entry(key.to_string()) - .or_insert_with(|| RateLimitBucket { - read_count: 0, - write_count: 0, - reset_time: now + Duration::from_secs(self.config.window_secs), - }); - - if now >= bucket.reset_time { - bucket.read_count = 0; - bucket.write_count = 0; - bucket.reset_time = now + Duration::from_secs(self.config.window_secs); - } - - let over_limit = match op { - BucketOp::Read => bucket.read_count >= limit, - BucketOp::Write => bucket.write_count >= limit, - }; - - if over_limit { - return false; - } - - match op { - BucketOp::Read => bucket.read_count += 1, - BucketOp::Write => bucket.write_count += 1, - } - true - } - - pub async fn retry_after(&self) -> u64 { - let key_read = "global:read".to_string(); - let now = Instant::now(); - let buckets = self.buckets.read().await; - - if let Some(bucket) = buckets.get(&key_read) { - if now < bucket.reset_time { - return bucket.reset_time.saturating_duration_since(now).as_secs() as u64; - } - } - 0 - } - - /// Start a background cleanup task that removes expired entries. - /// Should be spawned once at startup. - pub fn start_cleanup(self: Arc) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - let mut ticker = interval(Duration::from_secs(300)); // every 5 minutes - loop { - ticker.tick().await; - let now = Instant::now(); - let mut buckets = self.buckets.write().await; - buckets.retain(|_, bucket| now < bucket.reset_time); - } - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_rate_limit_allows_requests_up_to_limit() { - let limiter = Arc::new(RateLimiter::new(RateLimitConfig { - read_requests_per_window: 3, - write_requests_per_window: 1, - window_secs: 60, - })); - - for _ in 0..3 { - assert!(limiter.is_read_allowed().await); - } - assert!(!limiter.is_read_allowed().await); - } -} diff --git a/libs/git/http/routes.rs b/libs/git/http/routes.rs deleted file mode 100644 index 9f291cd..0000000 --- a/libs/git/http/routes.rs +++ /dev/null @@ -1,165 +0,0 @@ -use crate::http::HttpAppState; -use crate::http::auth::authorize_repo_access; -use crate::http::handler::GitHttpHandler; -use crate::http::utils::get_repo_model; -use crate::ssh::RepoReceiveSyncTask; -use crate::ssh::push_queue::{PushQueueEvent, PushQueueWaitError, wait_for_push_queue_slot}; -use actix_web::{Error, HttpRequest, HttpResponse, web}; -use std::path::PathBuf; -use std::time::Duration; -use tokio::time::timeout; - -pub async fn info_refs( - req: HttpRequest, - path: web::Path<(String, String)>, - state: web::Data, -) -> Result { - if !state.rate_limiter.is_read_allowed().await { - return Err(actix_web::error::ErrorTooManyRequests( - "Rate limit exceeded", - )); - } - - let service_param = req - .query_string() - .split('&') - .find(|s| s.starts_with("service=")) - .and_then(|s| s.strip_prefix("service=")) - .ok_or_else(|| actix_web::error::ErrorBadRequest("Missing service parameter"))?; - - if service_param != "git-upload-pack" && service_param != "git-receive-pack" { - return Err(actix_web::error::ErrorBadRequest("Invalid service")); - } - - let path_inner = path.into_inner(); - let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?; - let is_write = service_param == "git-receive-pack"; - authorize_repo_access(&req, &state.db, &model, is_write).await?; - - let storage_path = PathBuf::from(&model.storage_path); - let handler = GitHttpHandler::new(storage_path, model, state.db.clone()); - handler.info_refs(service_param).await -} - -pub async fn upload_pack( - req: HttpRequest, - path: web::Path<(String, String)>, - payload: web::Payload, - state: web::Data, -) -> Result { - if !state.rate_limiter.is_read_allowed().await { - return Err(actix_web::error::ErrorTooManyRequests( - "Rate limit exceeded", - )); - } - - let path_inner = path.into_inner(); - let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?; - authorize_repo_access(&req, &state.db, &model, false).await?; - - let storage_path = PathBuf::from(&model.storage_path); - let handler = GitHttpHandler::new(storage_path, model, state.db.clone()); - handler.upload_pack(payload).await -} - -pub async fn receive_pack( - req: HttpRequest, - path: web::Path<(String, String)>, - payload: web::Payload, - state: web::Data, -) -> Result { - if !state.rate_limiter.is_write_allowed().await { - return Err(actix_web::error::ErrorTooManyRequests( - "Rate limit exceeded", - )); - } - - let path_inner = path.into_inner(); - let model = get_repo_model(&path_inner.0, &path_inner.1, &state.db).await?; - authorize_repo_access(&req, &state.db, &model, true).await?; - - let mut push_queue_lease = - match wait_for_push_queue_slot(state.sync.clone(), model.id, |event, request_id| { - let request_id = request_id.to_string(); - let repo_name = model.repo_name.clone(); - let repo_id = model.id; - match event { - PushQueueEvent::Waiting(position) => { - tracing::info!( - repo = %repo_name, - repo_id = %repo_id, - request_id = %request_id, - position = position.position, - total = position.total, - "http_push_queue_waiting" - ); - } - PushQueueEvent::Acquired => { - tracing::info!( - repo = %repo_name, - repo_id = %repo_id, - request_id = %request_id, - "http_push_queue_acquired" - ); - } - } - }) - .await - { - Ok(lease) => lease, - Err(PushQueueWaitError::Join(e)) => { - tracing::error!( - error = %e, - repo = %model.repo_name, - repo_id = %model.id, - "http_push_queue_join_failed" - ); - return Err(actix_web::error::ErrorServiceUnavailable( - "GitData: push queue is temporarily unavailable. Please retry later.", - )); - } - Err(PushQueueWaitError::Lock(e)) => { - tracing::error!( - error = %e, - repo = %model.repo_name, - repo_id = %model.id, - "http_push_queue_lock_failed" - ); - return Err(actix_web::error::ErrorServiceUnavailable( - "GitData: push queue lock failed. Please retry later.", - )); - } - Err(PushQueueWaitError::Timeout) => { - tracing::info!( - repo = %model.repo_name, - repo_id = %model.id, - "http_push_queue_timeout" - ); - return Ok(HttpResponse::ServiceUnavailable() - .insert_header(("Retry-After", "5")) - .content_type("text/plain; charset=utf-8") - .body("GitData: push queue timed out. Please retry in a moment.\n")); - } - }; - - let storage_path = PathBuf::from(&model.storage_path); - let handler = GitHttpHandler::new(storage_path, model.clone(), state.db.clone()); - let result = handler.receive_pack(payload).await; - push_queue_lease.release().await; - - if result.is_ok() { - let _ = tokio::spawn({ - let sync = state.sync.clone(); - let repo_uid = model.id; - async move { - let _ = timeout( - Duration::from_secs(5), - sync.send(RepoReceiveSyncTask { repo_uid }), - ) - .await; - } - }); - } - - result -} diff --git a/libs/git/http/utils.rs b/libs/git/http/utils.rs deleted file mode 100644 index 5fb429b..0000000 --- a/libs/git/http/utils.rs +++ /dev/null @@ -1,82 +0,0 @@ -use actix_web::{Error, HttpRequest}; -use argon2::Argon2; -use argon2::password_hash::{PasswordHasher, SaltString}; -use base64::Engine; -use base64::engine::general_purpose::STANDARD; -use db::database::AppDatabase; -use models::projects::{project, project_history_name}; -use models::repos::repo; -use sea_orm::*; - -pub fn hash_access_key(access_key: &str) -> Result { - let salt = SaltString::generate(&mut rsa::rand_core::OsRng::default()); - Ok(Argon2::default() - .hash_password(access_key.as_bytes(), &salt)? - .to_string()) -} - -pub async fn get_repo_model( - namespace: &str, - repo_name: &str, - db: &AppDatabase, -) -> Result { - let project_id = if let Some(project_model) = project::Entity::find() - .filter(project::Column::Name.eq(namespace)) - .one(db.reader()) - .await - .map_err(|_| actix_web::error::ErrorInternalServerError("Database error"))? - { - project_model.id - } else if let Some(history) = project_history_name::Entity::find() - .filter(project_history_name::Column::HistoryName.eq(namespace)) - .one(db.reader()) - .await - .map_err(|_| actix_web::error::ErrorInternalServerError("Database error"))? - { - history.project_uid - } else { - return Err(actix_web::error::ErrorNotFound("Project not found").into()); - }; - - let repo = repo::Entity::find() - .filter(repo::Column::RepoName.eq(repo_name)) - .filter(repo::Column::Project.eq(project_id)) - .one(db.reader()) - .await - .map_err(|_| actix_web::error::ErrorInternalServerError("Database error"))? - .ok_or_else(|| actix_web::error::ErrorNotFound("Repository not found"))?; - - Ok(repo) -} - -pub fn extract_basic_credentials(req: &HttpRequest) -> Result<(String, String), Error> { - let auth_header = req - .headers() - .get("authorization") - .ok_or_else(|| actix_web::error::ErrorUnauthorized("Missing authorization header"))? - .to_str() - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid authorization header"))?; - - let encoded = auth_header - .strip_prefix("Basic ") - .ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid authorization scheme"))?; - - let decoded = STANDARD - .decode(encoded) - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid basic authorization encoding"))?; - - let decoded = String::from_utf8(decoded) - .map_err(|_| actix_web::error::ErrorUnauthorized("Invalid basic authorization payload"))?; - - let (username, access_key) = decoded - .split_once(':') - .ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid basic authorization format"))?; - - if username.is_empty() || access_key.is_empty() { - return Err(actix_web::error::ErrorUnauthorized( - "Username or access key is empty", - )); - } - - Ok((username.to_string(), access_key.to_string())) -} diff --git a/libs/git/lfs/mod.rs b/libs/git/lfs/mod.rs deleted file mode 100644 index 62e90e8..0000000 --- a/libs/git/lfs/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! LFS (Large File Storage) domain — pointer file parsing, attribute management, -//! and local object operations. -pub mod ops; -pub mod types; diff --git a/libs/git/lfs/ops.rs b/libs/git/lfs/ops.rs deleted file mode 100644 index b7e56b9..0000000 --- a/libs/git/lfs/ops.rs +++ /dev/null @@ -1,320 +0,0 @@ -//! LFS operations on a Git repository. - -use std::fs; -use std::path::{Path, PathBuf}; - -use globset::Glob; - -use crate::commit::types::CommitOid; -use crate::lfs::types::{LfsConfig, LfsEntry, LfsOid, LfsPointer}; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn lfs_pointer_from_blob(&self, oid: &CommitOid) -> GitResult> { - let content = self.blob_content(oid)?; - Ok(LfsPointer::from_bytes(&content.content)) - } - - pub fn lfs_is_pointer(&self, oid: &CommitOid) -> GitResult { - let content = self.blob_content(oid)?; - Ok(LfsPointer::from_bytes(&content.content).is_some()) - } - - pub fn lfs_resolve_oid(&self, oid: &CommitOid) -> GitResult> { - let pointer = self.lfs_pointer_from_blob(oid)?; - Ok(pointer.map(|p| p.oid)) - } - - pub fn lfs_create_pointer( - &self, - oid: &LfsOid, - size: u64, - extra: &[(String, String)], - ) -> GitResult { - let pointer = LfsPointer { - version: "https://git-lfs.github.com/spec/v1".to_string(), - oid: oid.clone(), - size, - extra: extra.to_vec(), - }; - self.blob_create_from_string(&pointer.to_string()) - } - - pub fn lfs_scan_tree(&self, tree_oid: &CommitOid, recursive: bool) -> GitResult> { - let tree_oid = tree_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(tree_oid.to_string()))?; - let tree = self - .repo() - .find_tree(tree_oid) - .map_err(|_| GitError::ObjectNotFound(tree_oid.to_string()))?; - let mut entries = Vec::new(); - self.lfs_scan_tree_impl(&mut entries, &tree, "", recursive)?; - Ok(entries) - } - - fn lfs_scan_tree_impl( - &self, - out: &mut Vec, - tree: &git2::Tree<'_>, - prefix: &str, - recursive: bool, - ) -> GitResult<()> { - for entry in tree.iter() { - let name = entry.name().unwrap_or(""); - let full_path = if prefix.is_empty() { - name.to_string() - } else { - format!("{}/{}", prefix, name) - }; - - let blob_oid = entry.id(); - let obj = match self.repo().find_object(blob_oid, None) { - Ok(o) => o, - Err(_) => continue, - }; - - if obj.kind() == Some(git2::ObjectType::Tree) { - if recursive { - let sub_tree = self - .repo() - .find_tree(blob_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - self.lfs_scan_tree_impl(out, &sub_tree, &full_path, recursive)?; - } - } else if let Some(blob) = obj.as_blob() { - if let Some(pointer) = LfsPointer::from_bytes(blob.content()) { - out.push(LfsEntry { - path: full_path, - pointer, - size: 0, - }); - } - } - } - Ok(()) - } - - fn gitattributes_path(&self) -> PathBuf { - self.repo() - .workdir() - .unwrap_or_else(|| self.repo().path()) - .join(".gitattributes") - } - - pub fn lfs_gitattributes_list(&self) -> GitResult> { - let path = self.gitattributes_path(); - if !path.exists() { - return Ok(Vec::new()); - } - let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?; - Ok(content - .lines() - .filter(|l| l.contains("filter=lfs")) - .map(|l| l.trim().to_string()) - .collect()) - } - - pub fn lfs_gitattributes_add(&self, pattern: &str) -> GitResult<()> { - let line = format!("{} filter=lfs diff=lfs merge=lfs -text", pattern); - let path = self.gitattributes_path(); - - let content = if path.exists() { - fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))? - } else { - String::new() - }; - - if content.lines().any(|l| l.trim() == line) { - return Ok(()); - } - - let new_content = if content.ends_with('\n') || content.is_empty() { - format!("{}{}\n", content, line) - } else { - format!("{}\n{}\n", content, line) - }; - - fs::write(&path, new_content).map_err(|e| GitError::IoError(e.to_string()))?; - Ok(()) - } - - pub fn lfs_gitattributes_remove(&self, pattern: &str) -> GitResult { - let path = self.gitattributes_path(); - if !path.exists() { - return Ok(false); - } - let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?; - let target = format!("{} filter=lfs diff=lfs merge=lfs -text", pattern); - let new_lines: Vec<&str> = content.lines().filter(|l| l.trim() != target).collect(); - - if new_lines.len() == content.lines().count() { - return Ok(false); - } - - let new_content = new_lines.join("\n"); - fs::write(&path, new_content).map_err(|e| GitError::IoError(e.to_string()))?; - Ok(true) - } - - pub fn lfs_gitattributes_match(&self, path_str: &str) -> GitResult { - let patterns = self.lfs_gitattributes_list()?; - for pattern in patterns { - let glob_str = pattern.split_whitespace().next().unwrap_or(&pattern); - if let Ok(glob) = Glob::new(glob_str) { - let matcher = glob.compile_matcher(); - if matcher.is_match(path_str) { - return Ok(true); - } - } - } - Ok(false) - } - - fn lfs_objects_dir(&self) -> PathBuf { - self.repo().path().join("lfs").join("objects") - } - - /// Validates that the OID is at least 4 characters for path splitting. - fn lfs_validate_oid(&self, oid: &LfsOid) -> GitResult<()> { - if oid.as_str().len() < 4 { - return Err(GitError::Internal(format!( - "LFS OID too short for path splitting: {}", - oid - ))); - } - Ok(()) - } - - pub fn lfs_object_cached(&self, oid: &LfsOid) -> bool { - if oid.as_str().len() < 4 { - return false; - } - let (p1, rest) = oid.as_str().split_at(2); - let (p2, _) = rest.split_at(2); - self.lfs_objects_dir() - .join(p1) - .join(p2) - .join(oid.as_str()) - .exists() - } - - pub fn lfs_object_path(&self, oid: &LfsOid) -> GitResult { - self.lfs_validate_oid(oid)?; - let (p1, rest) = oid.as_str().split_at(2); - let (p2, _) = rest.split_at(2); - Ok(self.lfs_objects_dir().join(p1).join(p2).join(oid.as_str())) - } - - pub fn lfs_object_put(&self, oid: &LfsOid, content: &[u8]) -> GitResult { - use std::io::Write; - - let path = self.lfs_object_path(oid)?; - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|e| GitError::IoError(e.to_string()))?; - } - let mut file = fs::File::create(&path).map_err(|e| GitError::IoError(e.to_string()))?; - file.write_all(content) - .map_err(|e| GitError::IoError(e.to_string()))?; - Ok(path) - } - - pub fn lfs_object_get(&self, oid: &LfsOid) -> GitResult> { - let path = self.lfs_object_path(oid)?; - if !path.exists() { - return Err(GitError::LfsError(format!( - "object {} not found in local cache", - oid - ))); - } - fs::read(&path).map_err(|e| GitError::IoError(e.to_string())) - } - - pub fn lfs_object_list(&self) -> GitResult> { - let base = self.lfs_objects_dir(); - if !base.exists() { - return Ok(Vec::new()); - } - let mut oids = Vec::new(); - self.lfs_object_list_impl(&base, &mut oids)?; - Ok(oids) - } - - fn lfs_object_list_impl(&self, dir: &Path, oids: &mut Vec) -> GitResult<()> { - for entry in fs::read_dir(dir).map_err(|e| GitError::IoError(e.to_string()))? { - let entry = entry.map_err(|e| GitError::IoError(e.to_string()))?; - let path = entry.path(); - if path.is_dir() { - self.lfs_object_list_impl(&path, oids)?; - } else if path.is_file() { - if let Some(name) = path.file_name().and_then(|n| n.to_str()) { - oids.push(LfsOid::new(name)); - } - } - } - Ok(()) - } - - pub fn lfs_object_delete(&self, oid: &LfsOid) -> GitResult { - let path = self.lfs_object_path(oid)?; - if path.exists() { - fs::remove_file(&path).map_err(|e| GitError::IoError(e.to_string()))?; - Ok(true) - } else { - Ok(false) - } - } - - pub fn lfs_cache_size(&self) -> GitResult { - let oids = self.lfs_object_list()?; - let mut total = 0u64; - for oid in oids { - if let Ok(path) = self.lfs_object_path(&oid) { - if let Ok(meta) = fs::metadata(&path) { - total += meta.len(); - } - } - } - Ok(total) - } - - pub fn lfs_config(&self) -> GitResult { - let root = self.repo().workdir().unwrap_or_else(|| self.repo().path()); - let path = root.join(".lfsconfig"); - if !path.exists() { - return Ok(LfsConfig::new()); - } - let content = fs::read_to_string(&path).map_err(|e| GitError::IoError(e.to_string()))?; - let mut config = LfsConfig::new(); - for line in content.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - if let Some((k, v)) = line.split_once('=') { - let k = k.trim(); - let v = v.trim(); - if k == "lfs.url" || k == "lfs.endpoint" { - config.endpoint = Some(v.to_string()); - } else if k == "lfs.accesskey" || k == "lfs.access_token" { - config.access_token = Some(v.to_string()); - } - } - } - Ok(config) - } - - pub fn lfs_config_set(&self, config: &LfsConfig) -> GitResult<()> { - let root = self.repo().workdir().unwrap_or_else(|| self.repo().path()); - let path = root.join(".lfsconfig"); - let mut lines = Vec::new(); - if let Some(ref ep) = config.endpoint { - lines.push(format!("lfs.url = {}", ep)); - } - if let Some(ref tok) = config.access_token { - lines.push(format!("lfs.access_token = {}", tok)); - } - fs::write(&path, lines.join("\n") + "\n").map_err(|e| GitError::IoError(e.to_string()))?; - Ok(()) - } -} diff --git a/libs/git/lfs/types.rs b/libs/git/lfs/types.rs deleted file mode 100644 index a274766..0000000 --- a/libs/git/lfs/types.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! Serializable types for the LFS domain. - -use serde::{Deserialize, Serialize}; - -/// The SHA256 OID of an LFS object. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct LfsOid(pub String); - -impl LfsOid { - pub fn new(hex: &str) -> Self { - Self(hex.to_lowercase()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn is_valid(&self) -> bool { - self.0.len() == 64 && self.0.chars().all(|c| c.is_ascii_hexdigit()) - } -} - -impl std::fmt::Display for LfsOid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl AsRef for LfsOid { - fn as_ref(&self) -> &str { - &self.0 - } -} - -/// An LFS pointer file parsed from a blob. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct LfsPointer { - pub version: String, - pub oid: LfsOid, - pub size: u64, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub extra: Vec<(String, String)>, -} - -impl LfsPointer { - pub fn from_bytes(data: &[u8]) -> Option { - Self::from_str(std::str::from_utf8(data).ok()?) - } - - pub fn from_str(s: &str) -> Option { - let mut version = None; - let mut oid = None; - let mut size = None; - let mut extra = Vec::new(); - - for line in s.lines() { - let line = line.trim(); - if line.is_empty() { - continue; - } - if let Some(val) = line.strip_prefix("version ") { - version = Some(val.trim().to_string()); - } else if let Some(val) = line.strip_prefix("oid sha256:") { - oid = Some(LfsOid::new(val.trim())); - } else if let Some(val) = line.strip_prefix("size ") { - size = val.trim().parse::().ok(); - } else if let Some((k, v)) = line.split_once(' ') { - extra.push((k.to_string(), v.to_string())); - } - } - - let version = version?; - if version != "https://git-lfs.github.com/spec/v1" { - return None; - } - let oid = oid?; - if !oid.is_valid() { - return None; - } - let size = size?; - - Some(Self { - version, - oid, - size, - extra, - }) - } - - pub fn to_string(&self) -> String { - let mut lines = Vec::with_capacity(3 + self.extra.len()); - lines.push(format!("version {}", self.version)); - lines.push(format!("oid sha256:{}", self.oid)); - lines.push(format!("size {}", self.size)); - for (k, v) in &self.extra { - lines.push(format!("{} {}", k, v)); - } - lines.join("\n") + "\n" - } - - /// Path inside `.git/lfs/objects/` where this object would be stored locally. - pub fn local_object_path(&self, git_dir: &std::path::Path) -> std::path::PathBuf { - let oid = self.oid.as_str(); - // LFS OIDs must be at least 4 chars (2+2 split) for the path layout. - // Valid LFS OIDs are 64-char SHA256 hex, so this is always safe for valid pointers. - // Defensive check: if OID is somehow invalid, use the full OID as filename. - if oid.len() < 4 { - return git_dir.join("lfs").join("objects").join(oid); - } - let (p1, rest) = oid.split_at(2); - let (p2, _) = rest.split_at(2); - git_dir - .join("lfs") - .join("objects") - .join(p1) - .join(p2) - .join(oid) - } -} - -/// Metadata for an LFS-managed file in a tree entry. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LfsEntry { - pub path: String, - pub pointer: LfsPointer, - pub size: u64, -} - -/// LFS configuration for a repository. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct LfsConfig { - pub endpoint: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub access_token: Option, -} - -impl LfsConfig { - pub fn new() -> Self { - Self::default() - } - - pub fn endpoint(mut self, url: &str) -> Self { - self.endpoint = Some(url.to_string()); - self - } - - pub fn access_token(mut self, token: &str) -> Self { - self.access_token = Some(token.to_string()); - self - } -} diff --git a/libs/git/lib.rs b/libs/git/lib.rs deleted file mode 100644 index e85cc1b..0000000 --- a/libs/git/lib.rs +++ /dev/null @@ -1,47 +0,0 @@ -pub mod archive; -pub mod blame; -pub mod blob; -pub mod branch; -pub mod commit; -pub mod config; -pub mod description; -pub mod diff; -pub mod domain; -pub mod error; -pub mod hook; -pub mod http; -pub mod lfs; -pub mod merge; -pub(crate) mod ref_utils; -pub mod reference; -pub mod ssh; -pub mod tags; -pub mod tree; - -pub use archive::types::{ArchiveEntry, ArchiveFormat, ArchiveOptions, ArchiveSummary}; -pub use blame::ops::BlameOptions; -pub use blob::types::{BlobContent, BlobInfo}; -pub use branch::types::{BranchDiff, BranchInfo, BranchSummary}; -pub use commit::graph::{CommitGraph, CommitGraphLine, CommitGraphOptions}; -pub use commit::traverse::CommitWalkOptions; -pub use commit::types::{ - CommitBlameHunk, CommitBlameLine, CommitDiffFile, CommitDiffHunk, CommitDiffStats, CommitMeta, - CommitOid, CommitRefInfo, CommitReflogEntry, CommitSignature, CommitSort, -}; -pub use config::types::{ConfigEntry, ConfigSnapshot}; -pub use diff::ops::diff_to_side_by_side; -pub use diff::types::{ - DiffDelta, DiffDeltaStatus, DiffFile, DiffHunk, DiffLine, DiffOptions, DiffResult, DiffStats, - SideBySideChangeType, SideBySideDiffResult, SideBySideFile, SideBySideLine, -}; -pub use domain::GitDomain; -pub use error::{GitError, GitResult}; -pub use hook::pool::HookWorker; -pub use hook::pool::PoolConfig; -pub use hook::pool::types::{HookTask, TaskType}; -pub use hook::sync::HookMetaDataSync; -pub use lfs::types::{LfsConfig, LfsEntry, LfsOid, LfsPointer}; -pub use merge::types::{MergeAnalysisResult, MergeOptions, MergePreferenceResult, MergeheadInfo}; -pub use reference::types::RefInfo; -pub use tags::types::{TagInfo, TagSummary}; -pub use tree::types::{TreeEntry, TreeInfo}; diff --git a/libs/git/merge/mod.rs b/libs/git/merge/mod.rs deleted file mode 100644 index d912fd0..0000000 --- a/libs/git/merge/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Merge domain — all merge-related operations on a GitDomain. -pub mod ops; -pub mod types; diff --git a/libs/git/merge/ops.rs b/libs/git/merge/ops.rs deleted file mode 100644 index 7a85ce2..0000000 --- a/libs/git/merge/ops.rs +++ /dev/null @@ -1,346 +0,0 @@ -//! Merge operations. - -use crate::commit::types::CommitOid; -use crate::merge::types::{ - MergeAnalysisResult, MergeOptions, MergePreferenceResult, MergeheadInfo, -}; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn merge_analysis( - &self, - their_oid: &CommitOid, - ) -> GitResult<(MergeAnalysisResult, MergePreferenceResult)> { - let oid = their_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(their_oid.to_string()))?; - - let head_ref = self - .repo() - .find_reference("HEAD") - .map_err(|e| GitError::Internal(e.to_string()))?; - let annotated = self - .repo() - .reference_to_annotated_commit(&head_ref) - .map_err(|e| GitError::Internal(e.to_string()))?; - let their_annotated = self - .repo() - .find_annotated_commit(oid) - .map_err(|e: git2::Error| GitError::Internal(e.to_string()))?; - - let (analysis, pref) = self - .repo() - .merge_analysis(&[&annotated, &their_annotated]) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(( - MergeAnalysisResult::from_git2(analysis), - MergePreferenceResult::from_git2(pref), - )) - } - - pub fn merge_analysis_for_ref( - &self, - ref_name: &str, - their_oid: &CommitOid, - ) -> GitResult<(MergeAnalysisResult, MergePreferenceResult)> { - let oid = their_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(their_oid.to_string()))?; - - let reference = self - .repo() - .find_reference(ref_name) - .map_err(|e| GitError::Internal(e.to_string()))?; - let their_annotated = self - .repo() - .find_annotated_commit(oid) - .map_err(|e: git2::Error| GitError::Internal(e.to_string()))?; - - let (analysis, pref) = self - .repo() - .merge_analysis_for_ref(&reference, &[&their_annotated]) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(( - MergeAnalysisResult::from_git2(analysis), - MergePreferenceResult::from_git2(pref), - )) - } - - pub fn merge_base(&self, oid1: &CommitOid, oid2: &CommitOid) -> GitResult { - let o1 = oid1 - .to_oid() - .map_err(|_| GitError::InvalidOid(oid1.to_string()))?; - let o2 = oid2 - .to_oid() - .map_err(|_| GitError::InvalidOid(oid2.to_string()))?; - - let base = self - .repo() - .merge_base(o1, o2) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(CommitOid::from_git2(base)) - } - - pub fn merge_base_many(&self, oids: &[CommitOid]) -> GitResult { - let oids: Vec<_> = oids - .iter() - .map(|o| o.to_oid().map_err(|_| GitError::InvalidOid(o.to_string()))) - .collect::, _>>()?; - - let base = self - .repo() - .merge_base_many(&oids) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(CommitOid::from_git2(base)) - } - - pub fn merge_base_octopus(&self, oids: &[CommitOid]) -> GitResult { - let oids: Vec<_> = oids - .iter() - .map(|o| o.to_oid().map_err(|_| GitError::InvalidOid(o.to_string()))) - .collect::, _>>()?; - - let base = self - .repo() - .merge_base_octopus(&oids) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(CommitOid::from_git2(base)) - } - - pub fn merge_commits( - &self, - local_commit: &CommitOid, - remote_commit: &CommitOid, - opts: Option, - ) -> GitResult<()> { - let local_oid = local_commit - .to_oid() - .map_err(|_| GitError::InvalidOid(local_commit.to_string()))?; - let remote_oid = remote_commit - .to_oid() - .map_err(|_| GitError::InvalidOid(remote_commit.to_string()))?; - - let local = self - .repo() - .find_commit(local_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let remote = self - .repo() - .find_commit(remote_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut merge_opts = opts - .map(|o| o.to_git2()) - .unwrap_or_else(git2::MergeOptions::new); - - self.repo() - .merge_commits(&local, &remote, Some(&mut merge_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(()) - } - - pub fn merge_trees( - &self, - ancestor_tree: &CommitOid, - our_tree: &CommitOid, - their_tree: &CommitOid, - opts: Option, - ) -> GitResult<()> { - let ancestor_oid = ancestor_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(ancestor_tree.to_string()))?; - let our_oid = our_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(our_tree.to_string()))?; - let their_oid = their_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(their_tree.to_string()))?; - - let ancestor = self - .repo() - .find_tree(ancestor_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let ours = self - .repo() - .find_tree(our_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let theirs = self - .repo() - .find_tree(their_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut merge_opts = opts - .map(|o| o.to_git2()) - .unwrap_or_else(git2::MergeOptions::new); - - self.repo() - .merge_trees(&ancestor, &ours, &theirs, Some(&mut merge_opts)) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(()) - } - - pub fn merge_abort(&self) -> GitResult<()> { - self.repo() - .cleanup_state() - .map_err(|e| GitError::Internal(e.to_string())) - } - - pub fn merge_is_in_progress(&self) -> bool { - matches!( - self.repo().state(), - git2::RepositoryState::Merge - | git2::RepositoryState::Revert - | git2::RepositoryState::RevertSequence - | git2::RepositoryState::CherryPick - | git2::RepositoryState::CherryPickSequence - ) - } - - pub fn mergehead_list(&mut self) -> GitResult> { - let mut heads = Vec::new(); - self.repo_mut()? - .mergehead_foreach(|oid| { - heads.push(MergeheadInfo { - oid: CommitOid::from_git2(*oid), - }); - true - }) - .map_err(|e: git2::Error| GitError::Internal(e.to_string()))?; - Ok(heads) - } - - pub fn merge_is_conflicted(&self) -> bool { - self.repo() - .index() - .map(|idx| idx.has_conflicts()) - .unwrap_or(false) - } - - /// Squash all commits from `source_branch` into a single commit on top of `base`. - pub fn squash_commits(&self, base: &CommitOid, source_branch: &str) -> GitResult { - let base_oid = base - .to_oid() - .map_err(|_| GitError::InvalidOid(base.to_string()))?; - - let source_ref = self - .repo() - .find_reference(source_branch) - .map_err(|e| GitError::Internal(e.to_string()))?; - let head_oid = source_ref - .target() - .ok_or_else(|| GitError::Internal("Branch has no target OID".to_string()))?; - - // Get the merge base (the common ancestor) - let merge_base = self - .repo() - .merge_base(base_oid, head_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Collect all commits from merge_base (exclusive) to head (inclusive) - let mut revwalk = self - .repo() - .revwalk() - .map_err(|e| GitError::Internal(e.to_string()))?; - revwalk - .push(head_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - revwalk - .hide(merge_base) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut commits: Vec = Vec::new(); - for oid_result in revwalk { - let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; - let commit = self - .repo() - .find_commit(oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - commits.push(commit); - } - - if commits.is_empty() { - // Nothing to squash — return base as-is - return Ok(CommitOid::from_git2(base_oid)); - } - - // Sort commits oldest-first (topological order) - commits.sort_by_key(|c| c.time().seconds()); - - // Apply all patches onto a temporary tree. - // Strategy: apply patches sequentially using `git2::apply` on the index. - let base_tree = self - .repo() - .find_commit(base_oid) - .map_err(|e| GitError::Internal(e.to_string()))? - .tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Build a diff from the accumulated patches - let sig = self - .repo() - .signature() - .map_err(|e| GitError::Internal(e.to_string()))?; - - // Apply each commit's diff sequentially to build the squash tree. - let mut current_tree = base_tree; - for commit in &commits { - let commit_tree = commit - .tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - let diff = self - .repo() - .diff_tree_to_tree(Some(¤t_tree), Some(&commit_tree), None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - // apply_to_tree applies the diff to current_tree, returning a new Index. - let mut new_index = self - .repo() - .apply_to_tree(¤t_tree, &diff, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let new_tree_oid = new_index - .write_tree() - .map_err(|e| GitError::Internal(e.to_string()))?; - - current_tree = self - .repo() - .find_tree(new_tree_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - } - - let squash_tree = current_tree; - - // Build the squash commit message: list all squashed commits - let mut msg = String::new(); - for commit in &commits { - if !msg.is_empty() { - msg.push_str("\n"); - } - msg.push_str(&format!("- {}", commit.summary().unwrap_or("(no message)"))); - } - - // Create the squash commit on top of base. - // Do NOT update any ref — the caller decides how to use the returned OID. - let squash_oid = self - .repo() - .commit( - None, - &sig, - &sig, - &msg, - &squash_tree, - &[&self - .repo() - .find_commit(base_oid) - .map_err(|e| GitError::Internal(e.to_string()))?], - ) - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(CommitOid::from_git2(squash_oid)) - } -} diff --git a/libs/git/merge/types.rs b/libs/git/merge/types.rs deleted file mode 100644 index a7cf7a5..0000000 --- a/libs/git/merge/types.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Serializable types for the merge domain. - -use serde::{Deserialize, Serialize}; - -use crate::commit::types::CommitOid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MergeAnalysisResult { - pub is_none: bool, - pub is_normal: bool, - pub is_up_to_date: bool, - pub is_fast_forward: bool, - pub is_unborn: bool, -} - -impl MergeAnalysisResult { - pub fn from_git2(analysis: git2::MergeAnalysis) -> Self { - Self { - is_none: analysis.is_none(), - is_normal: analysis.is_normal(), - is_up_to_date: analysis.is_up_to_date(), - is_fast_forward: analysis.is_fast_forward(), - is_unborn: analysis.is_unborn(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MergePreferenceResult { - pub is_none: bool, - pub is_no_fast_forward: bool, - pub is_fastforward_only: bool, -} - -impl MergePreferenceResult { - pub fn from_git2(pref: git2::MergePreference) -> Self { - Self { - is_none: pref.is_none(), - is_no_fast_forward: pref.is_no_fast_forward(), - is_fastforward_only: pref.is_fastforward_only(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MergeheadInfo { - pub oid: CommitOid, -} - -#[derive(Debug, Clone, Default)] -pub struct MergeOptions { - find_renames: bool, - fail_on_conflict: bool, - skip_reuc: bool, - no_recursive: bool, - rename_threshold: u32, - target_limit: u32, - recursion_limit: u32, -} - -impl MergeOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn find_renames(mut self, find: bool) -> Self { - self.find_renames = find; - self - } - - pub fn fail_on_conflict(mut self, fail: bool) -> Self { - self.fail_on_conflict = fail; - self - } - - pub fn skip_reuc(mut self, skip: bool) -> Self { - self.skip_reuc = skip; - self - } - - pub fn no_recursive(mut self, disable: bool) -> Self { - self.no_recursive = disable; - self - } - - pub fn rename_threshold(mut self, thresh: u32) -> Self { - self.rename_threshold = thresh; - self - } - - pub fn target_limit(mut self, limit: u32) -> Self { - self.target_limit = limit; - self - } - - pub fn recursion_limit(mut self, limit: u32) -> Self { - self.recursion_limit = limit; - self - } - - pub fn to_git2(&self) -> git2::MergeOptions { - let mut opts = git2::MergeOptions::new(); - opts.find_renames(self.find_renames); - opts.fail_on_conflict(self.fail_on_conflict); - opts.skip_reuc(self.skip_reuc); - opts.no_recursive(self.no_recursive); - opts.rename_threshold(self.rename_threshold); - opts.target_limit(self.target_limit); - opts.recursion_limit(self.recursion_limit); - opts - } -} diff --git a/libs/git/ref_utils.rs b/libs/git/ref_utils.rs deleted file mode 100644 index 9b8ac4b..0000000 --- a/libs/git/ref_utils.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Shared utility functions for reference name validation. - -use crate::GitError; - -/// # Rules -/// - Must not be empty -/// - Must not start with '.' or '@' (@ is shorthand for HEAD) -/// - Must not end with '/' -/// - Must not contain '..' -/// - Must not contain spaces, '~', '^', ':', '?', '*', '[', or '\' -/// -/// # Returns -/// - `Ok(())` if name is valid -/// - `Err(GitError::InvalidRefName)` if name is invalid -pub fn validate_ref_name(name: &str) -> Result<(), GitError> { - if name.is_empty() - || name.starts_with('.') - || name.starts_with('@') - || name.ends_with('/') - || name.contains("..") - || name.contains(' ') - || name.contains('~') - || name.contains('^') - || name.contains(':') - || name.contains('?') - || name.contains('*') - || name.contains('[') - || name.contains('\\') - { - return Err(GitError::InvalidRefName(format!( - "invalid ref name: {}", - name - ))); - } - Ok(()) -} diff --git a/libs/git/reference/mod.rs b/libs/git/reference/mod.rs deleted file mode 100644 index 2d742b2..0000000 --- a/libs/git/reference/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Reference domain — low-level ref operations with CAS support. -pub mod ops; -pub mod types; diff --git a/libs/git/reference/ops.rs b/libs/git/reference/ops.rs deleted file mode 100644 index fe0719f..0000000 --- a/libs/git/reference/ops.rs +++ /dev/null @@ -1,257 +0,0 @@ -//! Reference operations. - -use crate::commit::types::CommitOid; -use crate::reference::types::RefInfo; -use crate::{GitDomain, GitError, GitResult}; - -/// Specifies what the reference update should be based on. -pub enum RefUpdateTarget { - Oid(CommitOid), -} - -/// Result of a reference update operation. -pub struct RefUpdateResult { - pub name: String, - pub old_oid: Option, - pub new_oid: Option, -} - -impl GitDomain { - /// List all references matching a pattern (e.g. "refs/heads/*"). - pub fn ref_list(&self, pattern: Option<&str>) -> GitResult> { - let mut refs = Vec::new(); - let iter = self - .repo() - .references() - .map_err(|e| GitError::Internal(e.to_string()))?; - - for result in iter { - let r = result.map_err(|e| GitError::Internal(e.to_string()))?; - let name = match r.name() { - Some(n) => n.to_string(), - None => continue, - }; - - if let Some(pat) = pattern { - if !name_match(&name, pat) { - continue; - } - } - - let target = r.target().map(CommitOid::from_git2); - let oid = r - .peel_to_commit() - .ok() - .map(|c| CommitOid::from_git2(c.id())); - - let is_symbolic = r.kind() == Some(git2::ReferenceType::Symbolic); - let is_branch = name.starts_with("refs/heads/"); - let is_remote = name.starts_with("refs/remotes/"); - let is_tag = name.starts_with("refs/tags/"); - let is_note = name.starts_with("refs/notes/"); - refs.push(RefInfo { - name, - oid, - target, - is_symbolic, - is_branch, - is_remote, - is_tag, - is_note, - }); - } - - Ok(refs) - } - - pub fn ref_get(&self, name: &str) -> GitResult { - let r = self - .repo() - .find_reference(name) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - let target = r.target().map(CommitOid::from_git2); - let oid = r - .peel_to_commit() - .ok() - .map(|c| CommitOid::from_git2(c.id())); - - Ok(RefInfo { - name: name.to_string(), - oid, - target, - is_symbolic: r.kind() == Some(git2::ReferenceType::Symbolic), - is_branch: name.starts_with("refs/heads/"), - is_remote: name.starts_with("refs/remotes/"), - is_tag: name.starts_with("refs/tags/"), - is_note: name.starts_with("refs/notes/"), - }) - } - - pub fn ref_create( - &self, - name: &str, - oid: CommitOid, - force: bool, - message: Option<&str>, - ) -> GitResult { - let git_oid = oid - .to_oid() - .map_err(|_| GitError::InvalidOid(oid.to_string()))?; - - let old = self.repo().find_reference(name).ok(); - let old_oid = old - .as_ref() - .and_then(|r| r.target().map(CommitOid::from_git2)); - - self.repo() - .reference(name, git_oid, force, message.unwrap_or("create ref")) - .map_err(|e| { - if !force && e.code() == git2::ErrorCode::Exists { - GitError::BranchExists(name.to_string()) - } else { - GitError::Internal(e.to_string()) - } - })?; - - Ok(RefUpdateResult { - name: name.to_string(), - old_oid, - new_oid: Some(oid), - }) - } - - pub fn ref_delete(&self, name: &str) -> GitResult { - let mut r = self - .repo() - .find_reference(name) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - let target = r - .target() - .map(CommitOid::from_git2) - .ok_or_else(|| GitError::Internal("ref has no target".to_string()))?; - - r.delete().map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(target) - } - - /// Rename a reference. Fails if new name already exists unless `force` is true. - pub fn ref_rename(&self, old_name: &str, new_name: &str, force: bool) -> GitResult { - let mut r = self - .repo() - .find_reference(old_name) - .map_err(|_e| GitError::RefNotFound(old_name.to_string()))?; - - let target = r.target().map(CommitOid::from_git2); - let oid = r - .peel_to_commit() - .ok() - .map(|c| CommitOid::from_git2(c.id())); - let ref_kind = r.kind(); // Capture kind before rename - - if !force && self.repo().find_reference(new_name).is_ok() { - return Err(GitError::BranchExists(new_name.to_string())); - } - - r.rename(new_name, force, "rename ref") - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(RefInfo { - name: new_name.to_string(), - oid, - target, - is_symbolic: ref_kind == Some(git2::ReferenceType::Symbolic), - is_branch: new_name.starts_with("refs/heads/"), - is_remote: new_name.starts_with("refs/remotes/"), - is_tag: new_name.starts_with("refs/tags/"), - is_note: new_name.starts_with("refs/notes/"), - }) - } - - pub fn ref_update( - &self, - name: &str, - new_oid: CommitOid, - expected_oid: Option, - message: Option<&str>, - ) -> GitResult { - let old = self - .repo() - .find_reference(name) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - let old_oid = old.target().map(CommitOid::from_git2); - - // CAS check - if let Some(expected) = expected_oid { - let git_expected = expected - .to_oid() - .map_err(|_| GitError::InvalidOid(expected.to_string()))?; - if old.target() != Some(git_expected) { - return Err(GitError::Internal( - "ref update failed: unexpected current value (CAS mismatch)".to_string(), - )); - } - } - - let git_new_oid = new_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(new_oid.to_string()))?; - - // Use reference_matching for CAS. Pass None as previous target only when - // the ref has no target (symbolic ref with broken target) — fall back to - // unconditional reference update in that case. - match old.target() { - Some(prev) => { - self.repo() - .reference_matching( - name, - git_new_oid, - true, - prev, - message.unwrap_or("update ref"), - ) - .map_err(|e| GitError::Internal(e.to_string()))?; - } - None => { - self.repo() - .reference(name, git_new_oid, true, message.unwrap_or("update ref")) - .map_err(|e| GitError::Internal(e.to_string()))?; - } - } - - Ok(RefUpdateResult { - name: name.to_string(), - old_oid, - new_oid: Some(new_oid), - }) - } - - pub fn ref_exists(&self, name: &str) -> bool { - self.repo().find_reference(name).is_ok() - } - - /// Get the peeled (commit) OID of a reference. - pub fn ref_target(&self, name: &str) -> GitResult> { - let r = self - .repo() - .find_reference(name) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - Ok(r.peel_to_commit() - .ok() - .map(|c| CommitOid::from_git2(c.id()))) - } -} - -fn name_match(name: &str, pattern: &str) -> bool { - if let Some(stripped) = pattern.strip_suffix("/**") { - name.starts_with(stripped) - } else if let Some(stripped) = pattern.strip_suffix("/*") { - name.starts_with(stripped) && !name[stripped.len()..].contains('/') - } else { - name == pattern - } -} diff --git a/libs/git/reference/types.rs b/libs/git/reference/types.rs deleted file mode 100644 index 469c0f6..0000000 --- a/libs/git/reference/types.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Serializable types for the reference domain. - -use serde::{Deserialize, Serialize}; - -use crate::commit::types::CommitOid; - -/// A lightweight ref entry for listing. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RefInfo { - pub name: String, - pub oid: Option, - pub target: Option, - pub is_symbolic: bool, - pub is_branch: bool, - pub is_remote: bool, - pub is_tag: bool, - pub is_note: bool, -} diff --git a/libs/git/ssh/authz.rs b/libs/git/ssh/authz.rs deleted file mode 100644 index 62d28d4..0000000 --- a/libs/git/ssh/authz.rs +++ /dev/null @@ -1,342 +0,0 @@ -use crate::error::GitError; -use base64::{Engine as _, engine::general_purpose}; -use db::database::AppDatabase; -use models::projects::MemberRole; -use models::projects::{project, project_history_name, project_members}; -use models::repos::{repo, repo_history_name}; -use models::users::{user, user_ssh_key}; -use sea_orm::sqlx::types::chrono; -use sea_orm::*; -use sha2::{Digest, Sha256}; - -/// SSH authentication service optimized for performance -pub struct SshAuthService { - db: AppDatabase, -} - -pub struct SshKeyUser { - pub user: user::Model, - pub key_id: i64, - pub key_title: String, -} - -impl SshAuthService { - pub fn new(db: AppDatabase) -> Self { - Self { db } - } - - pub async fn find_repo( - &self, - namespace: &str, - repo_name: &str, - ) -> Result { - let namespace = self.find_namespace(namespace).await?; - self.find_repository_by_name_and_project(repo_name, namespace.id) - .await - } - - async fn find_namespace(&self, namespace: &str) -> Result { - if let Some(project) = project::Entity::find() - .filter(project::Column::Name.eq(namespace)) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - { - return Ok(project); - } - - if let Some(history) = project_history_name::Entity::find() - .filter(project_history_name::Column::HistoryName.eq(namespace)) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - { - if let Some(project) = project::Entity::find() - .filter(project::Column::Id.eq(history.project_uid)) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - { - return Ok(project); - } - } - - Err(GitError::NotFound("Project not found".to_string())) - } - - async fn find_repository_by_name_and_project( - &self, - repo_name: &str, - project_id: uuid::Uuid, - ) -> Result { - if let Some(repo) = repo::Entity::find() - .filter(repo::Column::RepoName.eq(repo_name)) - .filter(repo::Column::Project.eq(project_id)) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - { - return Ok(repo); - } - - if let Some(history) = repo_history_name::Entity::find() - .filter(repo_history_name::Column::Name.eq(repo_name)) - .filter(repo_history_name::Column::Project.eq(project_id)) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - { - if let Some(repo) = repo::Entity::find() - .filter(repo::Column::Id.eq(history.repo)) - .filter(repo::Column::Project.eq(project_id)) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))? - { - return Ok(repo); - } - } - - Err(GitError::NotFound("Repository not found".to_string())) - } - - pub async fn find_user_by_public_key( - &self, - public_key_str: &str, - ) -> Result, DbErr> { - let fingerprint = match self.generate_fingerprint_from_public_key(public_key_str) { - Ok(fp) => fp, - Err(e) => { - tracing::error!("failed to generate SSH key fingerprint error={}", e); - return Ok(None); - } - }; - - let fingerprint_preview = if fingerprint.len() > 16 { - format!("{}...", &fingerprint[..16]) - } else { - fingerprint.clone() - }; - tracing::info!( - "looking up user with SSH key fingerprint={}", - fingerprint_preview - ); - - let ssh_key = user_ssh_key::Entity::find() - .filter(user_ssh_key::Column::Fingerprint.eq(&fingerprint)) - .filter(user_ssh_key::Column::IsRevoked.eq(false)) - .one(self.db.reader()) - .await?; - - let ssh_key = match ssh_key { - Some(key) => key, - None => { - tracing::warn!("no SSH key found fingerprint={}", fingerprint); - return Ok(None); - } - }; - - if self.is_key_expired(&ssh_key) { - tracing::warn!( - "SSH key expired key_id={} expires_at={:?}", - ssh_key.id, - ssh_key.expires_at - ); - return Ok(None); - } - - let user_model = user::Entity::find() - .filter(user::Column::Uid.eq(ssh_key.user)) - .one(self.db.reader()) - .await?; - - if let Some(user) = user_model { - tracing::info!( - "SSH key matched user={} key={}", - user.username, - ssh_key.title - ); - return Ok(Some(SshKeyUser { - user, - key_id: ssh_key.id, - key_title: ssh_key.title, - })); - } - - Ok(None) - } - - fn is_key_expired(&self, ssh_key: &user_ssh_key::Model) -> bool { - if let Some(expires_at) = ssh_key.expires_at { - let now = chrono::Utc::now(); - now >= expires_at - } else { - false - } - } - - pub fn update_key_last_used_async(&self, key_id: i64) { - let db_clone = self.db.clone(); - tokio::spawn(async move { - if let Err(e) = Self::update_key_last_used_sync(db_clone, key_id).await { - tracing::warn!( - "failed to update key last_used key_id={} error={}", - key_id, - e - ); - } - }); - } - - async fn update_key_last_used_sync(db: AppDatabase, key_id: i64) -> Result<(), DbErr> { - let key = user_ssh_key::Entity::find_by_id(key_id) - .one(db.reader()) - .await?; - - if let Some(key) = key { - let now = chrono::Utc::now(); - let mut active_key: user_ssh_key::ActiveModel = key.into(); - active_key.last_used_at = Set(Some(now)); - active_key.updated_at = Set(now); - - active_key.update(db.writer()).await?; - tracing::info!("updated key last_used key_id={}", key_id); - } - - Ok(()) - } - - pub async fn check_repo_permission( - &self, - user: &user::Model, - repo: &repo::Model, - is_write: bool, - ) -> bool { - if repo.created_by == user.uid { - tracing::info!( - "user is repo owner user={} repo={}", - user.username, - repo.repo_name - ); - return true; - } - - if !is_write && !repo.is_private { - tracing::info!("public repo allows read access repo={}", repo.repo_name); - return true; - } - - if self - .check_collaborator_permission(user, repo, is_write) - .await - .unwrap_or(false) - { - tracing::info!( - "user has collaborator access user={} repo={}", - user.username, - repo.repo_name - ); - return true; - } - - let project_id = repo.project; - if self - .check_project_member_permission(user, project_id, is_write) - .await - .unwrap_or(false) - { - tracing::info!( - "user has project member access user={} repo={}", - user.username, - repo.repo_name - ); - return true; - } - - tracing::warn!( - "access denied user={} repo={} is_write={}", - user.username, - repo.repo_name, - is_write - ); - false - } - - async fn check_collaborator_permission( - &self, - user: &user::Model, - repo: &repo::Model, - is_write: bool, - ) -> Result { - use models::repos::repo_collaborator; - - let collaborator = repo_collaborator::Entity::find() - .filter(repo_collaborator::Column::Repo.eq(repo.id)) - .filter(repo_collaborator::Column::User.eq(user.uid)) - .one(self.db.reader()) - .await?; - - if let Some(collab) = collaborator { - let roles: Vec<&str> = collab.scope.split_whitespace().collect(); - if roles.contains(&"admin") || roles.contains(&"write") { - return Ok(true); - } - - if roles.contains(&"read") && !is_write { - return Ok(true); - } - - tracing::warn!("collaborator has no valid roles scope={}", collab.scope); - Ok(false) - } else { - Ok(false) - } - } - - async fn check_project_member_permission( - &self, - user: &user::Model, - project_id: uuid::Uuid, - is_write: bool, - ) -> Result { - let member = project_members::Entity::find() - .filter(project_members::Column::Project.eq(project_id)) - .filter(project_members::Column::User.eq(user.uid)) - .one(self.db.reader()) - .await?; - - if let Some(member) = member { - match member.scope_role() { - Ok(MemberRole::Admin) | Ok(MemberRole::Owner) => Ok(true), - Ok(MemberRole::Member) => Ok(!is_write), - Err(_) => Ok(false), - } - } else { - Ok(false) - } - } - - fn generate_fingerprint_from_public_key(&self, public_key_str: &str) -> Result { - // Performance: avoid allocating Vec, use split_once for efficiency - let key_data_base64 = public_key_str - .split_whitespace() - .nth(1) - .ok_or("Invalid SSH key format")?; - - let key_data = general_purpose::STANDARD - .decode(key_data_base64) - .map_err(|e| format!("Base64 decode error: {}", e))?; - - // Performance: SHA256 is already optimized, compute hash directly - let mut hasher = Sha256::new(); - hasher.update(&key_data); - let hash = hasher.finalize(); - - // Performance: pre-allocate string capacity to avoid reallocation - let mut fingerprint = String::with_capacity(51); // "SHA256:" (7) + base64 (44) - fingerprint.push_str("SHA256:"); - fingerprint.push_str(&general_purpose::STANDARD_NO_PAD.encode(&hash)); - - Ok(fingerprint) - } -} diff --git a/libs/git/ssh/branch_protect.rs b/libs/git/ssh/branch_protect.rs deleted file mode 100644 index a7d9b1d..0000000 --- a/libs/git/ssh/branch_protect.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::ssh::ref_update::RefUpdate; -use models::repos::repo_branch_protect; - -/// Ref name matches a protection rule exactly, or as a directory prefix -/// (e.g. "refs/heads/main" matches "refs/heads/main" and "refs/heads/main/*" -/// but NOT "refs/heads/main-v2"). -fn ref_matches_protection(ref_name: &str, protection_branch: &str) -> bool { - ref_name == protection_branch || ref_name.starts_with(&format!("{}/", protection_branch)) -} - -/// Granular branch protection check (same logic as HTTP handler). -/// Returns `Some(error_message)` if the push should be rejected. -pub fn check_branch_protection( - branch_protects: &[repo_branch_protect::Model], - r#ref: &RefUpdate, -) -> Option { - for protection in branch_protects { - if !ref_matches_protection(&r#ref.name, &protection.branch) { - continue; - } - - // Check deletion (new_oid is all zeros) - if r#ref.new_oid == "0000000000000000000000000000000000000000" { - if protection.forbid_deletion { - return Some(format!( - "GitData: 🛡️ protected branch rejected. Deletion of '{}' is forbidden. Create a PR or ask a maintainer to update branch protection.", - r#ref.name - )); - } - continue; - } - - // Check tag push - if r#ref.name.starts_with("refs/tags/") { - if protection.forbid_tag_push { - return Some(format!( - "GitData: 🛡️ protected ref rejected. Tag push to '{}' is forbidden by branch protection.", - r#ref.name - )); - } - continue; - } - - // Check force push: old != new AND old is non-zero (non-fast-forward) - let is_new_branch = r#ref.old_oid == "0000000000000000000000000000000000000000"; - if !is_new_branch - && r#ref.old_oid != r#ref.new_oid - && r#ref.name.starts_with("refs/heads/") - && protection.forbid_force_push - { - return Some(format!( - "GitData: 🛡️ protected branch rejected. Force push to '{}' is forbidden. Create a PR instead of rewriting protected history.", - r#ref.name - )); - } - - // Check push - if protection.forbid_push { - return Some(format!( - "GitData: 🛡️ protected branch rejected. Direct push to '{}' is forbidden. Please push to a feature branch and create a PR.", - r#ref.name - )); - } - } - None -} diff --git a/libs/git/ssh/forward.rs b/libs/git/ssh/forward.rs deleted file mode 100644 index 2ed144c..0000000 --- a/libs/git/ssh/forward.rs +++ /dev/null @@ -1,50 +0,0 @@ -use russh::ChannelId; -use russh::server::Handle; -use std::future::Future; -use std::time::Duration; -use tokio::io::{AsyncRead, AsyncReadExt}; -use tokio::time::sleep; -use tokio_util::bytes::Bytes; - -pub async fn forward<'a, R, Fut, Fwd>( - session_handle: &'a Handle, - chan_id: ChannelId, - r: &mut R, - mut fwd: Fwd, -) -> Result<(), russh::Error> -where - R: AsyncRead + Send + Unpin, - Fut: Future> + 'a, - Fwd: FnMut(&'a Handle, ChannelId, Bytes) -> Fut, -{ - const BUF_SIZE: usize = 1024 * 32; - const MAX_RETRIES: usize = 5; - const RETRY_DELAY: u64 = 10; // ms - - let mut buf = [0u8; BUF_SIZE]; - loop { - let read = r.read(&mut buf).await?; - - if read == 0 { - break; - } - - let mut chunk = Bytes::copy_from_slice(&buf[..read]); - let mut retries = 0; - loop { - match fwd(session_handle, chan_id, chunk).await { - Ok(()) => break, - Err(unsent) => { - retries += 1; - if retries >= MAX_RETRIES { - return Ok(()); - } - chunk = unsent; - sleep(Duration::from_millis(RETRY_DELAY)).await; - } - } - } - } - - Ok(()) -} diff --git a/libs/git/ssh/git_service.rs b/libs/git/ssh/git_service.rs deleted file mode 100644 index b0e21b8..0000000 --- a/libs/git/ssh/git_service.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::path::PathBuf; -use std::str::FromStr; - -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum GitService { - UploadPack, - ReceivePack, - UploadArchive, -} - -impl FromStr for GitService { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "upload-pack" => Ok(Self::UploadPack), - "receive-pack" => Ok(Self::ReceivePack), - "upload-archive" => Ok(Self::UploadArchive), - _ => Err(()), - } - } -} - -pub fn parse_git_command(cmd: &str) -> Option<(GitService, &str)> { - let (svc, path) = match cmd.split_once(' ') { - Some(("git-receive-pack", path)) => (GitService::ReceivePack, path), - Some(("git-upload-pack", path)) => (GitService::UploadPack, path), - Some(("git-upload-archive", path)) => (GitService::UploadArchive, path), - _ => return None, - }; - Some((svc, strip_apostrophes(path))) -} - -pub fn parse_repo_path(path: &str) -> Option<(&str, &str)> { - let path = path.trim_matches('/'); - let mut parts = path.splitn(2, '/'); - match (parts.next(), parts.next()) { - (Some(owner), Some(repo)) if !owner.is_empty() && !repo.is_empty() => Some((owner, repo)), - _ => None, - } -} - -pub fn build_git_command(service: GitService, path: PathBuf) -> tokio::process::Command { - let mut cmd = tokio::process::Command::new("git"); - - let cwd = match path.canonicalize() { - Ok(p) => p, - Err(e) => { - tracing::debug!(error = %e, "path canonicalize failed, falling back to raw path"); - path.clone() - } - }; - cmd.current_dir(cwd); - - match service { - GitService::UploadPack => { - cmd.arg("upload-pack"); - } - GitService::ReceivePack => { - cmd.arg("receive-pack"); - } - GitService::UploadArchive => { - cmd.arg("upload-archive"); - } - } - - cmd.arg(".") - .env("GIT_CONFIG_NOSYSTEM", "1") - .env("GIT_NO_REPLACE_OBJECTS", "1"); - - #[cfg(unix)] - { - cmd.env("GIT_CONFIG_GLOBAL", "/dev/null") - .env("GIT_CONFIG_SYSTEM", "/dev/null"); - } - #[cfg(windows)] - { - let nul = "NUL"; - cmd.env("GIT_CONFIG_GLOBAL", nul) - .env("GIT_CONFIG_SYSTEM", nul); - } - - cmd -} - -fn strip_apostrophes(s: &str) -> &str { - s.trim_matches('\'') -} diff --git a/libs/git/ssh/handle.rs b/libs/git/ssh/handle.rs deleted file mode 100644 index aa5cf23..0000000 --- a/libs/git/ssh/handle.rs +++ /dev/null @@ -1,992 +0,0 @@ -use crate::ssh::ReceiveSyncService; -use crate::ssh::RepoReceiveSyncTask; -use crate::ssh::SshTokenService; -use crate::ssh::authz::SshAuthService; -use crate::ssh::branch_protect::check_branch_protection; -use crate::ssh::forward::forward; -use crate::ssh::git_service::{GitService, build_git_command, parse_git_command, parse_repo_path}; -use crate::ssh::push_queue::{PushQueueEvent, PushQueueWaitError, wait_for_push_queue_slot}; -use crate::ssh::ref_update::RefUpdate; -use db::cache::AppCache; -use db::database::AppDatabase; -use models::repos::{repo, repo_branch_protect}; -use models::users::user; -use russh::keys::{Certificate, PublicKey}; -use russh::server::{Auth, Msg, Session}; -use russh::{Channel, ChannelId, Disconnect}; -use sea_orm::ColumnTrait; -use sea_orm::EntityTrait; -use sea_orm::QueryFilter; -use std::collections::{HashMap, HashSet}; -use std::io; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::process::Stdio; -use std::sync::Arc; -use std::time::Duration; -use tokio_util::bytes::Bytes; - -const PRE_PACK_LIMIT: usize = 1_048_576; -const ZERO_OID: &str = "0000000000000000000000000000000000000000"; -use tokio::io::AsyncWriteExt; -use tokio::process::ChildStdin; -use tokio::sync::{Mutex, mpsc::Sender}; -use tokio::time::sleep; -pub struct SSHandle { - pub repo: Option, - pub model: Option, - pub stdin: HashMap, - pub eof: HashMap>, - pub operator: Option, - pub db: AppDatabase, - pub auth: SshAuthService, - pub buffer: HashMap>, - pub branch: HashMap>, - pub post_receive_refs: HashMap>>>, - pub service: Option, - pub cache: AppCache, - pub sync: ReceiveSyncService, - pub upload_pack_eof_sent: HashSet, - pub token_service: SshTokenService, - pub client_addr: Option, -} - -impl SSHandle { - pub fn new( - db: AppDatabase, - cache: AppCache, - sync: ReceiveSyncService, - token_service: SshTokenService, - client_addr: Option, - ) -> Self { - let auth = SshAuthService::new(db.clone()); - let addr_str = client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - tracing::info!("SSH handler created client={}", addr_str); - Self { - repo: None, - model: None, - stdin: HashMap::new(), - eof: HashMap::new(), - operator: None, - db, - auth, - buffer: HashMap::new(), - branch: HashMap::new(), - post_receive_refs: HashMap::new(), - service: None, - cache, - sync, - upload_pack_eof_sent: HashSet::new(), - token_service, - client_addr, - } - } - - fn cleanup_channel(&mut self, channel_id: ChannelId) { - if let Some(stdin) = self.stdin.remove(&channel_id) { - let channel_id_for_task = channel_id; - tokio::spawn(async move { - let _ = tokio::time::timeout(Duration::from_secs(5), async { - let mut stdin = stdin; - if let Err(e) = stdin.flush().await { - tracing::warn!(error = %e, "ssh_cleanup_flush_failed channel={:?}", channel_id_for_task); - } - let _ = stdin.shutdown().await; - }) - .await; - }); - } - self.eof.remove(&channel_id); - self.post_receive_refs.remove(&channel_id); - self.upload_pack_eof_sent.remove(&channel_id); - } - - fn format_post_receive_hints( - namespace: &str, - repo: &repo::Model, - refs: &[RefUpdate], - queue: Option<(usize, usize)>, - ) -> String { - let mut lines = Vec::new(); - for r#ref in refs { - if r#ref.old_oid == ZERO_OID && r#ref.name.starts_with("refs/heads/") { - let branch = r#ref.name.trim_start_matches("refs/heads/"); - lines.push(format!( - "remote: GitData: 🌱 new branch '{}' pushed. Create a PR: /{}/repo/{}/pulls/new?head={}\r\n", - branch, - namespace, - repo.repo_name, - branch - )); - } - } - if let Some((position, total)) = queue { - lines.push(format!( - "remote: GitData: ⏳ repository sync queued ({}/{}). Metadata, webhooks and search indexes will update shortly.\r\n", - position, total - )); - } - lines.concat() - } -} - -impl Drop for SSHandle { - fn drop(&mut self) { - let addr_str = self - .client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - tracing::info!("ssh_handler_dropped client={}", addr_str); - - let channel_ids: Vec<_> = self.stdin.keys().copied().collect(); - for channel_id in channel_ids { - self.cleanup_channel(channel_id); - } - } -} - -impl russh::server::Handler for SSHandle { - type Error = russh::Error; - - async fn auth_none(&mut self, user: &str) -> Result { - let client_info = self - .client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - tracing::info!("auth_none_received user={} client={}", user, client_info); - Ok(Auth::UnsupportedMethod) - } - - async fn auth_password(&mut self, _user: &str, token: &str) -> Result { - let client_info = self - .client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - - if token.is_empty() { - tracing::warn!("auth_rejected_empty_token client={}", client_info); - return Err(russh::Error::NotAuthenticated); - } - - tracing::info!("auth_token_attempt client={}", client_info); - - let user_model = match self.token_service.find_user_by_token(token).await { - Ok(Some(model)) => model, - Ok(None) => { - tracing::warn!("auth_rejected_token_not_found client={}", client_info); - return Err(russh::Error::NotAuthenticated); - } - Err(e) => { - tracing::error!("auth_token_error error={}", e); - return Err(russh::Error::NotAuthenticated); - } - }; - - tracing::info!( - "auth_token_success user={} client={}", - user_model.username, - client_info - ); - self.operator = Some(user_model); - Ok(Auth::Accept) - } - async fn auth_publickey_offered( - &mut self, - user: &str, - public_key: &PublicKey, - ) -> Result { - let client_info = self - .client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - - if user != "git" { - tracing::warn!( - "auth_publickey_offer_rejected_invalid_username user={} client={}", - user, - client_info - ); - return Err(russh::Error::NotAuthenticated); - } - - let public_key_str = public_key.to_string(); - if public_key_str.len() < 32 { - tracing::warn!( - "auth_publickey_offer_rejected_invalid_key_length key_length={}", - public_key_str.len() - ); - return Err(russh::Error::NotAuthenticated); - } - - tracing::info!("auth_publickey_offer client={}", client_info); - match self.auth.find_user_by_public_key(&public_key_str).await { - Ok(Some(key_user)) => { - tracing::info!( - "auth_publickey_offer_accepted user={} key={} client={}", - key_user.user.username, - key_user.key_title, - client_info - ); - Ok(Auth::Accept) - } - Ok(None) => { - tracing::warn!( - "auth_publickey_offer_rejected_key_not_found client={}", - client_info - ); - Err(russh::Error::NotAuthenticated) - } - Err(e) => { - tracing::error!("auth_publickey_offer_error error={}", e); - Err(russh::Error::NotAuthenticated) - } - } - } - async fn auth_publickey( - &mut self, - user: &str, - public_key: &PublicKey, - ) -> Result { - let client_info = self - .client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - - if user != "git" { - tracing::warn!( - "auth_rejected_invalid_username user={} client={}", - user, - client_info - ); - return Err(russh::Error::NotAuthenticated); - } - let public_key_str = public_key.to_string(); - if public_key_str.len() < 32 { - tracing::warn!( - "auth_rejected_invalid_key_length key_length={}", - public_key_str.len() - ); - return Err(russh::Error::NotAuthenticated); - } - - tracing::info!("auth_publickey_attempt client={}", client_info); - let key_user = match self.auth.find_user_by_public_key(&public_key_str).await { - Ok(Some(key_user)) => key_user, - Ok(None) => { - tracing::warn!("auth_rejected_key_not_found client={}", client_info); - return Err(russh::Error::NotAuthenticated); - } - Err(e) => { - tracing::error!("auth_publickey_error error={}", e); - return Err(russh::Error::NotAuthenticated); - } - }; - - tracing::info!( - "auth_publickey_success user={} client={}", - key_user.user.username, - client_info - ); - self.auth.update_key_last_used_async(key_user.key_id); - self.operator = Some(key_user.user); - Ok(Auth::Accept) - } - async fn auth_openssh_certificate( - &mut self, - user: &str, - certificate: &Certificate, - ) -> Result { - let client_info = self - .client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - - if user != "git" { - tracing::warn!( - "auth_rejected_invalid_username user={} client={}", - user, - client_info - ); - return Err(russh::Error::NotAuthenticated); - } - let public_key_str = certificate.to_string(); - if public_key_str.len() < 32 { - tracing::warn!( - "auth_rejected_invalid_key_length key_length={}", - public_key_str.len() - ); - return Err(russh::Error::NotAuthenticated); - } - - tracing::info!("auth_publickey_attempt client={}", client_info); - let key_user = match self.auth.find_user_by_public_key(&public_key_str).await { - Ok(Some(key_user)) => key_user, - Ok(None) => { - tracing::warn!("auth_rejected_key_not_found client={}", client_info); - return Err(russh::Error::NotAuthenticated); - } - Err(e) => { - tracing::error!("auth_publickey_error error={}", e); - return Err(russh::Error::NotAuthenticated); - } - }; - - tracing::info!( - "auth_publickey_success user={} client={}", - key_user.user.username, - client_info - ); - self.auth.update_key_last_used_async(key_user.key_id); - self.operator = Some(key_user.user); - Ok(Auth::Accept) - } - async fn authentication_banner(&mut self) -> Result, Self::Error> { - Ok(None) - } - - async fn channel_close( - &mut self, - channel: ChannelId, - _: &mut Session, - ) -> Result<(), Self::Error> { - tracing::info!( - "channel_close channel={:?} client={:?}", - channel, - self.client_addr - ); - self.cleanup_channel(channel); - Ok(()) - } - - async fn channel_eof( - &mut self, - channel: ChannelId, - _: &mut Session, - ) -> Result<(), Self::Error> { - tracing::info!( - "channel_eof channel={:?} client={:?}", - channel, - self.client_addr - ); - - if let Some(eof) = self.eof.get(&channel) { - let _ = eof.send(true).await; - } - - if let Some(mut stdin) = self.stdin.remove(&channel) { - tracing::info!( - "Closing stdin channel={:?} client={:?}", - channel, - self.client_addr - ); - // Use timeout so we never block the SSH event loop waiting for git. - let _ = tokio::time::timeout(Duration::from_secs(5), async { - if let Err(e) = stdin.flush().await { - tracing::warn!(error = %e, "ssh_eof_flush_failed channel={:?}", channel); - } - let _ = stdin.shutdown().await; - }) - .await; - tracing::info!( - "stdin closed channel={:?} client={:?}", - channel, - self.client_addr - ); - } else { - tracing::warn!( - "stdin already removed channel={:?} client={:?}", - channel, - self.client_addr - ); - } - - Ok(()) - } - - async fn channel_open_session( - &mut self, - channel: Channel, - session: &mut Session, - ) -> Result { - let client_info = self - .client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - tracing::info!( - "channel_open_session channel={:?} client={}", - channel, - client_info - ); - if let Err(e) = session.flush() { - tracing::warn!(error = %e, "ssh_session_flush_failed"); - } - Ok(true) - } - - async fn pty_request( - &mut self, - channel: ChannelId, - term: &str, - col_width: u32, - row_height: u32, - _pix_width: u32, - _pix_height: u32, - _modes: &[(russh::Pty, u32)], - session: &mut Session, - ) -> Result<(), Self::Error> { - tracing::warn!( - "pty_request not supported channel={:?} term={} cols={} rows={}", - channel, - term, - col_width, - row_height - ); - if let Err(e) = session.flush() { - tracing::warn!(error = %e, "ssh_session_flush_failed"); - } - Ok(()) - } - - async fn subsystem_request( - &mut self, - channel: ChannelId, - name: &str, - session: &mut Session, - ) -> Result<(), Self::Error> { - tracing::info!("subsystem_request channel={:?} subsystem={}", channel, name); - // git-clients may send "subsystem" for git protocol over ssh. - // We don't use subsystem; exec_request handles it directly. - if let Err(e) = session.flush() { - tracing::warn!(error = %e, "ssh_session_flush_failed"); - } - Ok(()) - } - async fn data( - &mut self, - channel: ChannelId, - data: &[u8], - session: &mut Session, - ) -> Result<(), Self::Error> { - if matches!(self.service, Some(GitService::ReceivePack)) { - if !self.branch.contains_key(&channel) { - let bf = self.buffer.entry(channel).or_default(); - - // Reject oversized pre-PACK data to prevent memory exhaustion - if bf.len() + data.len() > PRE_PACK_LIMIT { - tracing::warn!("ssh_pre_pack_too_large channel={:?}", channel); - let msg = "remote: Ref negotiation exceeds size limit\r\n"; - let _ = - session.extended_data(channel, 1, Bytes::copy_from_slice(msg.as_bytes())); - let _ = session.exit_status_request(channel, 1); - let _ = session.eof(channel); - let _ = session.close(channel); - self.cleanup_channel(channel); - return Ok(()); - } - - bf.extend_from_slice(data); - - if !bf.windows(4).any(|w| w == b"0000") { - return Ok(()); - } - - let buffered = self.buffer.remove(&channel).unwrap_or_default(); - - match RefUpdate::parse_ref_updates(&buffered) { - Ok(refs) => { - if let Some(model) = &self.model { - let branch_protect_roles = repo_branch_protect::Entity::find() - .filter(repo_branch_protect::Column::Repo.eq(model.id)) - .all(self.db.reader()) - .await - .map_err(|e| { - russh::Error::IO(io::Error::new(io::ErrorKind::Other, e)) - })?; - - for r#ref in &refs { - if let Some(msg) = - check_branch_protection(&branch_protect_roles, r#ref) - { - let full_msg = format!("remote: {}\r\n", msg); - let _ = session.extended_data( - channel, - 1, - Bytes::copy_from_slice(full_msg.as_bytes()), - ); - let _ = session.exit_status_request(channel, 1); - let _ = session.eof(channel); - let _ = session.close(channel); - self.cleanup_channel(channel); - return Ok(()); - } - } - } - if let Some(refs_for_hints) = self.post_receive_refs.get(&channel) { - *refs_for_hints.lock().await = refs.clone(); - } - self.branch.insert(channel, refs); - } - Err(e) => { - tracing::warn!("ref_update_parse_error error={:?}", e); - if let Some(refs_for_hints) = self.post_receive_refs.get(&channel) { - refs_for_hints.lock().await.clear(); - } - self.branch.insert(channel, vec![]); - } - } - - if let Some(stdin) = self.stdin.get_mut(&channel) { - stdin.write_all(&buffered).await?; - stdin.flush().await?; - } else { - tracing::error!("stdin_not_found channel={:?}", channel); - } - return Ok(()); - } - - if let Some(stdin) = self.stdin.get_mut(&channel) { - stdin.write_all(data).await?; - stdin.flush().await?; - } else { - tracing::error!("stdin_not_found_forwarding channel={:?}", channel); - } - return Ok(()); - } - - if let Some(stdin) = self.stdin.get_mut(&channel) { - stdin.write_all(data).await?; - if matches!(self.service, Some(GitService::UploadPack)) - && !self.upload_pack_eof_sent.contains(&channel) - { - let has_flush_pkt = data.windows(4).any(|w| w == b"0000"); - if has_flush_pkt { - stdin.flush().await?; - let _ = stdin.shutdown().await; - self.upload_pack_eof_sent.insert(channel); - } - } - } - Ok(()) - } - async fn shell_request( - &mut self, - channel_id: ChannelId, - session: &mut Session, - ) -> Result<(), Self::Error> { - if let Some(user) = &self.operator { - let welcome_msg = format!( - "Hi {}! You've successfully authenticated, but GitData does not provide shell access.\r\n", - user.username - ); - - tracing::info!("shell_request user={}", user.username); - let _ = session.data(channel_id, Bytes::copy_from_slice(welcome_msg.as_bytes())); - let _ = session.exit_status_request(channel_id, 0); - let _ = session.eof(channel_id); - let _ = session.close(channel_id); - let _ = session.flush(); - } else { - tracing::warn!("shell_request_unauthenticated channel={:?}", channel_id); - let msg = "Authentication required\r\n"; - let _ = session.data(channel_id, Bytes::copy_from_slice(msg.as_bytes())); - let _ = session.exit_status_request(channel_id, 1); - let _ = session.eof(channel_id); - let _ = session.close(channel_id); - let _ = session.flush(); - } - Ok(()) - } - async fn exec_request( - &mut self, - channel_id: ChannelId, - data: &[u8], - session: &mut Session, - ) -> Result<(), Self::Error> { - let client_info = self - .client_addr - .map(|addr| format!("{}", addr)) - .unwrap_or_else(|| "unknown".to_string()); - - tracing::info!( - "exec_request received channel={:?} client={}", - channel_id, - client_info - ); - - let git_shell_cmd = match std::str::from_utf8(data) { - Ok(cmd) => cmd.trim(), - Err(e) => { - tracing::error!("invalid_command_encoding error={}", e); - let _ = session.disconnect( - Disconnect::ServiceNotAvailable, - "Invalid command encoding", - "", - ); - return Err(russh::Error::Disconnect); - } - }; - let (service, path) = match parse_git_command(git_shell_cmd) { - Some((s, p)) => (s, p), - None => { - tracing::error!("invalid_git_command command={}", git_shell_cmd); - let msg = format!("Invalid git command: {}", git_shell_cmd); - let _ = session.disconnect(Disconnect::ServiceNotAvailable, &msg, ""); - return Err(russh::Error::Disconnect); - } - }; - self.service = Some(service); - let (owner, repo) = match parse_repo_path(path) { - Some(pair) => pair, - None => { - let msg = format!("Invalid repository path: {}", path); - tracing::error!("invalid_repo_path path={}", path); - let _ = session.disconnect(Disconnect::ServiceNotAvailable, &msg, ""); - return Err(russh::Error::Disconnect); - } - }; - let namespace = owner.to_string(); - let repo = repo.strip_suffix(".git").unwrap_or(repo).to_string(); - - let repo = match self.auth.find_repo(owner, &repo).await { - Ok(repo) => repo, - Err(e) => { - // Log the detailed error internally; client receives generic message. - tracing::error!("repo_fetch_error error={}", e); - let _ = - session.disconnect(Disconnect::ServiceNotAvailable, "Repository not found", ""); - return Err(russh::Error::Disconnect); - } - }; - - self.model = Some(repo.clone()); - let operator = match &self.operator { - Some(user) => user, - None => { - let msg = "Authentication error: no authenticated user"; - tracing::error!("exec_no_authenticated_user channel={:?}", channel_id); - let _ = session.disconnect(Disconnect::ByApplication, msg, ""); - return Err(russh::Error::Disconnect); - } - }; - - let is_write = service == GitService::ReceivePack; - let has_permission = self - .auth - .check_repo_permission(operator, &repo, is_write) - .await; - - if !has_permission { - let msg = format!( - "Access denied: user '{}' does not have {} permission for repository {}", - operator.username, - if is_write { "write" } else { "read" }, - repo.repo_name - ); - tracing::error!( - "access_denied user={} repo={} is_write={}", - operator.username, - repo.repo_name, - is_write - ); - let _ = session.disconnect(Disconnect::ByApplication, &msg, ""); - return Err(russh::Error::Disconnect); - } - - tracing::info!( - "access_granted user={} repo={} is_write={}", - operator.username, - repo.repo_name, - is_write - ); - - let mut push_queue_lease = if is_write { - let repo_id = repo.id; - let queue_result = - wait_for_push_queue_slot(self.sync.clone(), repo_id, |event, request_id| { - let request_id = request_id.to_string(); - match event { - PushQueueEvent::Waiting(position) => { - let msg = format!( - "remote: GitData: ⏳ another push is running for this repository. Queued {}/{}.\r\n", - position.position, position.total - ); - let _ = session.extended_data( - channel_id, - 1, - Bytes::copy_from_slice(msg.as_bytes()), - ); - let _ = session.flush(); - tracing::info!( - repo_id = %repo_id, - request_id = %request_id, - position = position.position, - total = position.total, - "push_queue_waiting" - ); - } - PushQueueEvent::Acquired => { - let msg = "remote: GitData: 🚀 push queue slot acquired. Processing now.\r\n"; - let _ = session.extended_data( - channel_id, - 1, - Bytes::copy_from_slice(msg.as_bytes()), - ); - let _ = session.flush(); - tracing::info!( - repo_id = %repo_id, - request_id = %request_id, - "push_queue_acquired" - ); - } - } - }) - .await; - - match queue_result { - Ok(lease) => Some(lease), - Err(error) => { - match &error { - PushQueueWaitError::Join(e) => { - tracing::error!(error = %e, repo = %repo.repo_name, "push_queue_join_failed"); - let msg = "remote: GitData: ⛔ push queue is temporarily unavailable. Please retry later.\r\n"; - let _ = session.extended_data( - channel_id, - 1, - Bytes::copy_from_slice(msg.as_bytes()), - ); - } - PushQueueWaitError::Lock(e) => { - tracing::error!(error = %e, repo_id = %repo.id, "push_queue_lock_failed"); - let msg = "remote: GitData: ⛔ push queue lock failed. Please retry later.\r\n"; - let _ = session.extended_data( - channel_id, - 1, - Bytes::copy_from_slice(msg.as_bytes()), - ); - } - PushQueueWaitError::Timeout => { - tracing::warn!(repo_id = %repo.id, "push_queue_timeout"); - let msg = "remote: GitData: ⏱️ push queue timed out. Please retry in a moment.\r\n"; - let _ = session.extended_data( - channel_id, - 1, - Bytes::copy_from_slice(msg.as_bytes()), - ); - } - } - let _ = session.channel_failure(channel_id); - let _ = session.close(channel_id); - self.cleanup_channel(channel_id); - return if matches!(error, PushQueueWaitError::Timeout) { - Ok(()) - } else { - Err(russh::Error::IO(io::Error::new( - io::ErrorKind::Other, - error.to_string(), - ))) - }; - } - } - } else { - None - }; - - let repo_path = PathBuf::from(&repo.storage_path); - if !repo_path.exists() { - tracing::error!("repo_path_not_found path={}", repo.storage_path); - } - let mut cmd = build_git_command(service, repo_path); - - tracing::info!( - "spawn_git_process service={:?} path={}", - service, - repo.storage_path - ); - let mut shell = match cmd - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - { - Ok(shell) => { - let _ = session.channel_success(channel_id); - shell - } - Err(e) => { - tracing::error!("process_spawn_failed error={}", e); - if let Some(lease) = &mut push_queue_lease { - lease.release().await; - } - let _ = session.channel_failure(channel_id); - self.cleanup_channel(channel_id); - return Err(russh::Error::IO(e)); - } - }; - let session_handle = session.handle(); - let stdin = match shell.stdin.take() { - Some(s) => s, - None => { - tracing::error!("stdin pipe unavailable for channel={:?}", channel_id); - if let Some(lease) = &mut push_queue_lease { - lease.release().await; - } - let _ = session_handle.channel_failure(channel_id).await; - return Err(russh::Error::IO(io::Error::new( - io::ErrorKind::Other, - "stdin unavailable", - ))); - } - }; - self.stdin.insert(channel_id, stdin); - let mut shell_stdout = match shell.stdout.take() { - Some(s) => s, - None => { - tracing::error!("stdout pipe unavailable for channel={:?}", channel_id); - if let Some(lease) = &mut push_queue_lease { - lease.release().await; - } - return Err(russh::Error::IO(io::Error::new( - io::ErrorKind::Other, - "stdout unavailable", - ))); - } - }; - let mut shell_stderr = match shell.stderr.take() { - Some(s) => s, - None => { - tracing::error!("stderr pipe unavailable for channel={:?}", channel_id); - if let Some(lease) = &mut push_queue_lease { - lease.release().await; - } - return Err(russh::Error::IO(io::Error::new( - io::ErrorKind::Other, - "stderr unavailable", - ))); - } - }; - - let (eof_tx, mut eof_rx) = tokio::sync::mpsc::channel::(10); - self.eof.insert(channel_id, eof_tx); - let refs_for_hints = Arc::new(Mutex::new(Vec::new())); - self.post_receive_refs - .insert(channel_id, refs_for_hints.clone()); - let repo_uid = repo.id; - let repo_for_hints = repo.clone(); - let namespace_for_hints = namespace.clone(); - let should_sync = service == GitService::ReceivePack; - let sync = self.sync.clone(); - let mut push_queue_lease = push_queue_lease; - - let fut = async move { - tracing::info!(channel = ?channel_id, "git_task_started"); - - let mut stdout_done = false; - let mut stderr_done = false; - - let stdout_fut = forward( - &session_handle, - channel_id, - &mut shell_stdout, - |handle, chan, data| async move { handle.data(chan, data).await }, - ); - tokio::pin!(stdout_fut); - - let stderr_fut = forward( - &session_handle, - channel_id, - &mut shell_stderr, - |handle, chan, data| async move { handle.extended_data(chan, 1, data).await }, - ); - tokio::pin!(stderr_fut); - - loop { - tokio::select! { - result = shell.wait() => { - let status = match result { - Ok(status) => status, - Err(e) => { - if let Some(lease) = &mut push_queue_lease { - lease.release().await; - } - return Err(russh::Error::IO(e)); - } - }; - let status_code = status.code().unwrap_or(128) as u32; - - tracing::info!("git_process_exited channel={:?} status={}", channel_id, status_code); - - if let Some(lease) = &mut push_queue_lease { - lease.release().await; - } - - if !stdout_done || !stderr_done { - let _ = tokio::time::timeout(Duration::from_millis(100), async { - tokio::join!( - async { - if !stdout_done { - let _ = (&mut stdout_fut).await; - } - }, - async { - if !stderr_done { - let _ = (&mut stderr_fut).await; - } - } - ); - }).await; - } - - if should_sync && status_code == 0 { - let queue = sync.send(RepoReceiveSyncTask { repo_uid }).await; - let refs_for_hints = refs_for_hints.lock().await.clone(); - let msg = SSHandle::format_post_receive_hints( - &namespace_for_hints, - &repo_for_hints, - &refs_for_hints, - queue, - ); - if !msg.is_empty() { - let _ = session_handle - .extended_data(channel_id, 1, Bytes::copy_from_slice(msg.as_bytes())) - .await; - } - } - - let _ = session_handle.exit_status_request(channel_id, status_code).await; - sleep(Duration::from_millis(50)).await; - let _ = session_handle.eof(channel_id).await; - let _ = session_handle.close(channel_id).await; - tracing::info!(channel = ?channel_id, "channel_closed"); - break; - } - result = &mut stdout_fut, if !stdout_done => { - tracing::info!("stdout completed"); - stdout_done = true; - if let Err(e) = result { - tracing::warn!(error = ?e, "stdout_forward_error"); - } - } - result = &mut stderr_fut, if !stderr_done => { - tracing::info!("stderr completed"); - stderr_done = true; - if let Err(e) = result { - tracing::warn!(error = ?e, "stderr_forward_error"); - } - } - } - } - - Ok::<(), russh::Error>(()) - }; - - tokio::spawn(async move { - if let Err(e) = fut.await { - tracing::error!("git_ssh_channel_task_error error={}", e); - } - while eof_rx.recv().await.is_some() {} - }); - Ok(()) - } -} diff --git a/libs/git/ssh/mod.rs b/libs/git/ssh/mod.rs deleted file mode 100644 index 154c5fd..0000000 --- a/libs/git/ssh/mod.rs +++ /dev/null @@ -1,498 +0,0 @@ -use crate::error::GitError; -use crate::hook::pool::types::{HookTask, TaskType}; -use anyhow::Context; -use argon2::Argon2; -use argon2::password_hash::{PasswordHash, PasswordVerifier}; -use config::AppConfig; -use db::cache::AppCache; -use db::database::AppDatabase; -use deadpool_redis::cluster::Pool as RedisPool; -use models::users::{user, user_token}; -use redis::AsyncCommands; -use russh::keys::PrivateKey; -use russh::server::Server; -use russh::{MethodKind, MethodSet, SshId, server::Config}; -use sea_orm::prelude::*; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; - -pub mod authz; -pub mod branch_protect; -pub mod forward; -pub mod git_service; -pub mod handle; -pub mod push_queue; -pub mod rate_limit; -pub mod ref_update; -pub mod server; -#[derive(Clone)] -pub struct SSHHandle { - pub db: AppDatabase, - pub app: AppConfig, - pub cache: AppCache, - pub redis_pool: RedisPool, -} - -impl SSHHandle { - pub async fn run(&self) { - let this = self.clone(); - tokio::spawn(async move { - if let Err(e) = this.run_ssh().await { - tracing::error!("SSH server error: {}", e); - } - }); - } - pub fn new(db: AppDatabase, app: AppConfig, cache: AppCache, redis_pool: RedisPool) -> Self { - SSHHandle { - db, - app, - cache, - redis_pool, - } - } - pub async fn run_ssh(&self) -> anyhow::Result<()> { - tracing::info!("SSH server starting"); - let private_key_content = self.app.ssh_server_private_key()?; - if private_key_content.is_empty() { - return Err(anyhow::anyhow!("SSH server private key is not configured")); - } - - tracing::info!( - "Loading SSH private key (hex, {} bytes)", - private_key_content.len() - ); - - let private_key_bytes = hex::decode(&private_key_content).with_context(|| { - format!( - "Failed to decode hex-encoded SSH private key ({} bytes)", - private_key_content.len() - ) - })?; - - tracing::info!("Hex decoded to {} bytes", private_key_bytes.len()); - - let private_key_pem = std::str::from_utf8(&private_key_bytes) - .with_context(|| "Decoded SSH private key is not valid UTF-8")?; - - if let Some(first_line) = private_key_pem.lines().next() { - tracing::info!("PEM header: {}", first_line); - } - // Do NOT log the full private key content — that would be a severe security leak - - let private_key = { - match ssh_key::PrivateKey::from_openssh(private_key_pem) { - Ok(ssh_key) => { - tracing::info!("Successfully parsed with ssh-key crate"); - let openssh_pem = ssh_key - .to_openssh(ssh_key::LineEnding::LF) - .with_context(|| "Failed to serialize to OpenSSH format")?; - - PrivateKey::from_str(&openssh_pem) - .with_context(|| "Failed to parse with russh after ssh-key conversion")? - } - Err(e) => { - tracing::info!( - "ssh-key from_openssh failed: {}, trying direct russh parse", - e - ); - PrivateKey::from_str(private_key_pem).with_context(|| { - format!("Failed to parse SSH private key with both methods") - })? - } - } - }; - tracing::info!("SSH private key loaded"); - let mut config = Config::default(); - config.keys = vec![private_key]; - let version = format!("SSH-2.0-GitdataAI {}", env!("CARGO_PKG_VERSION")); - config.server_id = SshId::Standard(version.into()); - let mut method = MethodSet::empty(); - method.push(MethodKind::PublicKey); - method.push(MethodKind::KeyboardInteractive); - config.methods = method; - config.inactivity_timeout = Some(Duration::from_secs(300)); - config.keepalive_interval = Some(Duration::from_secs(60)); - config.keepalive_max = 3; - - tracing::info!("SSH server configured with methods: {:?}", config.methods); - let token_service = SshTokenService::new(self.db.clone()); - let mut server = server::SSHServer::new( - self.db.clone(), - self.cache.clone(), - self.redis_pool.clone(), - token_service, - ); - - // Start the rate limiter cleanup background task so the HashMap - // doesn't grow unbounded over time. - let _cleanup = server.rate_limiter.clone().start_cleanup(); - - let ssh_port = self.app.ssh_port()?; - let bind_addr = format!("0.0.0.0:{}", ssh_port); - let public_host = self.app.ssh_domain()?; - let msg = if ssh_port == 22 { - format!( - "SSH server listening on port 22. Please use port {} for SSH connections.", - ssh_port - ) - } else { - format!( - "SSH server listening on port {} (public: {}). Please use port {} for SSH connections.", - ssh_port, public_host, ssh_port - ) - }; - tracing::info!("{}", msg); - server.run_on_address(Arc::new(config), bind_addr).await?; - Ok(()) - } -} - -/// Enqueues a sync task to the Redis-backed hook queue. -/// The background worker picks it up and processes it with per-repo locking. -#[derive(Clone)] -pub struct ReceiveSyncService { - pool: deadpool_redis::cluster::Pool, - redis_prefix: String, - /// Optional NATS publish function: (subject, payload) -> Result - nats_publish: Option< - Arc< - dyn Fn( - String, - Vec, - ) -> std::pin::Pin< - Box> + Send>, - > + Send - + Sync, - >, - >, -} - -impl ReceiveSyncService { - pub fn new(pool: deadpool_redis::cluster::Pool) -> Self { - Self { - pool, - redis_prefix: "{hook}".to_string(), - nats_publish: None, - } - } - - pub fn with_nats( - pool: deadpool_redis::cluster::Pool, - nats_publish: Arc< - dyn Fn( - String, - Vec, - ) -> std::pin::Pin< - Box> + Send>, - > + Send - + Sync, - >, - ) -> Self { - Self { - pool, - redis_prefix: "{hook}".to_string(), - nats_publish: Some(nats_publish), - } - } - - /// Enqueue a sync task. Fire-and-forget — logs errors but does not block. - pub async fn queue_position(&self, repo_uid: uuid::Uuid) -> Option<(usize, usize)> { - let queue_key = format!("{}:sync", self.redis_prefix); - let work_key = format!("{}:work", queue_key); - let redis = self.pool.get().await.ok()?; - let mut conn: deadpool_redis::cluster::Connection = redis; - let queue_items: Vec = conn.lrange(&queue_key, 0, -1).await.ok()?; - let work_items: Vec = conn.lrange(&work_key, 0, -1).await.unwrap_or_default(); - let repo_id = repo_uid.to_string(); - let queued_before = queue_items - .iter() - .rev() - .take_while(|item| { - serde_json::from_str::(item) - .map(|task| task.repo_id != repo_id) - .unwrap_or(true) - }) - .count(); - let total = work_items.len() + queue_items.len() + 1; - Some((work_items.len() + queued_before + 1, total)) - } - - fn push_queue_keys(repo_uid: uuid::Uuid) -> (String, String) { - let hash_tag = format!("{{push:{}}}", repo_uid); - ( - format!("git:{}:queue", hash_tag), - format!("git:{}:lock", hash_tag), - ) - } - - pub async fn join_push_queue( - &self, - repo_uid: uuid::Uuid, - request_id: &str, - ) -> redis::RedisResult<()> { - let (queue_key, _) = Self::push_queue_keys(repo_uid); - let redis = self.pool.get().await.map_err(|e| { - redis::RedisError::from(( - redis::ErrorKind::Io, - "failed to get Redis connection", - e.to_string(), - )) - })?; - let mut conn: deadpool_redis::cluster::Connection = redis; - redis::cmd("RPUSH") - .arg(&queue_key) - .arg(request_id) - .query_async::<()>(&mut conn) - .await - } - - pub async fn push_queue_position( - &self, - repo_uid: uuid::Uuid, - request_id: &str, - ) -> Option<(usize, usize)> { - let (queue_key, _) = Self::push_queue_keys(repo_uid); - let redis = self.pool.get().await.ok()?; - let mut conn: deadpool_redis::cluster::Connection = redis; - let queue_items: Vec = conn.lrange(&queue_key, 0, -1).await.ok()?; - let position = queue_items.iter().position(|item| item == request_id)? + 1; - Some((position, queue_items.len())) - } - - pub async fn try_acquire_push_lock( - &self, - repo_uid: uuid::Uuid, - request_id: &str, - ttl_secs: usize, - ) -> redis::RedisResult { - let (_, lock_key) = Self::push_queue_keys(repo_uid); - let redis = self.pool.get().await.map_err(|e| { - redis::RedisError::from(( - redis::ErrorKind::Io, - "failed to get Redis connection", - e.to_string(), - )) - })?; - let mut conn: deadpool_redis::cluster::Connection = redis; - let acquired: Option = redis::cmd("SET") - .arg(&lock_key) - .arg(request_id) - .arg("NX") - .arg("EX") - .arg(ttl_secs) - .query_async(&mut conn) - .await?; - Ok(acquired.is_some()) - } - - pub async fn release_push_queue(&self, repo_uid: uuid::Uuid, request_id: &str) { - let (queue_key, lock_key) = Self::push_queue_keys(repo_uid); - let redis = match self.pool.get().await { - Ok(c) => c, - Err(e) => { - tracing::warn!(error = %e, repo_id = %repo_uid, "push_queue_release_redis_connection_failed"); - return; - } - }; - let mut conn: deadpool_redis::cluster::Connection = redis; - let script = redis::Script::new( - r#" - redis.call("LREM", KEYS[1], 0, ARGV[1]) - if redis.call("GET", KEYS[2]) == ARGV[1] then - redis.call("DEL", KEYS[2]) - end - return 1 - "#, - ); - if let Err(e) = script - .key(&queue_key) - .key(&lock_key) - .arg(request_id) - .invoke_async::<()>(&mut conn) - .await - { - tracing::warn!(error = %e, repo_id = %repo_uid, "push_queue_release_failed"); - } - } - - pub async fn refresh_push_lock( - &self, - repo_uid: uuid::Uuid, - request_id: &str, - ttl_secs: usize, - ) -> redis::RedisResult { - let (_, lock_key) = Self::push_queue_keys(repo_uid); - let redis = self.pool.get().await.map_err(|e| { - redis::RedisError::from(( - redis::ErrorKind::Io, - "failed to get Redis connection", - e.to_string(), - )) - })?; - let mut conn: deadpool_redis::cluster::Connection = redis; - let refreshed: i32 = redis::Script::new( - r#" - if redis.call("GET", KEYS[1]) == ARGV[1] then - redis.call("EXPIRE", KEYS[1], ARGV[2]) - return 1 - end - return 0 - "#, - ) - .key(&lock_key) - .arg(request_id) - .arg(ttl_secs) - .invoke_async(&mut conn) - .await?; - Ok(refreshed == 1) - } - - pub async fn send(&self, task: RepoReceiveSyncTask) -> Option<(usize, usize)> { - let position = self.queue_position(task.repo_uid).await; - let hook_task = HookTask { - id: uuid::Uuid::new_v4().to_string(), - repo_id: task.repo_uid.to_string(), - task_type: TaskType::Sync, - payload: serde_json::Value::Null, - created_at: chrono::Utc::now(), - retry_count: 0, - }; - - // Try NATS first if available - if let Some(nats_publish) = &self.nats_publish { - let payload = match serde_json::to_vec(&hook_task) { - Ok(p) => p, - Err(e) => { - tracing::error!("failed to serialize hook task: {}", e); - return position; - } - }; - - match nats_publish("queue.hook.sync".to_string(), payload).await { - Ok(seq) => { - tracing::info!(repo_id = %task.repo_uid, seq = seq, "hook task queued to NATS"); - metrics::counter!("hook_task_queued_total", "backend" => "nats").increment(1); - return position; - } - Err(e) => { - tracing::warn!(error = %e, "NATS publish failed, falling back to Redis"); - } - } - } - - // Fallback to Redis List - let task_json = match serde_json::to_string(&hook_task) { - Ok(j) => j, - Err(e) => { - tracing::error!("failed to serialize hook task: {}", e); - return position; - } - }; - - let queue_key = format!("{}:sync", self.redis_prefix); - - let redis = match self.pool.get().await { - Ok(c) => c, - Err(e) => { - tracing::error!("failed to get Redis connection: {}", e); - return position; - } - }; - - let mut conn: deadpool_redis::cluster::Connection = redis; - if let Err(e) = redis::cmd("LPUSH") - .arg(&queue_key) - .arg(&task_json) - .query_async::<()>(&mut conn) - .await - { - tracing::error!( - "failed to enqueue sync task repo_id={} error={}", - task.repo_uid, - e - ); - } else { - tracing::info!(repo_id = %task.repo_uid, "hook task queued to Redis"); - metrics::counter!("hook_task_queued_total", "backend" => "redis").increment(1); - } - position - } -} - -#[derive(Clone)] -pub struct RepoReceiveSyncTask { - pub repo_uid: uuid::Uuid, -} - -/// SSH token authentication service. -/// Uses the same token hash algorithm as user access keys (Argon2id PHC string). -#[derive(Clone)] -pub struct SshTokenService { - db: AppDatabase, -} - -impl SshTokenService { - pub fn new(db: AppDatabase) -> Self { - Self { db } - } - - pub async fn find_user_by_token(&self, token: &str) -> Result, GitError> { - let token_models = user_token::Entity::find() - .filter(user_token::Column::IsRevoked.eq(false)) - .all(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))?; - - for token_model in token_models { - if token_model - .expires_at - .map(|expires_at| expires_at < chrono::Utc::now()) - .unwrap_or(false) - { - continue; - } - - let Ok(hash) = PasswordHash::new(&token_model.token_hash) else { - tracing::warn!(token_id = token_model.id, "invalid stored SSH token hash"); - continue; - }; - - if Argon2::default() - .verify_password(token.as_bytes(), &hash) - .is_err() - { - continue; - } - - let user_model = user::Entity::find() - .filter(user::Column::Uid.eq(token_model.user)) - .one(self.db.reader()) - .await - .map_err(|e| GitError::Internal(e.to_string()))?; - - return Ok(user_model); - } - - Ok(None) - } -} - -pub async fn run_ssh(config: AppConfig) -> anyhow::Result<()> { - tracing::info!("SSH server initializing"); - let db = AppDatabase::init(&config).await?; - let cache = AppCache::init(&config).await?; - let redis_pool = cache.redis_pool().clone(); - - let _hook = crate::hook::HookService::new( - db.clone(), - cache.clone(), - redis_pool.clone(), - config.clone(), - ); - - SSHHandle::new(db, config.clone(), cache, redis_pool) - .run_ssh() - .await?; - Ok(()) -} diff --git a/libs/git/ssh/push_queue.rs b/libs/git/ssh/push_queue.rs deleted file mode 100644 index 33c3fc8..0000000 --- a/libs/git/ssh/push_queue.rs +++ /dev/null @@ -1,186 +0,0 @@ -use crate::ssh::ReceiveSyncService; -use std::fmt; -use std::time::{Duration, Instant}; -use tokio::task::JoinHandle; -use tokio::time::sleep; - -pub const PUSH_QUEUE_TIMEOUT: Duration = Duration::from_secs(120); -pub const PUSH_LOCK_TTL_SECS: usize = 300; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct PushQueuePosition { - pub position: usize, - pub total: usize, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum PushQueueEvent { - Waiting(PushQueuePosition), - Acquired, -} - -#[derive(Debug)] -pub enum PushQueueWaitError { - Join(redis::RedisError), - Lock(redis::RedisError), - Timeout, -} - -impl fmt::Display for PushQueueWaitError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Join(e) => write!(f, "failed to join push queue: {e}"), - Self::Lock(e) => write!(f, "failed to acquire push queue lock: {e}"), - Self::Timeout => write!(f, "push queue timed out"), - } - } -} - -impl std::error::Error for PushQueueWaitError {} - -pub struct PushQueueLease { - service: ReceiveSyncService, - repo_uid: uuid::Uuid, - request_id: String, - heartbeat: Option>, - released: bool, -} - -impl PushQueueLease { - fn new(service: ReceiveSyncService, repo_uid: uuid::Uuid, request_id: String) -> Self { - let heartbeat = Some(start_lock_heartbeat( - service.clone(), - repo_uid, - request_id.clone(), - )); - Self { - service, - repo_uid, - request_id, - heartbeat, - released: false, - } - } - - pub fn request_id(&self) -> &str { - &self.request_id - } - - pub async fn release(&mut self) { - if self.released { - return; - } - self.service - .release_push_queue(self.repo_uid, &self.request_id) - .await; - if let Some(heartbeat) = self.heartbeat.take() { - heartbeat.abort(); - } - self.released = true; - } -} - -impl Drop for PushQueueLease { - fn drop(&mut self) { - if self.released { - return; - } - if let Some(heartbeat) = self.heartbeat.take() { - heartbeat.abort(); - } - let service = self.service.clone(); - let repo_uid = self.repo_uid; - let request_id = self.request_id.clone(); - tokio::spawn(async move { - service.release_push_queue(repo_uid, &request_id).await; - }); - } -} - -fn start_lock_heartbeat( - service: ReceiveSyncService, - repo_uid: uuid::Uuid, - request_id: String, -) -> JoinHandle<()> { - tokio::spawn(async move { - let interval = Duration::from_secs((PUSH_LOCK_TTL_SECS as u64 / 3).max(30)); - loop { - sleep(interval).await; - match service - .refresh_push_lock(repo_uid, &request_id, PUSH_LOCK_TTL_SECS) - .await - { - Ok(true) => {} - Ok(false) => { - tracing::warn!( - repo_id = %repo_uid, - request_id = %request_id, - "push_queue_lock_lost" - ); - break; - } - Err(e) => { - tracing::warn!( - error = %e, - repo_id = %repo_uid, - request_id = %request_id, - "push_queue_lock_refresh_failed" - ); - } - } - } - }) -} - -pub async fn wait_for_push_queue_slot( - service: ReceiveSyncService, - repo_uid: uuid::Uuid, - mut on_event: F, -) -> Result -where - F: FnMut(PushQueueEvent, &str), -{ - let request_id = uuid::Uuid::new_v4().to_string(); - service - .join_push_queue(repo_uid, &request_id) - .await - .map_err(PushQueueWaitError::Join)?; - - let deadline = Instant::now() + PUSH_QUEUE_TIMEOUT; - let mut last_position = None; - - loop { - let position = service.push_queue_position(repo_uid, &request_id).await; - if let Some((position, total)) = position { - let position = PushQueuePosition { position, total }; - if last_position != Some(position) && position.position > 1 { - on_event(PushQueueEvent::Waiting(position), &request_id); - } - last_position = Some(position); - - if position.position == 1 { - match service - .try_acquire_push_lock(repo_uid, &request_id, PUSH_LOCK_TTL_SECS) - .await - { - Ok(true) => { - on_event(PushQueueEvent::Acquired, &request_id); - return Ok(PushQueueLease::new(service, repo_uid, request_id)); - } - Ok(false) => {} - Err(e) => { - service.release_push_queue(repo_uid, &request_id).await; - return Err(PushQueueWaitError::Lock(e)); - } - } - } - } - - if Instant::now() >= deadline { - service.release_push_queue(repo_uid, &request_id).await; - return Err(PushQueueWaitError::Timeout); - } - - sleep(Duration::from_secs(1)).await; - } -} diff --git a/libs/git/ssh/rate_limit.rs b/libs/git/ssh/rate_limit.rs deleted file mode 100644 index ea32f05..0000000 --- a/libs/git/ssh/rate_limit.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::RwLock; -use tokio::time::interval; - -#[derive(Debug, Clone)] -pub struct RateLimitConfig { - pub requests_per_window: u32, - pub window_duration: Duration, -} - -impl Default for RateLimitConfig { - fn default() -> Self { - Self { - requests_per_window: 100, - window_duration: Duration::from_secs(60), - } - } -} - -#[derive(Debug)] -struct RateLimitState { - count: u32, - reset_time: Instant, -} - -#[derive(Debug, Clone)] -pub struct RateLimiter { - limits: Arc>>, - config: RateLimitConfig, -} - -impl RateLimiter { - pub fn new(config: RateLimitConfig) -> Self { - Self { - limits: Arc::new(RwLock::new(HashMap::new())), - config, - } - } - - pub async fn is_allowed(&self, key: &str) -> bool { - let now = Instant::now(); - let mut limits = self.limits.write().await; - - let state = limits - .entry(key.to_string()) - .or_insert_with(|| RateLimitState { - count: 0, - reset_time: now + self.config.window_duration, - }); - - if now >= state.reset_time { - state.count = 0; - state.reset_time = now + self.config.window_duration; - } - - if state.count >= self.config.requests_per_window { - return false; - } - - state.count += 1; - true - } - - pub async fn remaining_requests(&self, key: &str) -> u32 { - let now = Instant::now(); - let limits = self.limits.read().await; - - if let Some(state) = limits.get(key) { - if now >= state.reset_time { - self.config.requests_per_window - } else { - self.config.requests_per_window.saturating_sub(state.count) - } - } else { - self.config.requests_per_window - } - } - - pub async fn reset_time(&self, key: &str) -> Duration { - let now = Instant::now(); - let limits = self.limits.read().await; - - if let Some(state) = limits.get(key) { - if now >= state.reset_time { - Duration::from_secs(0) - } else { - state.reset_time.duration_since(now) - } - } else { - Duration::from_secs(0) - } - } - - /// Start a background cleanup task that removes expired entries every 5 minutes. - /// This prevents unbounded HashMap growth. - pub fn start_cleanup(self: Arc) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - let mut ticker = interval(Duration::from_secs(300)); // every 5 minutes - loop { - ticker.tick().await; - let now = Instant::now(); - let mut limits = self.limits.write().await; - limits.retain(|_, state| now < state.reset_time); - } - }) - } -} - -pub struct SshRateLimiter { - limiter: RateLimiter, -} - -impl SshRateLimiter { - pub fn new() -> Self { - Self { - limiter: RateLimiter::new(RateLimitConfig::default()), - } - } - - pub async fn is_user_allowed(&self, user_id: &str) -> bool { - self.limiter.is_allowed(&format!("user:{}", user_id)).await - } - - pub async fn is_ip_allowed(&self, ip_address: &str) -> bool { - self.limiter.is_allowed(&format!("ip:{}", ip_address)).await - } - - pub async fn is_repo_access_allowed(&self, user_id: &str, repo_path: &str) -> bool { - self.limiter - .is_allowed(&format!("repo_access:{}:{}", user_id, repo_path)) - .await - } - - /// Start a background cleanup task that removes expired entries every 5 minutes. - /// Prevents unbounded HashMap growth in the underlying RateLimiter. - pub fn start_cleanup(self: Arc) -> tokio::task::JoinHandle<()> { - RateLimiter::start_cleanup(Arc::new(self.limiter.clone())) - } -} diff --git a/libs/git/ssh/ref_update.rs b/libs/git/ssh/ref_update.rs deleted file mode 100644 index f8a1466..0000000 --- a/libs/git/ssh/ref_update.rs +++ /dev/null @@ -1,113 +0,0 @@ -#[derive(Clone, Debug)] -pub struct RefUpdate { - pub name: String, - pub old_oid: String, - pub new_oid: String, -} - -impl RefUpdate { - /// Parse git receive-pack reference update commands from pkt-line data. - /// Payload format: " \0capabilities\n". - pub fn parse_ref_updates(data: &[u8]) -> Result, String> { - let mut refs = Vec::new(); - - for payload in parse_pkt_line_payloads(data)? { - let line = String::from_utf8_lossy(payload); - let line = line.trim_end_matches(['\r', '\n']); - if line.is_empty() { - continue; - } - - let mut parts = line.splitn(3, ' '); - let old_oid = parts.next().unwrap_or_default(); - let new_oid = parts.next().unwrap_or_default(); - let raw_name = parts.next().unwrap_or_default(); - let name = raw_name - .split_once('\0') - .map(|(name, _)| name) - .unwrap_or(raw_name) - .trim(); - - if old_oid.len() != 40 || new_oid.len() != 40 || name.is_empty() { - continue; - } - - refs.push(RefUpdate { - old_oid: old_oid.to_string(), - new_oid: new_oid.to_string(), - name: name.to_string(), - }); - } - - Ok(refs) - } -} - -fn parse_pkt_line_payloads(data: &[u8]) -> Result, String> { - let mut payloads = Vec::new(); - let mut offset = 0; - - while offset + 4 <= data.len() { - let header = std::str::from_utf8(&data[offset..offset + 4]) - .map_err(|_| "invalid pkt-line header encoding".to_string())?; - let len = usize::from_str_radix(header, 16) - .map_err(|_| format!("invalid pkt-line length: {header}"))?; - offset += 4; - - match len { - 0 => break, - 1..=3 => return Err(format!("invalid pkt-line length: {len}")), - _ => { - let payload_len = len - 4; - if offset + payload_len > data.len() { - return Err("truncated pkt-line payload".to_string()); - } - payloads.push(&data[offset..offset + payload_len]); - offset += payload_len; - } - } - } - - Ok(payloads) -} - -#[cfg(test)] -mod tests { - use super::RefUpdate; - - fn pkt(payload: &str) -> Vec { - let len = payload.len() + 4; - let mut out = format!("{len:04x}").into_bytes(); - out.extend_from_slice(payload.as_bytes()); - out - } - - #[test] - fn parses_receive_pack_ref_with_capabilities() { - let mut data = pkt( - "0000000000000000000000000000000000000000 1111111111111111111111111111111111111111 refs/heads/feature\0 report-status\n", - ); - data.extend_from_slice(b"0000"); - - let refs = RefUpdate::parse_ref_updates(&data).unwrap(); - - assert_eq!(refs.len(), 1); - assert_eq!(refs[0].old_oid, "0000000000000000000000000000000000000000"); - assert_eq!(refs[0].new_oid, "1111111111111111111111111111111111111111"); - assert_eq!(refs[0].name, "refs/heads/feature"); - } - - #[test] - fn parses_receive_pack_ref_without_pack_payload() { - let mut data = pkt( - "2222222222222222222222222222222222222222 0000000000000000000000000000000000000000 refs/heads/old\n", - ); - data.extend_from_slice(b"0000"); - - let refs = RefUpdate::parse_ref_updates(&data).unwrap(); - - assert_eq!(refs.len(), 1); - assert_eq!(refs[0].name, "refs/heads/old"); - assert_eq!(refs[0].new_oid, "0000000000000000000000000000000000000000"); - } -} diff --git a/libs/git/ssh/server.rs b/libs/git/ssh/server.rs deleted file mode 100644 index 552a23f..0000000 --- a/libs/git/ssh/server.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::ssh::ReceiveSyncService; -use crate::ssh::SshTokenService; -use crate::ssh::handle::SSHandle; -use crate::ssh::rate_limit::SshRateLimiter; -use db::cache::AppCache; -use db::database::AppDatabase; -use deadpool_redis::cluster::Pool as RedisPool; -use russh::server::Handler; -use std::io; -use std::net::SocketAddr; -use std::sync::Arc; - -pub struct SSHServer { - pub db: AppDatabase, - pub cache: AppCache, - pub redis_pool: RedisPool, - pub token_service: SshTokenService, - pub rate_limiter: Arc, -} - -impl SSHServer { - pub fn new( - db: AppDatabase, - cache: AppCache, - redis_pool: RedisPool, - token_service: SshTokenService, - ) -> Self { - SSHServer { - db, - cache, - redis_pool, - token_service, - rate_limiter: Arc::new(SshRateLimiter::new()), - } - } -} -impl russh::server::Server for SSHServer { - type Handler = SSHandle; - - fn new_client(&mut self, addr: Option) -> Self::Handler { - if let Some(addr) = addr { - let ip = addr.ip().to_string(); - tracing::info!("New SSH connection ip={} port={}", ip, addr.port()); - // Check IP rate limit before accepting the connection. - let limiter = self.rate_limiter.clone(); - let ip_clone = ip.clone(); - tokio::spawn(async move { - if !limiter.is_ip_allowed(&ip_clone).await { - tracing::warn!(ip = %ip_clone, "SSH connection rate limited"); - } - }); - } else { - tracing::info!("New SSH connection from unknown address"); - } - let sync_service = ReceiveSyncService::new(self.redis_pool.clone()); - SSHandle::new( - self.db.clone(), - self.cache.clone(), - sync_service, - self.token_service.clone(), - addr, - ) - } - - fn handle_session_error(&mut self, error: ::Error) { - match error { - russh::Error::Disconnect => { - tracing::info!("Connection disconnected by peer"); - } - russh::Error::Inconsistent => { - tracing::warn!("Protocol inconsistency detected"); - } - russh::Error::NotAuthenticated => { - tracing::warn!("Authentication failed"); - } - russh::Error::IO(ref io_err) => { - tracing::warn!( - "SSH IO error kind={:?} message={} raw_os_error={:?}", - io_err.kind(), - io_err, - io_err.raw_os_error() - ); - - if io_err.kind() == io::ErrorKind::UnexpectedEof { - tracing::warn!( - "SSH peer closed the connection before a clean disconnect was received" - ); - } - } - _ => { - tracing::warn!("SSH session error error={}", error); - } - } - } -} diff --git a/libs/git/tags/mod.rs b/libs/git/tags/mod.rs deleted file mode 100644 index 834ccf4..0000000 --- a/libs/git/tags/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Tag domain — all tag-related operations on a GitDomain. -pub mod ops; -pub mod query; -pub mod types; diff --git a/libs/git/tags/ops.rs b/libs/git/tags/ops.rs deleted file mode 100644 index 0f12ab3..0000000 --- a/libs/git/tags/ops.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Tag create/delete/rename operations. - -use crate::commit::types::{CommitOid, CommitSignature}; -use crate::ref_utils::validate_ref_name; -use crate::tags::types::TagInfo; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn tag_create( - &self, - name: &str, - target: &CommitOid, - message: &str, - tagger: &CommitSignature, - force: bool, - ) -> GitResult { - validate_ref_name(name)?; - - let target_oid = target - .to_oid() - .map_err(|_| GitError::InvalidOid(target.to_string()))?; - - let obj = self - .repo() - .find_object(target_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let sig = self.commit_signature_to_git2(tagger)?; - - let tag_oid = self - .repo() - .tag(name, &obj, &sig, message, force) - .map_err(|e| { - if e.code() == git2::ErrorCode::Exists { - GitError::TagExists(name.to_string()) - } else { - GitError::Internal(e.to_string()) - } - })?; - - let ref_name = format!("refs/tags/{}", name); - self.repo - .reference(&ref_name, tag_oid, true, "create tag") - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(TagInfo { - name: name.to_string(), - oid: CommitOid::from_git2(tag_oid), - target: target.clone(), - is_annotated: true, - message: Some(message.to_string()), - tagger: Some(tagger.name.clone()), - tagger_email: Some(tagger.email.clone()), - }) - } - - pub fn tag_create_lightweight( - &self, - name: &str, - target: &CommitOid, - force: bool, - ) -> GitResult { - validate_ref_name(name)?; - - let target_oid = target - .to_oid() - .map_err(|_| GitError::InvalidOid(target.to_string()))?; - - let obj = self - .repo() - .find_object(target_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let tag_oid = self - .repo() - .tag_lightweight(name, &obj, force) - .map_err(|e| { - if e.code() == git2::ErrorCode::Exists { - GitError::TagExists(name.to_string()) - } else { - GitError::Internal(e.to_string()) - } - })?; - - Ok(TagInfo { - name: name.to_string(), - oid: CommitOid::from_git2(tag_oid), - target: target.clone(), - is_annotated: false, - message: None, - tagger: None, - tagger_email: None, - }) - } - - pub fn tag_delete(&self, name: &str) -> GitResult<()> { - let full_name = if name.starts_with("refs/tags/") { - name.to_string() - } else { - format!("refs/tags/{}", name) - }; - - let mut reference = self - .repo() - .find_reference(&full_name) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - reference - .delete() - .map_err(|e| GitError::Internal(e.to_string())) - } - - pub fn tag_rename(&self, old_name: &str, new_name: &str) -> GitResult { - validate_ref_name(new_name)?; - - let old_ref = if old_name.starts_with("refs/tags/") { - old_name.to_string() - } else { - format!("refs/tags/{}", old_name) - }; - - let new_ref = format!("refs/tags/{}", new_name); - - let info = self.tag_get(old_name)?; - - let mut reference = self - .repo() - .find_reference(&old_ref) - .map_err(|_e| GitError::RefNotFound(old_name.to_string()))?; - - let target_oid = reference - .target() - .ok_or_else(|| GitError::Internal("tag has no target".to_string()))?; - - reference - .delete() - .map_err(|e| GitError::Internal(e.to_string()))?; - - self.repo - .reference(&new_ref, target_oid, true, "rename tag") - .map_err(|e| GitError::Internal(e.to_string()))?; - - Ok(TagInfo { - name: new_name.to_string(), - oid: info.oid, - target: info.target, - is_annotated: info.is_annotated, - message: info.message, - tagger: info.tagger, - tagger_email: info.tagger_email, - }) - } - - pub fn tag_update_message( - &self, - name: &str, - message: &str, - tagger: &CommitSignature, - ) -> GitResult { - let full_name = if name.starts_with("refs/tags/") { - name.to_string() - } else { - format!("refs/tags/{}", name) - }; - - let reference = self - .repo() - .find_reference(&full_name) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - let tag_oid = reference - .target() - .ok_or_else(|| GitError::Internal("tag reference has no target".to_string()))?; - - let tag_obj = self - .repo() - .find_object(tag_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - if tag_obj.kind() != Some(git2::ObjectType::Tag) { - return Err(GitError::Internal( - "cannot update message of a lightweight tag".to_string(), - )); - } - - let commit_obj = tag_obj - .as_tag() - .and_then(|t| t.peel().ok()) - .ok_or_else(|| GitError::Internal("cannot peel tag to commit".to_string()))?; - - let sig = self.commit_signature_to_git2(tagger)?; - - let new_tag_oid = self - .repo() - .tag(name, &commit_obj, &sig, message, true) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let temp_name = format!("{}/update-tmp-{}", full_name, std::process::id()); - self.repo - .reference(&temp_name, new_tag_oid, true, "update tag message (temp)") - .map_err(|e| GitError::Internal(e.to_string()))?; - - self.repo - .reference(&full_name, new_tag_oid, true, "update tag message") - .map_err(|e| GitError::Internal(e.to_string()))?; - - if let Ok(mut temp_ref) = self.repo().find_reference(&temp_name) { - if let Err(e) = temp_ref.delete() { - // Log but do not fail — a leftover temporary reference is non-critical - // but indicates something went wrong during tag update. - tracing::warn!(temp_name = %temp_name, error = %e, "failed to delete temporary tag reference"); - } - } - - Ok(TagInfo { - name: name.to_string(), - oid: CommitOid::from_git2(new_tag_oid), - target: CommitOid::from_git2(commit_obj.id()), - is_annotated: true, - message: Some(message.to_string()), - tagger: Some(tagger.name.clone()), - tagger_email: Some(tagger.email.clone()), - }) - } -} diff --git a/libs/git/tags/query.rs b/libs/git/tags/query.rs deleted file mode 100644 index 22e2db4..0000000 --- a/libs/git/tags/query.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Tag querying operations. - -use crate::commit::types::CommitOid; -use crate::tags::types::{TagInfo, TagSummary}; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - pub fn tag_list(&self) -> GitResult> { - let tag_names = self - .repo() - .tag_names(None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let mut tags = Vec::with_capacity(16); - let mut errors: Vec<(String, GitError)> = Vec::new(); - let count = tag_names.len(); - for i in 0..count { - if let Some(name) = tag_names.get(i) { - match self.tag_get(name) { - Ok(info) => tags.push(info), - Err(e) => errors.push((name.to_string(), e)), - } - } - } - if !errors.is_empty() { - return Err(GitError::Internal(format!( - "failed to get {} tag(s): {}", - errors.len(), - errors - .into_iter() - .map(|(n, e)| format!("{}: {}", n, e)) - .collect::>() - .join("; ") - ))); - } - Ok(tags) - } - - pub fn tag_list_names(&self) -> GitResult> { - let names = self - .repo() - .tag_names(None) - .map_err(|e| GitError::Internal(e.to_string()))?; - let count = names.len(); - let mut result = Vec::with_capacity(count); - for i in 0..count { - if let Some(name) = names.get(i) { - result.push(name.to_string()); - } - } - Ok(result) - } - - pub fn tag_count(&self) -> GitResult { - let names = self.tag_list_names()?; - Ok(names.len()) - } - - pub fn tag_summary(&self) -> GitResult { - let count = self.tag_count()?; - Ok(TagSummary { total_count: count }) - } - - pub fn tag_get(&self, name: &str) -> GitResult { - let full_name = if name.starts_with("refs/tags/") { - name.to_string() - } else { - format!("refs/tags/{}", name) - }; - - let reference = self - .repo() - .find_reference(&full_name) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - let target_oid = reference - .target() - .ok_or_else(|| GitError::Internal("tag reference has no target".to_string()))?; - - let target = CommitOid::from_git2(target_oid); - - let obj = self - .repo() - .find_object(target_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - if obj.kind() == Some(git2::ObjectType::Tag) { - let tag = self - .repo() - .find_tag(target_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let tagger = tag.tagger().map(|s| { - ( - s.name().unwrap_or("").to_string(), - s.email().unwrap_or("").to_string(), - ) - }); - - Ok(TagInfo { - name: name.to_string(), - oid: CommitOid::from_git2(target_oid), - target, - is_annotated: true, - message: tag.message().map(String::from), - tagger: tagger.as_ref().map(|(n, _)| n.clone()), - tagger_email: tagger.as_ref().map(|(_, e)| e.clone()), - }) - } else { - Ok(TagInfo { - name: name.to_string(), - oid: CommitOid::from_git2(target_oid), - target, - is_annotated: false, - message: None, - tagger: None, - tagger_email: None, - }) - } - } - - pub fn tag_exists(&self, name: &str) -> bool { - let full_name = if name.starts_with("refs/tags/") { - name.to_string() - } else { - format!("refs/tags/{}", name) - }; - - self.repo.find_reference(&full_name).is_ok() - } - - pub fn tag_target(&self, name: &str) -> GitResult> { - let info = self.tag_get(name)?; - Ok(Some(info.target)) - } - - pub fn tag_is_annotated(&self, name: &str) -> GitResult { - let (_, is_tag) = self.tag_reference_info(name)?; - Ok(is_tag) - } - - pub fn tag_message(&self, name: &str) -> GitResult> { - if let Some(tag) = self.find_annotated_tag(name)? { - Ok(tag.message().map(String::from)) - } else { - Ok(None) - } - } - - pub fn tag_tagger(&self, name: &str) -> GitResult> { - if let Some(tag) = self.find_annotated_tag(name)? { - Ok(tag.tagger().map(|s| { - ( - s.name().unwrap_or("").to_string(), - s.email().unwrap_or("").to_string(), - ) - })) - } else { - Ok(None) - } - } - - /// Look up a tag's reference OID and whether it is an annotated tag. - fn tag_reference_info(&self, name: &str) -> GitResult<(git2::Oid, bool)> { - let full_name = if name.starts_with("refs/tags/") { - name.to_string() - } else { - format!("refs/tags/{}", name) - }; - - let reference = self - .repo() - .find_reference(&full_name) - .map_err(|_e| GitError::RefNotFound(name.to_string()))?; - - let target_oid = reference - .target() - .ok_or_else(|| GitError::Internal("tag reference has no target".to_string()))?; - - let obj = self - .repo() - .find_object(target_oid, None) - .map_err(|e| GitError::Internal(e.to_string()))?; - - let is_tag = obj.kind() == Some(git2::ObjectType::Tag); - Ok((target_oid, is_tag)) - } - - /// Find the git2 Tag object for an annotated tag, or None if it is a lightweight tag. - fn find_annotated_tag(&self, name: &str) -> GitResult>> { - let (target_oid, is_tag) = self.tag_reference_info(name)?; - if !is_tag { - return Ok(None); - } - let tag = self - .repo() - .find_tag(target_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(Some(tag)) - } -} diff --git a/libs/git/tags/types.rs b/libs/git/tags/types.rs deleted file mode 100644 index 4950e6c..0000000 --- a/libs/git/tags/types.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Serializable types for the tag domain. - -use serde::{Deserialize, Serialize}; - -use crate::commit::types::CommitOid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TagInfo { - pub name: String, - pub oid: CommitOid, - pub target: CommitOid, - pub is_annotated: bool, - pub message: Option, - pub tagger: Option, - pub tagger_email: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TagSummary { - pub total_count: usize, -} diff --git a/libs/git/tree/mod.rs b/libs/git/tree/mod.rs deleted file mode 100644 index c919433..0000000 --- a/libs/git/tree/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Tree domain — all tree-related operations on a GitDomain. -pub mod query; -pub mod types; diff --git a/libs/git/tree/query.rs b/libs/git/tree/query.rs deleted file mode 100644 index d5af326..0000000 --- a/libs/git/tree/query.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! Tree query operations. - -use std::path::Path; - -use crate::commit::types::CommitOid; -use crate::tree::types::{TreeEntry, TreeInfo}; -use crate::{GitDomain, GitError, GitResult}; - -impl GitDomain { - fn resolve_tree(&self, oid: &CommitOid) -> GitResult> { - let oid = oid - .to_oid() - .map_err(|_| GitError::InvalidOid(oid.to_string()))?; - let obj = self - .repo() - .find_object(oid, None) - .map_err(|_| GitError::ObjectNotFound(oid.to_string()))?; - match obj.kind() { - Some(git2::ObjectType::Commit) => { - let commit = obj.as_commit().ok_or_else(|| { - GitError::Internal("object type mismatch: expected commit".into()) - })?; - self.repo() - .find_tree(commit.tree_id()) - .map_err(|e| GitError::Internal(e.to_string())) - } - Some(git2::ObjectType::Tree) => obj - .peel_to_tree() - .map_err(|e| GitError::Internal(e.to_string())), - _ => Err(GitError::InvalidOid(oid.to_string())), - } - } - - pub fn tree_get(&self, oid: &CommitOid) -> GitResult { - let tree = self.resolve_tree(oid)?; - Ok(TreeInfo::from_git2(&tree)) - } - - pub fn tree_exists(&self, oid: &CommitOid) -> bool { - self.resolve_tree(oid).is_ok() - } - - pub fn tree_entry(&self, oid: &CommitOid, index: usize) -> GitResult { - let tree = self.resolve_tree(oid)?; - let entry = tree - .get(index) - .ok_or_else(|| GitError::Internal("tree entry not found".to_string()))?; - Ok(TreeEntry::from_git2(entry, self.repo())) - } - - pub fn tree_list(&self, oid: &CommitOid) -> GitResult> { - let tree = self.resolve_tree(oid)?; - let repo = self.repo(); - let entries: Vec = tree - .iter() - .map(|entry| TreeEntry::from_git2(entry, repo)) - .collect(); - Ok(entries) - } - - pub fn tree_entry_count(&self, oid: &CommitOid) -> GitResult { - let info = self.tree_get(oid)?; - Ok(info.entry_count) - } - - pub fn tree_entry_by_path(&self, tree_oid: &CommitOid, path: &str) -> GitResult { - let tree = self.resolve_tree(tree_oid)?; - let entry = tree - .get_path(Path::new(path)) - .map_err(|e| GitError::Internal(format!("path '{}': {}", path, e)))?; - Ok(TreeEntry::from_git2(entry, self.repo())) - } - - pub fn tree_entry_by_path_from_commit( - &self, - commit_oid: &CommitOid, - path: &str, - ) -> GitResult { - let oid = commit_oid - .to_oid() - .map_err(|_| GitError::InvalidOid(commit_oid.to_string()))?; - let commit = self - .repo() - .find_commit(oid) - .map_err(|_| GitError::ObjectNotFound(commit_oid.to_string()))?; - let tree = self - .repo() - .find_tree(commit.tree_id()) - .map_err(|e| GitError::Internal(e.to_string()))?; - let entry = tree - .get_path(Path::new(path)) - .map_err(|e| GitError::Internal(format!("path '{}': {}", path, e)))?; - Ok(TreeEntry::from_git2(entry, self.repo())) - } - - pub fn tree_is_empty(&self, oid: &CommitOid) -> GitResult { - let info = self.tree_get(oid)?; - Ok(info.is_empty) - } - - pub fn tree_diffstats( - &self, - old_tree: &CommitOid, - new_tree: &CommitOid, - ) -> GitResult { - use crate::diff::types::DiffStats; - let old_oid = old_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(old_tree.to_string()))?; - let new_oid = new_tree - .to_oid() - .map_err(|_| GitError::InvalidOid(new_tree.to_string()))?; - let old_tree = self - .repo() - .find_tree(old_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let new_tree = self - .repo() - .find_tree(new_oid) - .map_err(|e| GitError::Internal(e.to_string()))?; - let diff = self - .repo() - .diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None) - .map_err(|e| GitError::Internal(e.to_string()))?; - let stats = diff - .stats() - .map_err(|e| GitError::Internal(e.to_string()))?; - Ok(DiffStats::from_git2(&stats)) - } -} diff --git a/libs/git/tree/types.rs b/libs/git/tree/types.rs deleted file mode 100644 index acd376d..0000000 --- a/libs/git/tree/types.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Serializable types for the tree domain. - -use serde::{Deserialize, Serialize}; - -use crate::commit::types::CommitOid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TreeInfo { - pub oid: CommitOid, - pub entry_count: usize, - pub is_empty: bool, -} - -impl TreeInfo { - pub fn from_git2(tree: &git2::Tree<'_>) -> Self { - Self { - oid: CommitOid::from_git2(tree.id()), - entry_count: tree.len(), - is_empty: tree.is_empty(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TreeEntry { - pub name: String, - pub oid: CommitOid, - pub kind: String, - pub filemode: u32, - pub is_binary: bool, -} - -impl TreeEntry { - pub fn from_git2(entry: git2::TreeEntry<'_>, repo: &git2::Repository) -> Self { - let kind = entry - .kind() - .map(|k| format!("{:?}", k).to_lowercase()) - .unwrap_or_default(); - // Binary detection: check actual blob content, not just object type. - // Blob type means "file content" in git, not "binary file". - let is_binary = entry - .to_object(repo) - .ok() - .and_then(|o| o.as_blob().map(|blob| blob.is_binary())) - .unwrap_or(false); - Self { - name: entry.name().unwrap_or("").to_string(), - oid: CommitOid::from_git2(entry.id()), - kind, - filemode: entry.filemode() as u32, - is_binary, - } - } -} diff --git a/libs/migrate/Cargo.toml b/libs/migrate/Cargo.toml deleted file mode 100644 index 4da33e1..0000000 --- a/libs/migrate/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "migrate" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[lib] -path = "lib.rs" -name = "migrate" - -[dependencies] -sea-orm-migration = { workspace = true } -sea-orm = { workspace = true } -sea-query = { workspace = true } -models = { workspace = true } -async-trait = { workspace = true } - -[lints] -workspace = true diff --git a/libs/migrate/bootstrap.rs b/libs/migrate/bootstrap.rs deleted file mode 100644 index c83a0ec..0000000 --- a/libs/migrate/bootstrap.rs +++ /dev/null @@ -1,28 +0,0 @@ -use sea_orm_migration::prelude::*; - -pub struct Migration; - -impl MigrationName for Migration { - fn name(&self) -> &str { - "bootstrap" - } -} - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .get_connection() - .execute_unprepared(include_str!("sql/bootstrap/bootstrap_up_01.sql")) - .await?; - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .get_connection() - .execute_unprepared(include_str!("sql/bootstrap/bootstrap_down_01.sql")) - .await?; - Ok(()) - } -} diff --git a/libs/migrate/lib.rs b/libs/migrate/lib.rs deleted file mode 100644 index 98e5343..0000000 --- a/libs/migrate/lib.rs +++ /dev/null @@ -1,301 +0,0 @@ -pub use sea_orm_migration::prelude::*; - -pub async fn execute_sql(manager: &SchemaManager<'_>, sql: &str) -> Result<(), DbErr> { - for stmt in split_sql_statements(sql) { - if stmt.is_empty() { - continue; - } - manager - .get_connection() - .execute_raw(sea_orm::Statement::from_string( - sea_orm::DbBackend::Postgres, - stmt, - )) - .await?; - } - Ok(()) -} - -fn split_sql_statements(sql: &str) -> Vec<&str> { - sql.split(';') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_split_simple() { - let sql = "SELECT 1; SELECT 2; SELECT 3"; - let stmts = split_sql_statements(sql); - assert_eq!(stmts, &["SELECT 1", "SELECT 2", "SELECT 3"]); - } -} - -macro_rules! define_sql_migrations { - ($( $module:ident => { name: $name:literal, up: $up:literal, down: $down:literal } ),+ $(,)?) => { - $( - pub mod $module { - use sea_orm_migration::prelude::*; - - pub struct Migration; - - impl MigrationName for Migration { - fn name(&self) -> &str { - $name - } - } - - #[async_trait::async_trait] - impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - let sql = include_str!($up); - super::execute_sql(manager, sql).await?; - Ok(()) - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - let sql = include_str!($down); - super::execute_sql(manager, sql).await?; - Ok(()) - } - } - } - )+ - - pub struct Migrator; - - #[async_trait::async_trait] - impl MigratorTrait for Migrator { - fn migrations() -> Vec> { - vec![ - Box::new(user::Migration), - Box::new(user_password::Migration), - Box::new(user_email::Migration), - Box::new(user_2fa::Migration), - Box::new(user_notification::Migration), - Box::new(user_preferences::Migration), - Box::new(user_password_reset::Migration), - Box::new(user_relation::Migration), - Box::new(user_ssh_key::Migration), - Box::new(user_token::Migration), - Box::new(user_activity_log::Migration), - Box::new(project_access_log::Migration), - Box::new(project_audit_log::Migration), - Box::new(project_billing::Migration), - Box::new(project_billing_history::Migration), - Box::new(project_follow::Migration), - Box::new(project_history_name::Migration), - Box::new(project_label::Migration), - Box::new(project_like::Migration), - Box::new(project_member_invitations::Migration), - Box::new(project_member_join_answers::Migration), - Box::new(project_member_join_request::Migration), - Box::new(project_member_join_settings::Migration), - Box::new(project_members::Migration), - Box::new(project_watch::Migration), - Box::new(repo::Migration), - Box::new(repo_branch::Migration), - Box::new(repo_branch_protect::Migration), - Box::new(repo_collaborator::Migration), - Box::new(repo_commit::Migration), - Box::new(repo_fork::Migration), - Box::new(repo_history_name::Migration), - Box::new(repo_hook::Migration), - Box::new(repo_lfs_lock::Migration), - Box::new(repo_lfs_object::Migration), - Box::new(repo_lock::Migration), - Box::new(repo_star::Migration), - Box::new(repo_tag::Migration), - Box::new(repo_upstream::Migration), - Box::new(repo_watch::Migration), - Box::new(repo_webhook::Migration), - Box::new(issue::Migration), - Box::new(issue_assignee::Migration), - Box::new(issue_comment::Migration), - Box::new(issue_comment_reaction::Migration), - Box::new(issue_label::Migration), - Box::new(issue_pull_request::Migration), - Box::new(issue_reaction::Migration), - Box::new(issue_repo::Migration), - Box::new(issue_subscriber::Migration), - Box::new(pull_request::Migration), - Box::new(pull_request_commit::Migration), - Box::new(pull_request_review::Migration), - Box::new(pull_request_review_comment::Migration), - Box::new(room_category::Migration), - Box::new(room::Migration), - Box::new(room_ai::Migration), - Box::new(room_message::Migration), - Box::new(room_pin::Migration), - Box::new(room_thread::Migration), - Box::new(ai_model_provider::Migration), - Box::new(ai_model::Migration), - Box::new(ai_model_version::Migration), - Box::new(ai_model_capability::Migration), - Box::new(ai_model_parameter_profile::Migration), - Box::new(ai_model_pricing::Migration), - Box::new(ai_session::Migration), - Box::new(ai_tool_call::Migration), - Box::new(ai_tool_auth::Migration), - Box::new(label::Migration), - Box::new(notify::Migration), - Box::new(room_notifications::Migration), - Box::new(user_email_change::Migration), - Box::new(project_activity::Migration), - Box::new(room_message_reaction::Migration), - Box::new(room_message_edit_history::Migration), - Box::new(project_board::Migration), - Box::new(project_board_column::Migration), - Box::new(project_board_card::Migration), - Box::new(pull_request_review_request::Migration), - Box::new(workspace::Migration), - Box::new(project::Migration), - Box::new(workspace_membership::Migration), - Box::new(workspace_billing::Migration), - Box::new(workspace_billing_history::Migration), - Box::new(project_skill::Migration), - Box::new(agent_task::Migration), - Box::new(admin_user::Migration), - Box::new(admin_role::Migration), - Box::new(admin_permission::Migration), - Box::new(admin_user_role::Migration), - Box::new(admin_role_permission::Migration), - Box::new(admin_audit_log::Migration), - Box::new(admin_api_token::Migration), - Box::new(workspace_alert_config::Migration), - Box::new(room_attachment::Migration), - Box::new(room_access::Migration), - Box::new(room_user_state::Migration), - Box::new(project_role_priority::Migration), - Box::new(ai_conversation::Migration), - Box::new(ai_message::Migration), - Box::new(ai_message_fork::Migration), - Box::new(ai_shared_conversation::Migration), - Box::new(ai_token_usage::Migration), - Box::new(room_compact_summary::Migration), - Box::new(user_billing_history::Migration), - Box::new(project_message_favorite::Migration), - Box::new(ai_subagent_session::Migration), - Box::new(bootstrap::Migration), - ] - } - } - }; -} - -define_sql_migrations! { - user => { name: "user", up: "sql/user/user_up_01.sql", down: "sql/user/user_down_01.sql" }, - user_password => { name: "user_password", up: "sql/user_password/user_password_up_01.sql", down: "sql/user_password/user_password_down_01.sql" }, - user_email => { name: "user_email", up: "sql/user_email/user_email_up_01.sql", down: "sql/user_email/user_email_down_01.sql" }, - user_2fa => { name: "user_2fa", up: "sql/user_2fa/user_2fa_up_01.sql", down: "sql/user_2fa/user_2fa_down_01.sql" }, - user_notification => { name: "user_notification", up: "sql/user_notification/user_notification_up_01.sql", down: "sql/user_notification/user_notification_down_01.sql" }, - user_preferences => { name: "user_preferences", up: "sql/user_preferences/user_preferences_up_01.sql", down: "sql/user_preferences/user_preferences_down_01.sql" }, - user_password_reset => { name: "user_password_reset", up: "sql/user_password_reset/user_password_reset_up_01.sql", down: "sql/user_password_reset/user_password_reset_down_01.sql" }, - user_relation => { name: "user_relation", up: "sql/user_relation/user_relation_up_01.sql", down: "sql/user_relation/user_relation_down_01.sql" }, - user_ssh_key => { name: "user_ssh_key", up: "sql/user_ssh_key/user_ssh_key_up_01.sql", down: "sql/user_ssh_key/user_ssh_key_down_01.sql" }, - user_token => { name: "user_token", up: "sql/user_token/user_token_up_01.sql", down: "sql/user_token/user_token_down_01.sql" }, - user_activity_log => { name: "user_activity_log", up: "sql/user_activity_log/user_activity_log_up_01.sql", down: "sql/user_activity_log/user_activity_log_down_01.sql" }, - project_access_log => { name: "project_access_log", up: "sql/project_access_log/project_access_log_up_01.sql", down: "sql/project_access_log/project_access_log_down_01.sql" }, - project_audit_log => { name: "project_audit_log", up: "sql/project_audit_log/project_audit_log_up_01.sql", down: "sql/project_audit_log/project_audit_log_down_01.sql" }, - project_billing => { name: "project_billing", up: "sql/project_billing/project_billing_up_01.sql", down: "sql/project_billing/project_billing_down_01.sql" }, - project_billing_history => { name: "project_billing_history", up: "sql/project_billing_history/project_billing_history_up_01.sql", down: "sql/project_billing_history/project_billing_history_down_01.sql" }, - project_follow => { name: "project_follow", up: "sql/project_follow/project_follow_up_01.sql", down: "sql/project_follow/project_follow_down_01.sql" }, - project_history_name => { name: "project_history_name", up: "sql/project_history_name/project_history_name_up_01.sql", down: "sql/project_history_name/project_history_name_down_01.sql" }, - project_label => { name: "project_label", up: "sql/project_label/project_label_up_01.sql", down: "sql/project_label/project_label_down_01.sql" }, - project_like => { name: "project_like", up: "sql/project_like/project_like_up_01.sql", down: "sql/project_like/project_like_down_01.sql" }, - project_member_invitations => { name: "project_member_invitations", up: "sql/project_member_invitations/project_member_invitations_up_01.sql", down: "sql/project_member_invitations/project_member_invitations_down_01.sql" }, - project_member_join_answers => { name: "project_member_join_answers", up: "sql/project_member_join_answers/project_member_join_answers_up_01.sql", down: "sql/project_member_join_answers/project_member_join_answers_down_01.sql" }, - project_member_join_request => { name: "project_member_join_request", up: "sql/project_member_join_request/project_member_join_request_up_01.sql", down: "sql/project_member_join_request/project_member_join_request_down_01.sql" }, - project_member_join_settings => { name: "project_member_join_settings", up: "sql/project_member_join_settings/project_member_join_settings_up_01.sql", down: "sql/project_member_join_settings/project_member_join_settings_down_01.sql" }, - project_members => { name: "project_members", up: "sql/project_members/project_members_up_01.sql", down: "sql/project_members/project_members_down_01.sql" }, - project_watch => { name: "project_watch", up: "sql/project_watch/project_watch_up_01.sql", down: "sql/project_watch/project_watch_down_01.sql" }, - repo => { name: "repo", up: "sql/repo/repo_up_01.sql", down: "sql/repo/repo_down_01.sql" }, - repo_branch => { name: "repo_branch", up: "sql/repo_branch/repo_branch_up_01.sql", down: "sql/repo_branch/repo_branch_down_01.sql" }, - repo_branch_protect => { name: "repo_branch_protect", up: "sql/repo_branch_protect/repo_branch_protect_up_01.sql", down: "sql/repo_branch_protect/repo_branch_protect_down_01.sql" }, - repo_collaborator => { name: "repo_collaborator", up: "sql/repo_collaborator/repo_collaborator_up_01.sql", down: "sql/repo_collaborator/repo_collaborator_down_01.sql" }, - repo_commit => { name: "repo_commit", up: "sql/repo_commit/repo_commit_up_01.sql", down: "sql/repo_commit/repo_commit_down_01.sql" }, - repo_fork => { name: "repo_fork", up: "sql/repo_fork/repo_fork_up_01.sql", down: "sql/repo_fork/repo_fork_down_01.sql" }, - repo_history_name => { name: "repo_history_name", up: "sql/repo_history_name/repo_history_name_up_01.sql", down: "sql/repo_history_name/repo_history_name_down_01.sql" }, - repo_hook => { name: "repo_hook", up: "sql/repo_hook/repo_hook_up_01.sql", down: "sql/repo_hook/repo_hook_down_01.sql" }, - repo_lfs_lock => { name: "repo_lfs_lock", up: "sql/repo_lfs_lock/repo_lfs_lock_up_01.sql", down: "sql/repo_lfs_lock/repo_lfs_lock_down_01.sql" }, - repo_lfs_object => { name: "repo_lfs_object", up: "sql/repo_lfs_object/repo_lfs_object_up_01.sql", down: "sql/repo_lfs_object/repo_lfs_object_down_01.sql" }, - repo_lock => { name: "repo_lock", up: "sql/repo_lock/repo_lock_up_01.sql", down: "sql/repo_lock/repo_lock_down_01.sql" }, - repo_star => { name: "repo_star", up: "sql/repo_star/repo_star_up_01.sql", down: "sql/repo_star/repo_star_down_01.sql" }, - repo_tag => { name: "repo_tag", up: "sql/repo_tag/repo_tag_up_01.sql", down: "sql/repo_tag/repo_tag_down_01.sql" }, - repo_upstream => { name: "repo_upstream", up: "sql/repo_upstream/repo_upstream_up_01.sql", down: "sql/repo_upstream/repo_upstream_down_01.sql" }, - repo_watch => { name: "repo_watch", up: "sql/repo_watch/repo_watch_up_01.sql", down: "sql/repo_watch/repo_watch_down_01.sql" }, - repo_webhook => { name: "repo_webhook", up: "sql/repo_webhook/repo_webhook_up_01.sql", down: "sql/repo_webhook/repo_webhook_down_01.sql" }, - issue => { name: "issue", up: "sql/issue/issue_up_01.sql", down: "sql/issue/issue_down_01.sql" }, - issue_assignee => { name: "issue_assignee", up: "sql/issue_assignee/issue_assignee_up_01.sql", down: "sql/issue_assignee/issue_assignee_down_01.sql" }, - issue_comment => { name: "issue_comment", up: "sql/issue_comment/issue_comment_up_01.sql", down: "sql/issue_comment/issue_comment_down_01.sql" }, - issue_comment_reaction => { name: "issue_comment_reaction", up: "sql/issue_comment_reaction/issue_comment_reaction_up_01.sql", down: "sql/issue_comment_reaction/issue_comment_reaction_down_01.sql" }, - issue_label => { name: "issue_label", up: "sql/issue_label/issue_label_up_01.sql", down: "sql/issue_label/issue_label_down_01.sql" }, - issue_pull_request => { name: "issue_pull_request", up: "sql/issue_pull_request/issue_pull_request_up_01.sql", down: "sql/issue_pull_request/issue_pull_request_down_01.sql" }, - issue_reaction => { name: "issue_reaction", up: "sql/issue_reaction/issue_reaction_up_01.sql", down: "sql/issue_reaction/issue_reaction_down_01.sql" }, - issue_repo => { name: "issue_repo", up: "sql/issue_repo/issue_repo_up_01.sql", down: "sql/issue_repo/issue_repo_down_01.sql" }, - issue_subscriber => { name: "issue_subscriber", up: "sql/issue_subscriber/issue_subscriber_up_01.sql", down: "sql/issue_subscriber/issue_subscriber_down_01.sql" }, - pull_request => { name: "pull_request", up: "sql/pull_request/pull_request_up_01.sql", down: "sql/pull_request/pull_request_down_01.sql" }, - pull_request_commit => { name: "pull_request_commit", up: "sql/pull_request_commit/pull_request_commit_up_01.sql", down: "sql/pull_request_commit/pull_request_commit_down_01.sql" }, - pull_request_review => { name: "pull_request_review", up: "sql/pull_request_review/pull_request_review_up_01.sql", down: "sql/pull_request_review/pull_request_review_down_01.sql" }, - pull_request_review_comment => { name: "pull_request_review_comment", up: "sql/pull_request_review_comment/pull_request_review_comment_up_01.sql", down: "sql/pull_request_review_comment/pull_request_review_comment_down_01.sql" }, - room_category => { name: "room_category", up: "sql/room_category/room_category_up_01.sql", down: "sql/room_category/room_category_down_01.sql" }, - room => { name: "room", up: "sql/room/room_up_01.sql", down: "sql/room/room_down_01.sql" }, - room_ai => { name: "room_ai", up: "sql/room_ai/room_ai_up_01.sql", down: "sql/room_ai/room_ai_down_01.sql" }, - room_message => { name: "room_message", up: "sql/room_message/room_message_up_01.sql", down: "sql/room_message/room_message_down_01.sql" }, - room_pin => { name: "room_pin", up: "sql/room_pin/room_pin_up_01.sql", down: "sql/room_pin/room_pin_down_01.sql" }, - room_thread => { name: "room_thread", up: "sql/room_thread/room_thread_up_01.sql", down: "sql/room_thread/room_thread_down_01.sql" }, - ai_model_provider => { name: "ai_model_provider", up: "sql/ai_model_provider/ai_model_provider_up_01.sql", down: "sql/ai_model_provider/ai_model_provider_down_01.sql" }, - ai_model => { name: "ai_model", up: "sql/ai_model/ai_model_up_01.sql", down: "sql/ai_model/ai_model_down_01.sql" }, - ai_model_version => { name: "ai_model_version", up: "sql/ai_model_version/ai_model_version_up_01.sql", down: "sql/ai_model_version/ai_model_version_down_01.sql" }, - ai_model_capability => { name: "ai_model_capability", up: "sql/ai_model_capability/ai_model_capability_up_01.sql", down: "sql/ai_model_capability/ai_model_capability_down_01.sql" }, - ai_model_parameter_profile => { name: "ai_model_parameter_profile", up: "sql/ai_model_parameter_profile/ai_model_parameter_profile_up_01.sql", down: "sql/ai_model_parameter_profile/ai_model_parameter_profile_down_01.sql" }, - ai_model_pricing => { name: "ai_model_pricing", up: "sql/ai_model_pricing/ai_model_pricing_up_01.sql", down: "sql/ai_model_pricing/ai_model_pricing_down_01.sql" }, - ai_session => { name: "ai_session", up: "sql/ai_session/ai_session_up_01.sql", down: "sql/ai_session/ai_session_down_01.sql" }, - ai_tool_call => { name: "ai_tool_call", up: "sql/ai_tool_call/ai_tool_call_up_01.sql", down: "sql/ai_tool_call/ai_tool_call_down_01.sql" }, - ai_tool_auth => { name: "ai_tool_auth", up: "sql/ai_tool_auth/ai_tool_auth_up_01.sql", down: "sql/ai_tool_auth/ai_tool_auth_down_01.sql" }, - label => { name: "label", up: "sql/label/label_up_01.sql", down: "sql/label/label_down_01.sql" }, - notify => { name: "notify", up: "sql/notify/notify_up_01.sql", down: "sql/notify/notify_down_01.sql" }, - room_notifications => { name: "room_notifications", up: "sql/room_notifications/room_notifications_up_01.sql", down: "sql/room_notifications/room_notifications_down_01.sql" }, - user_email_change => { name: "user_email_change", up: "sql/user_email_change/user_email_change_up_01.sql", down: "sql/user_email_change/user_email_change_down_01.sql" }, - project_activity => { name: "project_activity", up: "sql/project_activity/project_activity_up_01.sql", down: "sql/project_activity/project_activity_down_01.sql" }, - room_message_reaction => { name: "room_message_reaction", up: "sql/room_message_reaction/room_message_reaction_up_01.sql", down: "sql/room_message_reaction/room_message_reaction_down_01.sql" }, - room_message_edit_history => { name: "room_message_edit_history", up: "sql/room_message_edit_history/room_message_edit_history_up_01.sql", down: "sql/room_message_edit_history/room_message_edit_history_down_01.sql" }, - project_board => { name: "project_board", up: "sql/project_board/project_board_up_01.sql", down: "sql/project_board/project_board_down_01.sql" }, - project_board_column => { name: "project_board_column", up: "sql/project_board_column/project_board_column_up_01.sql", down: "sql/project_board_column/project_board_column_down_01.sql" }, - project_board_card => { name: "project_board_card", up: "sql/project_board_card/project_board_card_up_01.sql", down: "sql/project_board_card/project_board_card_down_01.sql" }, - pull_request_review_request => { name: "pull_request_review_request", up: "sql/pull_request_review_request/pull_request_review_request_up_01.sql", down: "sql/pull_request_review_request/pull_request_review_request_down_01.sql" }, - workspace => { name: "workspace", up: "sql/workspace/workspace_up_01.sql", down: "sql/workspace/workspace_down_01.sql" }, - project => { name: "project", up: "sql/project/project_up_01.sql", down: "sql/project/project_down_01.sql" }, - workspace_membership => { name: "workspace_membership", up: "sql/workspace_membership/workspace_membership_up_01.sql", down: "sql/workspace_membership/workspace_membership_down_01.sql" }, - workspace_billing => { name: "workspace_billing", up: "sql/workspace_billing/workspace_billing_up_01.sql", down: "sql/workspace_billing/workspace_billing_down_01.sql" }, - workspace_billing_history => { name: "workspace_billing_history", up: "sql/workspace_billing_history/workspace_billing_history_up_01.sql", down: "sql/workspace_billing_history/workspace_billing_history_down_01.sql" }, - project_skill => { name: "project_skill", up: "sql/project_skill/project_skill_up_01.sql", down: "sql/project_skill/project_skill_down_01.sql" }, - agent_task => { name: "agent_task", up: "sql/agent_task/agent_task_up_01.sql", down: "sql/agent_task/agent_task_down_01.sql" }, - admin_user => { name: "admin_user", up: "sql/admin_user/admin_user_up_01.sql", down: "sql/admin_user/admin_user_down_01.sql" }, - admin_role => { name: "admin_role", up: "sql/admin_role/admin_role_up_01.sql", down: "sql/admin_role/admin_role_down_01.sql" }, - admin_permission => { name: "admin_permission", up: "sql/admin_permission/admin_permission_up_01.sql", down: "sql/admin_permission/admin_permission_down_01.sql" }, - admin_user_role => { name: "admin_user_role", up: "sql/admin_user_role/admin_user_role_up_01.sql", down: "sql/admin_user_role/admin_user_role_down_01.sql" }, - admin_role_permission => { name: "admin_role_permission", up: "sql/admin_role_permission/admin_role_permission_up_01.sql", down: "sql/admin_role_permission/admin_role_permission_down_01.sql" }, - admin_audit_log => { name: "admin_audit_log", up: "sql/admin_audit_log/admin_audit_log_up_01.sql", down: "sql/admin_audit_log/admin_audit_log_down_01.sql" }, - admin_api_token => { name: "admin_api_token", up: "sql/admin_api_token/admin_api_token_up_01.sql", down: "sql/admin_api_token/admin_api_token_down_01.sql" }, - workspace_alert_config => { name: "workspace_alert_config", up: "sql/workspace_alert_config/workspace_alert_config_up_01.sql", down: "sql/workspace_alert_config/workspace_alert_config_down_01.sql" }, - room_attachment => { name: "room_attachment", up: "sql/room_attachment/room_attachment_up_01.sql", down: "sql/room_attachment/room_attachment_down_01.sql" }, - room_access => { name: "room_access", up: "sql/room_access/room_access_up_01.sql", down: "sql/room_access/room_access_down_01.sql" }, - room_user_state => { name: "room_user_state", up: "sql/room_user_state/room_user_state_up_01.sql", down: "sql/room_user_state/room_user_state_down_01.sql" }, - project_role_priority => { name: "project_role_priority", up: "sql/project_role_priority/project_role_priority_up_01.sql", down: "sql/project_role_priority/project_role_priority_down_01.sql" }, - ai_conversation => { name: "ai_conversation", up: "sql/ai_conversation/ai_conversation_up_01.sql", down: "sql/ai_conversation/ai_conversation_down_01.sql" }, - ai_message => { name: "ai_message", up: "sql/ai_message/ai_message_up_01.sql", down: "sql/ai_message/ai_message_down_01.sql" }, - ai_message_fork => { name: "ai_message_fork", up: "sql/ai_message_fork/ai_message_fork_up_01.sql", down: "sql/ai_message_fork/ai_message_fork_down_01.sql" }, - ai_shared_conversation => { name: "ai_shared_conversation", up: "sql/ai_shared_conversation/ai_shared_conversation_up_01.sql", down: "sql/ai_shared_conversation/ai_shared_conversation_down_01.sql" }, - ai_token_usage => { name: "ai_token_usage", up: "sql/ai_token_usage/ai_token_usage_up_01.sql", down: "sql/ai_token_usage/ai_token_usage_down_01.sql" }, - room_compact_summary => { name: "room_compact_summary", up: "sql/room_compact_summary/room_compact_summary_up_01.sql", down: "sql/room_compact_summary/room_compact_summary_down_01.sql" }, - user_billing_history => { name: "user_billing_history", up: "sql/user_billing_history/user_billing_history_up_01.sql", down: "sql/user_billing_history/user_billing_history_down_01.sql" }, - project_message_favorite => { name: "project_message_favorite", up: "sql/project_message_favorite/project_message_favorite_up_01.sql", down: "sql/project_message_favorite/project_message_favorite_down_01.sql" }, - ai_subagent_session => { name: "ai_subagent_session", up: "sql/ai_subagent_session/ai_subagent_session_up_01.sql", down: "sql/ai_subagent_session/ai_subagent_session_down_01.sql" }, -} - -pub mod bootstrap; diff --git a/libs/migrate/sql/admin_api_token/admin_api_token_down_01.sql b/libs/migrate/sql/admin_api_token/admin_api_token_down_01.sql deleted file mode 100644 index 2e52e2c..0000000 --- a/libs/migrate/sql/admin_api_token/admin_api_token_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_admin_api_token_hash; -DROP TABLE IF EXISTS admin_api_token; diff --git a/libs/migrate/sql/admin_api_token/admin_api_token_up_01.sql b/libs/migrate/sql/admin_api_token/admin_api_token_up_01.sql deleted file mode 100644 index 3c8507b..0000000 --- a/libs/migrate/sql/admin_api_token/admin_api_token_up_01.sql +++ /dev/null @@ -1,22 +0,0 @@ -create table if not exists admin_api_token -( - id serial - primary key, - name varchar(255) not null, - token_hash varchar(64) not null - unique, - token_prefix varchar(32) not null, - permissions text[] default '{}'::text[] not null, - created_by integer not null - references admin_user - on delete set null, - created_at timestamp with time zone default now() not null, - last_used_at timestamp with time zone, - expires_at timestamp with time zone, - is_active boolean default true not null -); - -comment on table admin_api_token is 'Admin API Token for programmatic access'; - -create index if not exists idx_admin_api_token_hash - on admin_api_token (token_hash); diff --git a/libs/migrate/sql/admin_audit_log/admin_audit_log_down_01.sql b/libs/migrate/sql/admin_audit_log/admin_audit_log_down_01.sql deleted file mode 100644 index 935b134..0000000 --- a/libs/migrate/sql/admin_audit_log/admin_audit_log_down_01.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP INDEX IF EXISTS idx_admin_audit_log_resource; -DROP INDEX IF EXISTS idx_admin_audit_log_action; -DROP INDEX IF EXISTS idx_admin_audit_log_created_at; -DROP INDEX IF EXISTS idx_admin_audit_log_user_id; -DROP TABLE IF EXISTS admin_audit_log; diff --git a/libs/migrate/sql/admin_audit_log/admin_audit_log_up_01.sql b/libs/migrate/sql/admin_audit_log/admin_audit_log_up_01.sql deleted file mode 100644 index 4bdf8cf..0000000 --- a/libs/migrate/sql/admin_audit_log/admin_audit_log_up_01.sql +++ /dev/null @@ -1,28 +0,0 @@ -create table if not exists admin_audit_log -( - id bigserial - primary key, - user_id integer not null, - username varchar(255) not null, - action varchar(50) not null, - resource varchar(255) not null, - resource_id varchar(255), - request_params jsonb, - ip_address varchar(255), - user_agent text, - result varchar(20) default 'success'::character varying not null, - error_message text, - created_at timestamp with time zone default now() not null -); - -create index if not exists idx_admin_audit_log_user_id - on admin_audit_log (user_id); - -create index if not exists idx_admin_audit_log_created_at - on admin_audit_log (created_at desc); - -create index if not exists idx_admin_audit_log_action - on admin_audit_log (action); - -create index if not exists idx_admin_audit_log_resource - on admin_audit_log (resource); diff --git a/libs/migrate/sql/admin_permission/admin_permission_down_01.sql b/libs/migrate/sql/admin_permission/admin_permission_down_01.sql deleted file mode 100644 index 05f26b4..0000000 --- a/libs/migrate/sql/admin_permission/admin_permission_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS admin_permission; diff --git a/libs/migrate/sql/admin_permission/admin_permission_up_01.sql b/libs/migrate/sql/admin_permission/admin_permission_up_01.sql deleted file mode 100644 index 5aee6c1..0000000 --- a/libs/migrate/sql/admin_permission/admin_permission_up_01.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table if not exists admin_permission -( - id serial - primary key, - name varchar(255) not null, - code varchar(255) not null - unique, - description text, - created_at timestamp with time zone default now() not null -); diff --git a/libs/migrate/sql/admin_role/admin_role_down_01.sql b/libs/migrate/sql/admin_role/admin_role_down_01.sql deleted file mode 100644 index 866a0ab..0000000 --- a/libs/migrate/sql/admin_role/admin_role_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS admin_role; diff --git a/libs/migrate/sql/admin_role/admin_role_up_01.sql b/libs/migrate/sql/admin_role/admin_role_up_01.sql deleted file mode 100644 index e397d75..0000000 --- a/libs/migrate/sql/admin_role/admin_role_up_01.sql +++ /dev/null @@ -1,9 +0,0 @@ -create table if not exists admin_role -( - id serial - primary key, - name varchar(255) not null - unique, - description text, - created_at timestamp with time zone default now() not null -); diff --git a/libs/migrate/sql/admin_role_permission/admin_role_permission_down_01.sql b/libs/migrate/sql/admin_role_permission/admin_role_permission_down_01.sql deleted file mode 100644 index 1561786..0000000 --- a/libs/migrate/sql/admin_role_permission/admin_role_permission_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS admin_role_permission; diff --git a/libs/migrate/sql/admin_role_permission/admin_role_permission_up_01.sql b/libs/migrate/sql/admin_role_permission/admin_role_permission_up_01.sql deleted file mode 100644 index cf3910e..0000000 --- a/libs/migrate/sql/admin_role_permission/admin_role_permission_up_01.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table if not exists admin_role_permission -( - role_id integer not null - references admin_role - on delete cascade, - permission_id integer not null - references admin_permission - on delete cascade, - primary key (role_id, permission_id) -); diff --git a/libs/migrate/sql/admin_user/admin_user_down_01.sql b/libs/migrate/sql/admin_user/admin_user_down_01.sql deleted file mode 100644 index 84d36bb..0000000 --- a/libs/migrate/sql/admin_user/admin_user_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_admin_user_username; -DROP TABLE IF EXISTS admin_user; diff --git a/libs/migrate/sql/admin_user/admin_user_up_01.sql b/libs/migrate/sql/admin_user/admin_user_up_01.sql deleted file mode 100644 index 1aae385..0000000 --- a/libs/migrate/sql/admin_user/admin_user_up_01.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table if not exists admin_user -( - id serial - primary key, - username varchar(255) not null - unique, - password_hash varchar(255) not null, - is_active boolean default true not null, - created_at timestamp with time zone default now() not null, - updated_at timestamp with time zone default now() not null -); - -create index if not exists idx_admin_user_username - on admin_user (username); diff --git a/libs/migrate/sql/admin_user_role/admin_user_role_down_01.sql b/libs/migrate/sql/admin_user_role/admin_user_role_down_01.sql deleted file mode 100644 index 2086b16..0000000 --- a/libs/migrate/sql/admin_user_role/admin_user_role_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS admin_user_role; diff --git a/libs/migrate/sql/admin_user_role/admin_user_role_up_01.sql b/libs/migrate/sql/admin_user_role/admin_user_role_up_01.sql deleted file mode 100644 index 69a9cdb..0000000 --- a/libs/migrate/sql/admin_user_role/admin_user_role_up_01.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table if not exists admin_user_role -( - user_id integer not null - references admin_user - on delete cascade, - role_id integer not null - references admin_role - on delete cascade, - primary key (user_id, role_id) -); diff --git a/libs/migrate/sql/agent_task/agent_task_down_01.sql b/libs/migrate/sql/agent_task/agent_task_down_01.sql deleted file mode 100644 index ce375d3..0000000 --- a/libs/migrate/sql/agent_task/agent_task_down_01.sql +++ /dev/null @@ -1,8 +0,0 @@ -DROP INDEX IF EXISTS idx_agent_task_retry_count; -DROP INDEX IF EXISTS idx_agent_task_issue; -DROP INDEX IF EXISTS idx_agent_task_created_at; -DROP INDEX IF EXISTS idx_agent_task_created_by; -DROP INDEX IF EXISTS idx_agent_task_status; -DROP INDEX IF EXISTS idx_agent_task_parent; -DROP INDEX IF EXISTS idx_agent_task_project; -DROP TABLE IF EXISTS agent_task; diff --git a/libs/migrate/sql/agent_task/agent_task_up_01.sql b/libs/migrate/sql/agent_task/agent_task_up_01.sql deleted file mode 100644 index 0408a45..0000000 --- a/libs/migrate/sql/agent_task/agent_task_up_01.sql +++ /dev/null @@ -1,45 +0,0 @@ -create table if not exists agent_task -( - id bigserial - primary key, - project_uuid uuid not null, - parent_id bigint - constraint fk_agent_task_parent - references agent_task - on delete set null, - agent_type varchar(20) default 'react'::character varying not null, - status varchar(20) default 'pending'::character varying not null, - title varchar(255), - input text not null, - output text, - error text, - created_by uuid, - created_at timestamp with time zone default now() not null, - updated_at timestamp with time zone default now() not null, - started_at timestamp with time zone, - done_at timestamp with time zone, - progress varchar(255), - issue_id uuid, - retry_count integer default 0 -); - -create index if not exists idx_agent_task_project - on agent_task (project_uuid); - -create index if not exists idx_agent_task_parent - on agent_task (parent_id); - -create index if not exists idx_agent_task_status - on agent_task (status); - -create index if not exists idx_agent_task_created_by - on agent_task (created_by); - -create index if not exists idx_agent_task_created_at - on agent_task (created_at); - -create index if not exists idx_agent_task_issue - on agent_task (issue_id); - -create index if not exists idx_agent_task_retry_count - on agent_task (retry_count); diff --git a/libs/migrate/sql/ai_conversation/ai_conversation_down_01.sql b/libs/migrate/sql/ai_conversation/ai_conversation_down_01.sql deleted file mode 100644 index 25ff6f5..0000000 --- a/libs/migrate/sql/ai_conversation/ai_conversation_down_01.sql +++ /dev/null @@ -1,8 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_conv_project_uid; -DROP INDEX IF EXISTS idx_ai_conv_access_vis; -DROP INDEX IF EXISTS idx_ai_conv_project_created; -DROP INDEX IF EXISTS idx_ai_conv_user_created; -DROP INDEX IF EXISTS idx_ai_conv_scope; -DROP INDEX IF EXISTS idx_ai_conv_project_id; -DROP INDEX IF EXISTS idx_ai_conv_user_id; -DROP TABLE IF EXISTS ai_conversation; diff --git a/libs/migrate/sql/ai_conversation/ai_conversation_up_01.sql b/libs/migrate/sql/ai_conversation/ai_conversation_up_01.sql deleted file mode 100644 index d21c8af..0000000 --- a/libs/migrate/sql/ai_conversation/ai_conversation_up_01.sql +++ /dev/null @@ -1,49 +0,0 @@ -create table if not exists ai_conversation -( - id uuid default gen_random_uuid() not null - primary key, - user_id uuid not null, - project_id uuid - constraint fk_ai_conv_project - references project - on delete cascade, - scope varchar(16) not null, - title varchar(512), - model varchar(128) default 'gpt-4'::character varying not null, - model_config jsonb, - status varchar(32) default 'active'::character varying not null, - root_message_id uuid, - fork_count integer default 0 not null, - is_shared boolean default false not null, - message_count integer default 0 not null, - token_usage_total integer, - created_at timestamp with time zone default now() not null, - updated_at timestamp with time zone default now() not null, - access_visibility varchar(32) default 'owner'::character varying not null, - can_ask varchar(32) default 'owner'::character varying not null, - project_uid integer, - model_uid uuid, - model_name varchar(256) -); - -create index if not exists idx_ai_conv_user_id - on ai_conversation (user_id); - -create index if not exists idx_ai_conv_project_id - on ai_conversation (project_id); - -create index if not exists idx_ai_conv_scope - on ai_conversation (scope); - -create index if not exists idx_ai_conv_user_created - on ai_conversation (user_id asc, created_at desc); - -create index if not exists idx_ai_conv_project_created - on ai_conversation (project_id asc, created_at desc); - -create index if not exists idx_ai_conv_access_vis - on ai_conversation (access_visibility); - -create index if not exists idx_ai_conv_project_uid - on ai_conversation (project_id, project_uid) - where (project_uid IS NOT NULL); diff --git a/libs/migrate/sql/ai_message/ai_message_down_01.sql b/libs/migrate/sql/ai_message/ai_message_down_01.sql deleted file mode 100644 index f770d24..0000000 --- a/libs/migrate/sql/ai_message/ai_message_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_msg_parent; -DROP INDEX IF EXISTS idx_ai_msg_conv; -DROP TABLE IF EXISTS ai_message; diff --git a/libs/migrate/sql/ai_message/ai_message_up_01.sql b/libs/migrate/sql/ai_message/ai_message_up_01.sql deleted file mode 100644 index c0d88cf..0000000 --- a/libs/migrate/sql/ai_message/ai_message_up_01.sql +++ /dev/null @@ -1,30 +0,0 @@ -create table if not exists ai_message -( - id uuid default gen_random_uuid() not null - primary key, - conversation_id uuid not null - constraint fk_ai_msg_conv - references ai_conversation - on delete cascade, - parent_message_id uuid - constraint fk_ai_msg_parent - references ai_message - on delete set null, - role varchar(16) not null, - content jsonb not null, - model varchar(128), - is_fork_origin boolean default false not null, - stop_reason varchar(32), - input_tokens integer, - output_tokens integer, - latency_ms integer, - metadata jsonb, - room_id uuid, - created_at timestamp with time zone default now() not null -); - -create index if not exists idx_ai_msg_conv - on ai_message (conversation_id, created_at); - -create index if not exists idx_ai_msg_parent - on ai_message (parent_message_id); diff --git a/libs/migrate/sql/ai_message_fork/ai_message_fork_down_01.sql b/libs/migrate/sql/ai_message_fork/ai_message_fork_down_01.sql deleted file mode 100644 index 3df2611..0000000 --- a/libs/migrate/sql/ai_message_fork/ai_message_fork_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_fork_fork; -DROP INDEX IF EXISTS idx_ai_fork_source; -DROP TABLE IF EXISTS ai_message_fork; diff --git a/libs/migrate/sql/ai_message_fork/ai_message_fork_up_01.sql b/libs/migrate/sql/ai_message_fork/ai_message_fork_up_01.sql deleted file mode 100644 index b7db7b6..0000000 --- a/libs/migrate/sql/ai_message_fork/ai_message_fork_up_01.sql +++ /dev/null @@ -1,20 +0,0 @@ -create table if not exists ai_message_fork -( - id uuid default gen_random_uuid() not null - primary key, - source_message_id uuid not null - constraint fk_ai_fork_source - references ai_message - on delete cascade, - fork_message_id uuid not null - constraint fk_ai_fork_fork - references ai_message - on delete cascade, - created_at timestamp with time zone default now() not null -); - -create index if not exists idx_ai_fork_source - on ai_message_fork (source_message_id); - -create index if not exists idx_ai_fork_fork - on ai_message_fork (fork_message_id); diff --git a/libs/migrate/sql/ai_model/ai_model_down_01.sql b/libs/migrate/sql/ai_model/ai_model_down_01.sql deleted file mode 100644 index e172e66..0000000 --- a/libs/migrate/sql/ai_model/ai_model_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_model_provider_id; -DROP TABLE IF EXISTS ai_model; diff --git a/libs/migrate/sql/ai_model/ai_model_up_01.sql b/libs/migrate/sql/ai_model/ai_model_up_01.sql deleted file mode 100644 index 4a85064..0000000 --- a/libs/migrate/sql/ai_model/ai_model_up_01.sql +++ /dev/null @@ -1,19 +0,0 @@ -create table if not exists ai_model -( - id uuid not null - primary key, - provider_id uuid not null, - name varchar(255) not null, - modality varchar(255) not null, - capability varchar(255) not null, - context_length bigint not null, - max_output_tokens bigint, - training_cutoff timestamp with time zone, - is_open_source boolean default false not null, - status varchar(255) not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); - -create index if not exists idx_ai_model_provider_id - on ai_model (provider_id); diff --git a/libs/migrate/sql/ai_model_capability/ai_model_capability_down_01.sql b/libs/migrate/sql/ai_model_capability/ai_model_capability_down_01.sql deleted file mode 100644 index d895ce0..0000000 --- a/libs/migrate/sql/ai_model_capability/ai_model_capability_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_model_capability_model_version_id; -DROP TABLE IF EXISTS ai_model_capability; diff --git a/libs/migrate/sql/ai_model_capability/ai_model_capability_up_01.sql b/libs/migrate/sql/ai_model_capability/ai_model_capability_up_01.sql deleted file mode 100644 index ee38ef0..0000000 --- a/libs/migrate/sql/ai_model_capability/ai_model_capability_up_01.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table if not exists ai_model_capability -( - id bigserial - primary key, - model_version_id uuid not null, - capability varchar(255) not null, - is_supported boolean default false not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_ai_model_capability_model_version_id - on ai_model_capability (model_version_id); diff --git a/libs/migrate/sql/ai_model_parameter_profile/ai_model_parameter_profile_down_01.sql b/libs/migrate/sql/ai_model_parameter_profile/ai_model_parameter_profile_down_01.sql deleted file mode 100644 index 2dd6a94..0000000 --- a/libs/migrate/sql/ai_model_parameter_profile/ai_model_parameter_profile_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_model_parameter_profile_model_version_id; -DROP TABLE IF EXISTS ai_model_parameter_profile; diff --git a/libs/migrate/sql/ai_model_parameter_profile/ai_model_parameter_profile_up_01.sql b/libs/migrate/sql/ai_model_parameter_profile/ai_model_parameter_profile_up_01.sql deleted file mode 100644 index 5a229e1..0000000 --- a/libs/migrate/sql/ai_model_parameter_profile/ai_model_parameter_profile_up_01.sql +++ /dev/null @@ -1,16 +0,0 @@ -create table if not exists ai_model_parameter_profile -( - id bigserial - primary key, - model_version_id uuid not null - unique, - temperature_min double precision not null, - temperature_max double precision not null, - top_p_min double precision not null, - top_p_max double precision not null, - frequency_penalty_supported boolean default false not null, - presence_penalty_supported boolean default false not null -); - -create unique index if not exists idx_ai_model_parameter_profile_model_version_id - on ai_model_parameter_profile (model_version_id); diff --git a/libs/migrate/sql/ai_model_pricing/ai_model_pricing_down_01.sql b/libs/migrate/sql/ai_model_pricing/ai_model_pricing_down_01.sql deleted file mode 100644 index 8f79e00..0000000 --- a/libs/migrate/sql/ai_model_pricing/ai_model_pricing_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_model_pricing_model_version_id; -DROP TABLE IF EXISTS ai_model_pricing; diff --git a/libs/migrate/sql/ai_model_pricing/ai_model_pricing_up_01.sql b/libs/migrate/sql/ai_model_pricing/ai_model_pricing_up_01.sql deleted file mode 100644 index 5f54c92..0000000 --- a/libs/migrate/sql/ai_model_pricing/ai_model_pricing_up_01.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table if not exists ai_model_pricing -( - id bigserial - primary key, - model_version_id uuid not null, - input_price_per_1k_tokens varchar(255) not null, - output_price_per_1k_tokens varchar(255) not null, - currency varchar(255) not null, - effective_from timestamp with time zone not null -); - -create index if not exists idx_ai_model_pricing_model_version_id - on ai_model_pricing (model_version_id); diff --git a/libs/migrate/sql/ai_model_provider/ai_model_provider_down_01.sql b/libs/migrate/sql/ai_model_provider/ai_model_provider_down_01.sql deleted file mode 100644 index 6af389d..0000000 --- a/libs/migrate/sql/ai_model_provider/ai_model_provider_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS ai_model_provider; diff --git a/libs/migrate/sql/ai_model_provider/ai_model_provider_up_01.sql b/libs/migrate/sql/ai_model_provider/ai_model_provider_up_01.sql deleted file mode 100644 index 53941ae..0000000 --- a/libs/migrate/sql/ai_model_provider/ai_model_provider_up_01.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table if not exists ai_model_provider -( - id uuid not null - primary key, - name varchar(255) not null, - display_name varchar(255) not null, - website varchar(255), - status varchar(255) not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); diff --git a/libs/migrate/sql/ai_model_version/ai_model_version_down_01.sql b/libs/migrate/sql/ai_model_version/ai_model_version_down_01.sql deleted file mode 100644 index 1e1ce7a..0000000 --- a/libs/migrate/sql/ai_model_version/ai_model_version_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_model_version_model_id; -DROP TABLE IF EXISTS ai_model_version; diff --git a/libs/migrate/sql/ai_model_version/ai_model_version_up_01.sql b/libs/migrate/sql/ai_model_version/ai_model_version_up_01.sql deleted file mode 100644 index 2f772f2..0000000 --- a/libs/migrate/sql/ai_model_version/ai_model_version_up_01.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table if not exists ai_model_version -( - id uuid not null - primary key, - model_id uuid not null, - version varchar(255) not null, - release_date timestamp with time zone, - change_log text, - is_default boolean default false not null, - status varchar(255) not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_ai_model_version_model_id - on ai_model_version (model_id); diff --git a/libs/migrate/sql/ai_session/ai_session_down_01.sql b/libs/migrate/sql/ai_session/ai_session_down_01.sql deleted file mode 100644 index 2578f8a..0000000 --- a/libs/migrate/sql/ai_session/ai_session_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_session_room; -DROP TABLE IF EXISTS ai_session; diff --git a/libs/migrate/sql/ai_session/ai_session_up_01.sql b/libs/migrate/sql/ai_session/ai_session_up_01.sql deleted file mode 100644 index 0605131..0000000 --- a/libs/migrate/sql/ai_session/ai_session_up_01.sql +++ /dev/null @@ -1,19 +0,0 @@ -create table if not exists ai_session -( - id uuid not null - primary key, - room uuid not null, - model uuid not null, - version uuid not null, - token_input bigint default 0 not null, - token_output bigint default 0 not null, - latency_ms bigint, - cost double precision, - currency varchar(255), - error_message text, - error_code varchar(255), - created_at timestamp with time zone not null -); - -create index if not exists idx_ai_session_room - on ai_session (room); diff --git a/libs/migrate/sql/ai_shared_conversation/ai_shared_conversation_down_01.sql b/libs/migrate/sql/ai_shared_conversation/ai_shared_conversation_down_01.sql deleted file mode 100644 index bc4d65e..0000000 --- a/libs/migrate/sql/ai_shared_conversation/ai_shared_conversation_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_share_token; -DROP INDEX IF EXISTS idx_ai_share_conv; -DROP TABLE IF EXISTS ai_shared_conversation; diff --git a/libs/migrate/sql/ai_shared_conversation/ai_shared_conversation_up_01.sql b/libs/migrate/sql/ai_shared_conversation/ai_shared_conversation_up_01.sql deleted file mode 100644 index 6a7e486..0000000 --- a/libs/migrate/sql/ai_shared_conversation/ai_shared_conversation_up_01.sql +++ /dev/null @@ -1,21 +0,0 @@ -create table if not exists ai_shared_conversation -( - id uuid default gen_random_uuid() not null - primary key, - conversation_id uuid not null - constraint fk_ai_share_conv - references ai_conversation - on delete cascade, - share_token varchar(128) not null - unique, - created_by uuid not null, - view_count integer default 0 not null, - created_at timestamp with time zone default now() not null, - expires_at timestamp with time zone -); - -create index if not exists idx_ai_share_conv - on ai_shared_conversation (conversation_id); - -create index if not exists idx_ai_share_token - on ai_shared_conversation (share_token); diff --git a/libs/migrate/sql/ai_subagent_session/ai_subagent_session_down_01.sql b/libs/migrate/sql/ai_subagent_session/ai_subagent_session_down_01.sql deleted file mode 100644 index 1da651c..0000000 --- a/libs/migrate/sql/ai_subagent_session/ai_subagent_session_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_subagent_session_message; -DROP INDEX IF EXISTS idx_ai_subagent_session_children; -DROP INDEX IF EXISTS idx_ai_subagent_session_conv; -DROP TABLE IF EXISTS ai_subagent_session; diff --git a/libs/migrate/sql/ai_subagent_session/ai_subagent_session_up_01.sql b/libs/migrate/sql/ai_subagent_session/ai_subagent_session_up_01.sql deleted file mode 100644 index 9fd6b6b..0000000 --- a/libs/migrate/sql/ai_subagent_session/ai_subagent_session_up_01.sql +++ /dev/null @@ -1,25 +0,0 @@ -create table if not exists ai_subagent_session -( - id uuid not null primary key, - conversation_id uuid not null, - message_id uuid not null, - children_id varchar(255) not null, - role varchar(64) not null, - task text not null, - output text not null, - input_tokens bigint default 0 not null, - output_tokens bigint default 0 not null, - model_name varchar(255), - status varchar(32) default 'completed' not null, - error_message text, - created_at timestamp with time zone not null -); - -create index if not exists idx_ai_subagent_session_conv - on ai_subagent_session (conversation_id); - -create index if not exists idx_ai_subagent_session_children - on ai_subagent_session (children_id); - -create index if not exists idx_ai_subagent_session_message - on ai_subagent_session (message_id); diff --git a/libs/migrate/sql/ai_token_usage/ai_token_usage_down_01.sql b/libs/migrate/sql/ai_token_usage/ai_token_usage_down_01.sql deleted file mode 100644 index 4583024..0000000 --- a/libs/migrate/sql/ai_token_usage/ai_token_usage_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_token_recorded; -DROP INDEX IF EXISTS idx_ai_token_conv; -DROP INDEX IF EXISTS idx_ai_token_user; -DROP TABLE IF EXISTS ai_token_usage; diff --git a/libs/migrate/sql/ai_token_usage/ai_token_usage_up_01.sql b/libs/migrate/sql/ai_token_usage/ai_token_usage_up_01.sql deleted file mode 100644 index c6e20b4..0000000 --- a/libs/migrate/sql/ai_token_usage/ai_token_usage_up_01.sql +++ /dev/null @@ -1,21 +0,0 @@ -create table if not exists ai_token_usage -( - id uuid default gen_random_uuid() not null - primary key, - user_id uuid not null, - conversation_id uuid, - model varchar(128) not null, - input_tokens integer not null, - output_tokens integer not null, - cost_usd numeric(10, 6), - recorded_at timestamp with time zone default now() not null -); - -create index if not exists idx_ai_token_user - on ai_token_usage (user_id); - -create index if not exists idx_ai_token_conv - on ai_token_usage (conversation_id); - -create index if not exists idx_ai_token_recorded - on ai_token_usage (recorded_at); diff --git a/libs/migrate/sql/ai_tool_auth/ai_tool_auth_down_01.sql b/libs/migrate/sql/ai_tool_auth/ai_tool_auth_down_01.sql deleted file mode 100644 index 94b4d05..0000000 --- a/libs/migrate/sql/ai_tool_auth/ai_tool_auth_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS ai_tool_auth; diff --git a/libs/migrate/sql/ai_tool_auth/ai_tool_auth_up_01.sql b/libs/migrate/sql/ai_tool_auth/ai_tool_auth_up_01.sql deleted file mode 100644 index 33abfd8..0000000 --- a/libs/migrate/sql/ai_tool_auth/ai_tool_auth_up_01.sql +++ /dev/null @@ -1,17 +0,0 @@ -create table if not exists ai_tool_auth -( - session uuid not null, - tool_call_id varchar(255) not null, - method varchar(255) not null, - arguments text not null, - decision boolean default false not null, - reason varchar(255) not null, - decision_by uuid not null, - decision_comment text, - logs jsonb not null, - expires_at timestamp with time zone, - authorized_at timestamp with time zone, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - primary key (session, tool_call_id) -); diff --git a/libs/migrate/sql/ai_tool_call/ai_tool_call_down_01.sql b/libs/migrate/sql/ai_tool_call/ai_tool_call_down_01.sql deleted file mode 100644 index 4e2546b..0000000 --- a/libs/migrate/sql/ai_tool_call/ai_tool_call_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_ai_tool_call_status; -DROP INDEX IF EXISTS idx_ai_tool_call_session; -DROP TABLE IF EXISTS ai_tool_call; diff --git a/libs/migrate/sql/ai_tool_call/ai_tool_call_up_01.sql b/libs/migrate/sql/ai_tool_call/ai_tool_call_up_01.sql deleted file mode 100644 index 3ae60f8..0000000 --- a/libs/migrate/sql/ai_tool_call/ai_tool_call_up_01.sql +++ /dev/null @@ -1,24 +0,0 @@ -create table if not exists ai_tool_call -( - tool_call_id varchar(255) not null, - session uuid not null, - tool_name varchar(255) not null, - caller uuid not null, - arguments jsonb not null, - result jsonb not null, - status varchar(255) not null, - execution_time_ms bigint, - error_message text, - error_stack text, - retry_count integer default 0 not null, - created_at timestamp with time zone not null, - completed_at timestamp with time zone, - updated_at timestamp with time zone not null, - primary key (tool_call_id, session) -); - -create index if not exists idx_ai_tool_call_session - on ai_tool_call (session); - -create index if not exists idx_ai_tool_call_status - on ai_tool_call (status); diff --git a/libs/migrate/sql/bootstrap/bootstrap_down_01.sql b/libs/migrate/sql/bootstrap/bootstrap_down_01.sql deleted file mode 100644 index 3a78dd0..0000000 --- a/libs/migrate/sql/bootstrap/bootstrap_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP TRIGGER IF EXISTS room_message_tsv_update ON room_message; -DROP FUNCTION IF EXISTS room_message_tsv_trigger(); -DROP TYPE IF EXISTS notification_type; diff --git a/libs/migrate/sql/bootstrap/bootstrap_up_01.sql b/libs/migrate/sql/bootstrap/bootstrap_up_01.sql deleted file mode 100644 index 75a3396..0000000 --- a/libs/migrate/sql/bootstrap/bootstrap_up_01.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE OR REPLACE FUNCTION room_message_tsv_trigger() -RETURNS trigger -LANGUAGE plpgsql -AS $function$ -BEGIN - NEW.content_tsv := to_tsvector('english', NEW.content); - RETURN NEW; -END; -$function$; -CREATE OR REPLACE TRIGGER room_message_tsv_update - BEFORE INSERT OR UPDATE - ON room_message - FOR EACH ROW -EXECUTE PROCEDURE room_message_tsv_trigger(); - -DO -$$ - BEGIN - IF NOT EXISTS (SELECT 1 - FROM pg_type - WHERE typname = 'notification_type' - AND typtype = 'e') THEN - CREATE TYPE notification_type AS ENUM ( - 'mention', - 'invitation', - 'role_change', - 'room_created', - 'room_deleted', - 'system_announcement' - ); - END IF; - END -$$; diff --git a/libs/migrate/sql/issue/issue_down_01.sql b/libs/migrate/sql/issue/issue_down_01.sql deleted file mode 100644 index 85f98e9..0000000 --- a/libs/migrate/sql/issue/issue_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_issue_state; -DROP INDEX IF EXISTS idx_issue_author; -DROP INDEX IF EXISTS idx_issue_project; -DROP TABLE IF EXISTS issue; diff --git a/libs/migrate/sql/issue/issue_up_01.sql b/libs/migrate/sql/issue/issue_up_01.sql deleted file mode 100644 index 05d433b..0000000 --- a/libs/migrate/sql/issue/issue_up_01.sql +++ /dev/null @@ -1,25 +0,0 @@ -create table if not exists issue -( - id uuid not null - primary key, - project uuid not null, - number bigint not null, - title varchar(255) not null, - body text, - state varchar(255) not null, - author uuid not null, - milestone varchar(255), - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - closed_at timestamp with time zone, - created_by_ai boolean default false not null -); - -create index if not exists idx_issue_project - on issue (project); - -create index if not exists idx_issue_author - on issue (author); - -create index if not exists idx_issue_state - on issue (state); diff --git a/libs/migrate/sql/issue_assignee/issue_assignee_down_01.sql b/libs/migrate/sql/issue_assignee/issue_assignee_down_01.sql deleted file mode 100644 index 9fae8c2..0000000 --- a/libs/migrate/sql/issue_assignee/issue_assignee_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS issue_assignee; diff --git a/libs/migrate/sql/issue_assignee/issue_assignee_up_01.sql b/libs/migrate/sql/issue_assignee/issue_assignee_up_01.sql deleted file mode 100644 index 8549f09..0000000 --- a/libs/migrate/sql/issue_assignee/issue_assignee_up_01.sql +++ /dev/null @@ -1,7 +0,0 @@ -create table if not exists issue_assignee -( - issue uuid not null, - "user" uuid not null, - assigned_at timestamp with time zone not null, - primary key (issue, "user") -); diff --git a/libs/migrate/sql/issue_comment/issue_comment_down_01.sql b/libs/migrate/sql/issue_comment/issue_comment_down_01.sql deleted file mode 100644 index 622c11f..0000000 --- a/libs/migrate/sql/issue_comment/issue_comment_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_issue_comment_issue; -DROP TABLE IF EXISTS issue_comment; diff --git a/libs/migrate/sql/issue_comment/issue_comment_up_01.sql b/libs/migrate/sql/issue_comment/issue_comment_up_01.sql deleted file mode 100644 index d936c14..0000000 --- a/libs/migrate/sql/issue_comment/issue_comment_up_01.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table if not exists issue_comment -( - id bigserial - primary key, - issue uuid not null, - author uuid not null, - body text not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); - -create index if not exists idx_issue_comment_issue - on issue_comment (issue); diff --git a/libs/migrate/sql/issue_comment_reaction/issue_comment_reaction_down_01.sql b/libs/migrate/sql/issue_comment_reaction/issue_comment_reaction_down_01.sql deleted file mode 100644 index d3d0187..0000000 --- a/libs/migrate/sql/issue_comment_reaction/issue_comment_reaction_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS issue_comment_reaction; diff --git a/libs/migrate/sql/issue_comment_reaction/issue_comment_reaction_up_01.sql b/libs/migrate/sql/issue_comment_reaction/issue_comment_reaction_up_01.sql deleted file mode 100644 index 0b720cf..0000000 --- a/libs/migrate/sql/issue_comment_reaction/issue_comment_reaction_up_01.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table if not exists issue_comment_reaction -( - comment_id bigint not null, - user_uuid uuid not null, - reaction varchar(255) not null, - created_at timestamp with time zone not null, - primary key (comment_id, user_uuid, reaction) -); diff --git a/libs/migrate/sql/issue_label/issue_label_down_01.sql b/libs/migrate/sql/issue_label/issue_label_down_01.sql deleted file mode 100644 index 2ae8000..0000000 --- a/libs/migrate/sql/issue_label/issue_label_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS issue_label; diff --git a/libs/migrate/sql/issue_label/issue_label_up_01.sql b/libs/migrate/sql/issue_label/issue_label_up_01.sql deleted file mode 100644 index 22c14a5..0000000 --- a/libs/migrate/sql/issue_label/issue_label_up_01.sql +++ /dev/null @@ -1,7 +0,0 @@ -create table if not exists issue_label -( - issue uuid not null, - label bigint not null, - relation_at timestamp with time zone not null, - primary key (issue, label) -); diff --git a/libs/migrate/sql/issue_pull_request/issue_pull_request_down_01.sql b/libs/migrate/sql/issue_pull_request/issue_pull_request_down_01.sql deleted file mode 100644 index 79082e5..0000000 --- a/libs/migrate/sql/issue_pull_request/issue_pull_request_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS issue_pull_request; diff --git a/libs/migrate/sql/issue_pull_request/issue_pull_request_up_01.sql b/libs/migrate/sql/issue_pull_request/issue_pull_request_up_01.sql deleted file mode 100644 index 05d4946..0000000 --- a/libs/migrate/sql/issue_pull_request/issue_pull_request_up_01.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table if not exists issue_pull_request -( - issue uuid not null, - repo uuid not null, - number bigint not null, - relation_at timestamp with time zone not null, - primary key (issue, repo, number) -); diff --git a/libs/migrate/sql/issue_reaction/issue_reaction_down_01.sql b/libs/migrate/sql/issue_reaction/issue_reaction_down_01.sql deleted file mode 100644 index f6193d1..0000000 --- a/libs/migrate/sql/issue_reaction/issue_reaction_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS issue_reaction; diff --git a/libs/migrate/sql/issue_reaction/issue_reaction_up_01.sql b/libs/migrate/sql/issue_reaction/issue_reaction_up_01.sql deleted file mode 100644 index b19a67e..0000000 --- a/libs/migrate/sql/issue_reaction/issue_reaction_up_01.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table if not exists issue_reaction -( - issue_uuid uuid not null, - user_uuid uuid not null, - reaction varchar(255) not null, - created_at timestamp with time zone not null, - primary key (issue_uuid, user_uuid, reaction) -); diff --git a/libs/migrate/sql/issue_repo/issue_repo_down_01.sql b/libs/migrate/sql/issue_repo/issue_repo_down_01.sql deleted file mode 100644 index 4641432..0000000 --- a/libs/migrate/sql/issue_repo/issue_repo_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS issue_repo; diff --git a/libs/migrate/sql/issue_repo/issue_repo_up_01.sql b/libs/migrate/sql/issue_repo/issue_repo_up_01.sql deleted file mode 100644 index cf3e024..0000000 --- a/libs/migrate/sql/issue_repo/issue_repo_up_01.sql +++ /dev/null @@ -1,7 +0,0 @@ -create table if not exists issue_repo -( - issue uuid not null, - repo uuid not null, - relation_at timestamp with time zone not null, - primary key (issue, repo) -); diff --git a/libs/migrate/sql/issue_subscriber/issue_subscriber_down_01.sql b/libs/migrate/sql/issue_subscriber/issue_subscriber_down_01.sql deleted file mode 100644 index 84d835b..0000000 --- a/libs/migrate/sql/issue_subscriber/issue_subscriber_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS issue_subscriber; diff --git a/libs/migrate/sql/issue_subscriber/issue_subscriber_up_01.sql b/libs/migrate/sql/issue_subscriber/issue_subscriber_up_01.sql deleted file mode 100644 index 168b7a0..0000000 --- a/libs/migrate/sql/issue_subscriber/issue_subscriber_up_01.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table if not exists issue_subscriber -( - issue uuid not null, - "user" uuid not null, - subscribed boolean default true not null, - created_at timestamp with time zone not null, - primary key (issue, "user") -); diff --git a/libs/migrate/sql/label/label_down_01.sql b/libs/migrate/sql/label/label_down_01.sql deleted file mode 100644 index 58b4e40..0000000 --- a/libs/migrate/sql/label/label_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_label_project; -DROP TABLE IF EXISTS label; diff --git a/libs/migrate/sql/label/label_up_01.sql b/libs/migrate/sql/label/label_up_01.sql deleted file mode 100644 index f012c9a..0000000 --- a/libs/migrate/sql/label/label_up_01.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table if not exists label -( - id bigserial - primary key, - project_uuid uuid not null, - name varchar(255) not null, - color varchar(255) not null -); - -create index if not exists idx_label_project - on label (project_uuid); diff --git a/libs/migrate/sql/notify/notify_down_01.sql b/libs/migrate/sql/notify/notify_down_01.sql deleted file mode 100644 index 6337ffc..0000000 --- a/libs/migrate/sql/notify/notify_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_notify_created_at; -DROP INDEX IF EXISTS idx_notify_user; -DROP TABLE IF EXISTS notify; diff --git a/libs/migrate/sql/notify/notify_up_01.sql b/libs/migrate/sql/notify/notify_up_01.sql deleted file mode 100644 index 3690c62..0000000 --- a/libs/migrate/sql/notify/notify_up_01.sql +++ /dev/null @@ -1,20 +0,0 @@ -create table if not exists notify -( - id bigserial - primary key, - user_uuid uuid not null, - title varchar(255) not null, - description text, - content text not null, - url varchar(255), - kind integer not null, - read_at timestamp with time zone, - deleted_at timestamp with time zone, - created_at timestamp with time zone not null -); - -create index if not exists idx_notify_user - on notify (user_uuid); - -create index if not exists idx_notify_created_at - on notify (created_at); diff --git a/libs/migrate/sql/project/project_down_01.sql b/libs/migrate/sql/project/project_down_01.sql deleted file mode 100644 index e9e350e..0000000 --- a/libs/migrate/sql/project/project_down_01.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP INDEX IF EXISTS idx_workspace_deleted_at; -DROP INDEX IF EXISTS idx_workspace_slug; -DROP INDEX IF EXISTS idx_project_workspace_id; -DROP INDEX IF EXISTS idx_project_created_by; -DROP INDEX IF EXISTS idx_project_name; -DROP TABLE IF EXISTS project; diff --git a/libs/migrate/sql/project/project_up_01.sql b/libs/migrate/sql/project/project_up_01.sql deleted file mode 100644 index 80aeb86..0000000 --- a/libs/migrate/sql/project/project_up_01.sql +++ /dev/null @@ -1,32 +0,0 @@ -create table if not exists project -( - id uuid not null - primary key, - name varchar(255) not null, - display_name varchar(255) not null, - avatar_url varchar(255), - description text, - is_public boolean default false not null, - created_by uuid not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - workspace_id uuid - references workspace - on delete set null -); - -create index if not exists idx_project_name - on project (name); - -create index if not exists idx_project_created_by - on project (created_by); - -create index if not exists idx_project_workspace_id - on project (workspace_id) - where (workspace_id IS NOT NULL); - -create unique index if not exists idx_workspace_slug - on workspace (slug); - -create index if not exists idx_workspace_deleted_at - on workspace (deleted_at); diff --git a/libs/migrate/sql/project_access_log/project_access_log_down_01.sql b/libs/migrate/sql/project_access_log/project_access_log_down_01.sql deleted file mode 100644 index efc098f..0000000 --- a/libs/migrate/sql/project_access_log/project_access_log_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_project_access_log_created_at; -DROP INDEX IF EXISTS idx_project_access_log_project; -DROP TABLE IF EXISTS project_access_log; diff --git a/libs/migrate/sql/project_access_log/project_access_log_up_01.sql b/libs/migrate/sql/project_access_log/project_access_log_up_01.sql deleted file mode 100644 index e97c3ee..0000000 --- a/libs/migrate/sql/project_access_log/project_access_log_up_01.sql +++ /dev/null @@ -1,17 +0,0 @@ -create table if not exists project_access_log -( - id bigserial - primary key, - project uuid not null, - actor_uid uuid, - action varchar(255) not null, - ip_address varchar(255), - user_agent varchar(255), - created_at timestamp with time zone not null -); - -create index if not exists idx_project_access_log_project - on project_access_log (project); - -create index if not exists idx_project_access_log_created_at - on project_access_log (created_at); diff --git a/libs/migrate/sql/project_activity/project_activity_down_01.sql b/libs/migrate/sql/project_activity/project_activity_down_01.sql deleted file mode 100644 index 3135f5a..0000000 --- a/libs/migrate/sql/project_activity/project_activity_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_project_activity_event_type; -DROP INDEX IF EXISTS idx_project_activity_created_at; -DROP INDEX IF EXISTS idx_project_activity_project; -DROP TABLE IF EXISTS project_activity; diff --git a/libs/migrate/sql/project_activity/project_activity_up_01.sql b/libs/migrate/sql/project_activity/project_activity_up_01.sql deleted file mode 100644 index 4af3537..0000000 --- a/libs/migrate/sql/project_activity/project_activity_up_01.sql +++ /dev/null @@ -1,25 +0,0 @@ -create table if not exists project_activity -( - id bigserial - primary key, - project uuid not null, - repo uuid, - actor uuid not null, - event_type varchar(50) not null, - event_id uuid, - event_sub_id bigint, - title varchar(500) not null, - content text, - metadata jsonb, - is_private boolean default false not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_project_activity_project - on project_activity (project); - -create index if not exists idx_project_activity_created_at - on project_activity (created_at desc); - -create index if not exists idx_project_activity_event_type - on project_activity (event_type); diff --git a/libs/migrate/sql/project_audit_log/project_audit_log_down_01.sql b/libs/migrate/sql/project_audit_log/project_audit_log_down_01.sql deleted file mode 100644 index a98adb5..0000000 --- a/libs/migrate/sql/project_audit_log/project_audit_log_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_project_audit_log_created_at; -DROP INDEX IF EXISTS idx_project_audit_log_project; -DROP TABLE IF EXISTS project_audit_log; diff --git a/libs/migrate/sql/project_audit_log/project_audit_log_up_01.sql b/libs/migrate/sql/project_audit_log/project_audit_log_up_01.sql deleted file mode 100644 index d6d0977..0000000 --- a/libs/migrate/sql/project_audit_log/project_audit_log_up_01.sql +++ /dev/null @@ -1,18 +0,0 @@ -create table if not exists project_audit_log -( - id bigserial - primary key, - project uuid not null, - actor uuid not null, - action text not null, - details jsonb, - ip_address varchar(255), - user_agent varchar(255), - created_at timestamp with time zone not null -); - -create index if not exists idx_project_audit_log_project - on project_audit_log (project); - -create index if not exists idx_project_audit_log_created_at - on project_audit_log (created_at); diff --git a/libs/migrate/sql/project_billing/project_billing_down_01.sql b/libs/migrate/sql/project_billing/project_billing_down_01.sql deleted file mode 100644 index f31bcff..0000000 --- a/libs/migrate/sql/project_billing/project_billing_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS project_billing; diff --git a/libs/migrate/sql/project_billing/project_billing_up_01.sql b/libs/migrate/sql/project_billing/project_billing_up_01.sql deleted file mode 100644 index 4e75b19..0000000 --- a/libs/migrate/sql/project_billing/project_billing_up_01.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table if not exists project_billing -( - project_uuid uuid not null - primary key, - balance numeric default 0.0 not null, - currency text not null, - user_uuid uuid, - updated_at timestamp with time zone not null, - created_at timestamp with time zone not null -); diff --git a/libs/migrate/sql/project_billing_history/project_billing_history_down_01.sql b/libs/migrate/sql/project_billing_history/project_billing_history_down_01.sql deleted file mode 100644 index b6fc8d7..0000000 --- a/libs/migrate/sql/project_billing_history/project_billing_history_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_billing_history_project; -DROP TABLE IF EXISTS project_billing_history; diff --git a/libs/migrate/sql/project_billing_history/project_billing_history_up_01.sql b/libs/migrate/sql/project_billing_history/project_billing_history_up_01.sql deleted file mode 100644 index 2cb02a1..0000000 --- a/libs/migrate/sql/project_billing_history/project_billing_history_up_01.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table if not exists project_billing_history -( - uid uuid not null - primary key, - project uuid not null, - "user" uuid, - amount numeric not null, - currency text not null, - reason text not null, - extra jsonb, - created_at timestamp with time zone not null -); - -create index if not exists idx_project_billing_history_project - on project_billing_history (project); diff --git a/libs/migrate/sql/project_board/project_board_down_01.sql b/libs/migrate/sql/project_board/project_board_down_01.sql deleted file mode 100644 index 286ae7f..0000000 --- a/libs/migrate/sql/project_board/project_board_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_board_project; -DROP TABLE IF EXISTS project_board; diff --git a/libs/migrate/sql/project_board/project_board_up_01.sql b/libs/migrate/sql/project_board/project_board_up_01.sql deleted file mode 100644 index 9091ffe..0000000 --- a/libs/migrate/sql/project_board/project_board_up_01.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table if not exists project_board -( - id uuid default gen_random_uuid() not null - primary key, - project_uuid uuid not null, - name varchar(255) not null, - description text, - created_by uuid not null, - created_at timestamp with time zone default now() not null, - updated_at timestamp with time zone default now() not null -); - -create index if not exists idx_project_board_project - on project_board (project_uuid); diff --git a/libs/migrate/sql/project_board_card/project_board_card_down_01.sql b/libs/migrate/sql/project_board_card/project_board_card_down_01.sql deleted file mode 100644 index 81b3411..0000000 --- a/libs/migrate/sql/project_board_card/project_board_card_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_project_board_card_issue; -DROP INDEX IF EXISTS idx_project_board_card_column; -DROP TABLE IF EXISTS project_board_card; diff --git a/libs/migrate/sql/project_board_card/project_board_card_up_01.sql b/libs/migrate/sql/project_board_card/project_board_card_up_01.sql deleted file mode 100644 index 61e7bfd..0000000 --- a/libs/migrate/sql/project_board_card/project_board_card_up_01.sql +++ /dev/null @@ -1,26 +0,0 @@ -create table if not exists project_board_card -( - id uuid default gen_random_uuid() not null - primary key, - column_uuid uuid not null - references project_board_column - on delete cascade, - issue_id bigint, - project uuid, - title varchar(500) not null, - description text, - position integer default 0 not null, - assignee_id uuid, - due_date timestamp with time zone, - priority varchar(10), - created_by uuid not null, - created_at timestamp with time zone default now() not null, - updated_at timestamp with time zone default now() not null -); - -create index if not exists idx_project_board_card_column - on project_board_card (column_uuid); - -create index if not exists idx_project_board_card_issue - on project_board_card (issue_id) - where (issue_id IS NOT NULL); diff --git a/libs/migrate/sql/project_board_column/project_board_column_down_01.sql b/libs/migrate/sql/project_board_column/project_board_column_down_01.sql deleted file mode 100644 index 2d89265..0000000 --- a/libs/migrate/sql/project_board_column/project_board_column_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_board_column_board; -DROP TABLE IF EXISTS project_board_column; diff --git a/libs/migrate/sql/project_board_column/project_board_column_up_01.sql b/libs/migrate/sql/project_board_column/project_board_column_up_01.sql deleted file mode 100644 index 819ec3b..0000000 --- a/libs/migrate/sql/project_board_column/project_board_column_up_01.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table if not exists project_board_column -( - id uuid default gen_random_uuid() not null - primary key, - board_uuid uuid not null - references project_board - on delete cascade, - name varchar(255) not null, - position integer default 0 not null, - wip_limit integer, - color varchar(20) -); - -create index if not exists idx_project_board_column_board - on project_board_column (board_uuid); diff --git a/libs/migrate/sql/project_follow/project_follow_down_01.sql b/libs/migrate/sql/project_follow/project_follow_down_01.sql deleted file mode 100644 index 7eb9a24..0000000 --- a/libs/migrate/sql/project_follow/project_follow_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_follow_project_user; -DROP TABLE IF EXISTS project_follow; diff --git a/libs/migrate/sql/project_follow/project_follow_up_01.sql b/libs/migrate/sql/project_follow/project_follow_up_01.sql deleted file mode 100644 index 2915f81..0000000 --- a/libs/migrate/sql/project_follow/project_follow_up_01.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table if not exists project_follow -( - id bigserial - primary key, - project uuid not null, - "user" uuid not null, - created_at timestamp with time zone not null, - unique (project, "user") -); - -create unique index if not exists idx_project_follow_project_user - on project_follow (project, "user"); diff --git a/libs/migrate/sql/project_history_name/project_history_name_down_01.sql b/libs/migrate/sql/project_history_name/project_history_name_down_01.sql deleted file mode 100644 index a79f469..0000000 --- a/libs/migrate/sql/project_history_name/project_history_name_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_history_name_project_uid; -DROP TABLE IF EXISTS project_history_name; diff --git a/libs/migrate/sql/project_history_name/project_history_name_up_01.sql b/libs/migrate/sql/project_history_name/project_history_name_up_01.sql deleted file mode 100644 index 6883e68..0000000 --- a/libs/migrate/sql/project_history_name/project_history_name_up_01.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table if not exists project_history_name -( - id bigserial - primary key, - project_uid uuid not null, - history_name varchar(255) not null, - changed_at timestamp with time zone not null -); - -create index if not exists idx_project_history_name_project_uid - on project_history_name (project_uid); diff --git a/libs/migrate/sql/project_label/project_label_down_01.sql b/libs/migrate/sql/project_label/project_label_down_01.sql deleted file mode 100644 index 20ce5c0..0000000 --- a/libs/migrate/sql/project_label/project_label_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_label_project; -DROP TABLE IF EXISTS project_label; diff --git a/libs/migrate/sql/project_label/project_label_up_01.sql b/libs/migrate/sql/project_label/project_label_up_01.sql deleted file mode 100644 index 7813669..0000000 --- a/libs/migrate/sql/project_label/project_label_up_01.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table if not exists project_label -( - id bigserial - primary key, - project_uuid uuid not null, - label_id bigint not null, - relation_at timestamp with time zone not null -); - -create index if not exists idx_project_label_project - on project_label (project_uuid); diff --git a/libs/migrate/sql/project_like/project_like_down_01.sql b/libs/migrate/sql/project_like/project_like_down_01.sql deleted file mode 100644 index 565a95c..0000000 --- a/libs/migrate/sql/project_like/project_like_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS project_like; diff --git a/libs/migrate/sql/project_like/project_like_up_01.sql b/libs/migrate/sql/project_like/project_like_up_01.sql deleted file mode 100644 index e6faf33..0000000 --- a/libs/migrate/sql/project_like/project_like_up_01.sql +++ /dev/null @@ -1,7 +0,0 @@ -create table if not exists project_like -( - project uuid not null, - "user" uuid not null, - created_at timestamp with time zone not null, - primary key (project, "user") -); diff --git a/libs/migrate/sql/project_member_invitations/project_member_invitations_down_01.sql b/libs/migrate/sql/project_member_invitations/project_member_invitations_down_01.sql deleted file mode 100644 index 499ab95..0000000 --- a/libs/migrate/sql/project_member_invitations/project_member_invitations_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_member_invitations_project_user; -DROP TABLE IF EXISTS project_member_invitations; diff --git a/libs/migrate/sql/project_member_invitations/project_member_invitations_up_01.sql b/libs/migrate/sql/project_member_invitations/project_member_invitations_up_01.sql deleted file mode 100644 index 22af1bd..0000000 --- a/libs/migrate/sql/project_member_invitations/project_member_invitations_up_01.sql +++ /dev/null @@ -1,17 +0,0 @@ -create table if not exists project_member_invitations -( - id bigserial - primary key, - project uuid not null, - "user" uuid not null, - invited_by uuid not null, - scope varchar(255) not null, - accepted boolean default false not null, - accepted_at timestamp with time zone, - rejected boolean default false not null, - rejected_at timestamp with time zone, - created_at timestamp with time zone not null -); - -create index if not exists idx_project_member_invitations_project_user - on project_member_invitations (project, "user"); diff --git a/libs/migrate/sql/project_member_join_answers/project_member_join_answers_down_01.sql b/libs/migrate/sql/project_member_join_answers/project_member_join_answers_down_01.sql deleted file mode 100644 index b7b9519..0000000 --- a/libs/migrate/sql/project_member_join_answers/project_member_join_answers_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_member_join_answers_request_id; -DROP TABLE IF EXISTS project_member_join_answers; diff --git a/libs/migrate/sql/project_member_join_answers/project_member_join_answers_up_01.sql b/libs/migrate/sql/project_member_join_answers/project_member_join_answers_up_01.sql deleted file mode 100644 index 6308816..0000000 --- a/libs/migrate/sql/project_member_join_answers/project_member_join_answers_up_01.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table if not exists project_member_join_answers -( - id bigserial - primary key, - project uuid not null, - "user" uuid not null, - request_id bigint not null, - question varchar(255) not null, - answer varchar(255) not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_project_member_join_answers_request_id - on project_member_join_answers (request_id); diff --git a/libs/migrate/sql/project_member_join_request/project_member_join_request_down_01.sql b/libs/migrate/sql/project_member_join_request/project_member_join_request_down_01.sql deleted file mode 100644 index cf6216b..0000000 --- a/libs/migrate/sql/project_member_join_request/project_member_join_request_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_project_member_join_request_status; -DROP INDEX IF EXISTS idx_project_member_join_request_project_user; -DROP TABLE IF EXISTS project_member_join_request; diff --git a/libs/migrate/sql/project_member_join_request/project_member_join_request_up_01.sql b/libs/migrate/sql/project_member_join_request/project_member_join_request_up_01.sql deleted file mode 100644 index c5c7495..0000000 --- a/libs/migrate/sql/project_member_join_request/project_member_join_request_up_01.sql +++ /dev/null @@ -1,20 +0,0 @@ -create table if not exists project_member_join_request -( - id bigserial - primary key, - project uuid not null, - "user" uuid not null, - status varchar(255) not null, - message text, - processed_by uuid, - processed_at timestamp with time zone, - reject_reason text, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); - -create index if not exists idx_project_member_join_request_project_user - on project_member_join_request (project, "user"); - -create index if not exists idx_project_member_join_request_status - on project_member_join_request (status); diff --git a/libs/migrate/sql/project_member_join_settings/project_member_join_settings_down_01.sql b/libs/migrate/sql/project_member_join_settings/project_member_join_settings_down_01.sql deleted file mode 100644 index a116934..0000000 --- a/libs/migrate/sql/project_member_join_settings/project_member_join_settings_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS project_member_join_settings; diff --git a/libs/migrate/sql/project_member_join_settings/project_member_join_settings_up_01.sql b/libs/migrate/sql/project_member_join_settings/project_member_join_settings_up_01.sql deleted file mode 100644 index f2d72a2..0000000 --- a/libs/migrate/sql/project_member_join_settings/project_member_join_settings_up_01.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table if not exists project_member_join_settings -( - id bigserial - primary key, - project uuid not null, - require_approval boolean default false not null, - require_questions boolean default false not null, - questions jsonb not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); diff --git a/libs/migrate/sql/project_members/project_members_down_01.sql b/libs/migrate/sql/project_members/project_members_down_01.sql deleted file mode 100644 index 409ee63..0000000 --- a/libs/migrate/sql/project_members/project_members_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_members_project_user; -DROP TABLE IF EXISTS project_members; diff --git a/libs/migrate/sql/project_members/project_members_up_01.sql b/libs/migrate/sql/project_members/project_members_up_01.sql deleted file mode 100644 index 5b7855c..0000000 --- a/libs/migrate/sql/project_members/project_members_up_01.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table if not exists project_members -( - id bigserial - primary key, - project_uuid uuid not null, - user_uuid uuid not null, - scope varchar(255) not null, - joined_at timestamp with time zone not null, - unique (project_uuid, user_uuid) -); - -create unique index if not exists idx_project_members_project_user - on project_members (project_uuid, user_uuid); diff --git a/libs/migrate/sql/project_message_favorite/project_message_favorite_down_01.sql b/libs/migrate/sql/project_message_favorite/project_message_favorite_down_01.sql deleted file mode 100644 index 2f75fcc..0000000 --- a/libs/migrate/sql/project_message_favorite/project_message_favorite_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_project_message_favorite_room; -DROP INDEX IF EXISTS idx_project_message_favorite_project_user; -DROP INDEX IF EXISTS idx_project_message_favorite_user_message; -DROP TABLE IF EXISTS project_message_favorite; diff --git a/libs/migrate/sql/project_message_favorite/project_message_favorite_up_01.sql b/libs/migrate/sql/project_message_favorite/project_message_favorite_up_01.sql deleted file mode 100644 index 29b45a8..0000000 --- a/libs/migrate/sql/project_message_favorite/project_message_favorite_up_01.sql +++ /dev/null @@ -1,19 +0,0 @@ -create table if not exists project_message_favorite -( - uid uuid not null - primary key, - project uuid not null, - room uuid not null, - message uuid not null, - user_uuid uuid not null, - created_at timestamp with time zone not null -); - -create unique index if not exists idx_project_message_favorite_user_message - on project_message_favorite (user_uuid, message); - -create index if not exists idx_project_message_favorite_project_user - on project_message_favorite (project, user_uuid, created_at desc); - -create index if not exists idx_project_message_favorite_room - on project_message_favorite (room); diff --git a/libs/migrate/sql/project_role_priority/project_role_priority_down_01.sql b/libs/migrate/sql/project_role_priority/project_role_priority_down_01.sql deleted file mode 100644 index 50bedd0..0000000 --- a/libs/migrate/sql/project_role_priority/project_role_priority_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_project_role_priority_priority; -DROP INDEX IF EXISTS idx_project_role_priority_project; -DROP TABLE IF EXISTS project_role_priority; diff --git a/libs/migrate/sql/project_role_priority/project_role_priority_up_01.sql b/libs/migrate/sql/project_role_priority/project_role_priority_up_01.sql deleted file mode 100644 index 131fd7c..0000000 --- a/libs/migrate/sql/project_role_priority/project_role_priority_up_01.sql +++ /dev/null @@ -1,19 +0,0 @@ -create table if not exists project_role_priority -( - id bigserial - primary key, - project_uuid uuid not null, - role_key varchar(64) not null, - display_name varchar(128) not null, - priority integer default 0 not null, - color varchar(32), - created_at timestamp with time zone default now(), - updated_at timestamp with time zone default now(), - unique (project_uuid, role_key) -); - -create index if not exists idx_project_role_priority_project - on project_role_priority (project_uuid); - -create index if not exists idx_project_role_priority_priority - on project_role_priority (project_uuid, priority); diff --git a/libs/migrate/sql/project_skill/project_skill_down_01.sql b/libs/migrate/sql/project_skill/project_skill_down_01.sql deleted file mode 100644 index 8c01613..0000000 --- a/libs/migrate/sql/project_skill/project_skill_down_01.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP INDEX IF EXISTS idx_project_skill_blob_hash; -DROP INDEX IF EXISTS idx_project_skill_commit_sha; -DROP INDEX IF EXISTS idx_project_skill_source; -DROP INDEX IF EXISTS idx_project_skill_slug; -DROP INDEX IF EXISTS idx_project_skill_project; -DROP TABLE IF EXISTS project_skill; diff --git a/libs/migrate/sql/project_skill/project_skill_up_01.sql b/libs/migrate/sql/project_skill/project_skill_up_01.sql deleted file mode 100644 index 0642c6b..0000000 --- a/libs/migrate/sql/project_skill/project_skill_up_01.sql +++ /dev/null @@ -1,35 +0,0 @@ -create table if not exists project_skill -( - id bigserial - primary key, - project_uuid uuid not null, - slug varchar(255) not null, - name varchar(255) not null, - description text, - source varchar(20) default 'manual'::character varying not null, - repo_id uuid, - content text default ''::text not null, - metadata jsonb default '{}'::jsonb not null, - enabled boolean default true not null, - created_by uuid, - created_at timestamp with time zone default now() not null, - updated_at timestamp with time zone default now() not null, - commit_sha varchar(40), - blob_hash varchar(40), - unique (project_uuid, slug) -); - -create index if not exists idx_project_skill_project - on project_skill (project_uuid); - -create index if not exists idx_project_skill_slug - on project_skill (slug); - -create index if not exists idx_project_skill_source - on project_skill (source); - -create index if not exists idx_project_skill_commit_sha - on project_skill (commit_sha); - -create index if not exists idx_project_skill_blob_hash - on project_skill (blob_hash); diff --git a/libs/migrate/sql/project_watch/project_watch_down_01.sql b/libs/migrate/sql/project_watch/project_watch_down_01.sql deleted file mode 100644 index 0f0b198..0000000 --- a/libs/migrate/sql/project_watch/project_watch_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_project_watch_project_user; -DROP TABLE IF EXISTS project_watch; diff --git a/libs/migrate/sql/project_watch/project_watch_up_01.sql b/libs/migrate/sql/project_watch/project_watch_up_01.sql deleted file mode 100644 index 2330c80..0000000 --- a/libs/migrate/sql/project_watch/project_watch_up_01.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table if not exists project_watch -( - id bigserial - primary key, - project uuid not null, - "user" uuid not null, - notifications_enabled boolean default true not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - unique (project, "user") -); - -create unique index if not exists idx_project_watch_project_user - on project_watch (project, "user"); diff --git a/libs/migrate/sql/pull_request/pull_request_down_01.sql b/libs/migrate/sql/pull_request/pull_request_down_01.sql deleted file mode 100644 index c064916..0000000 --- a/libs/migrate/sql/pull_request/pull_request_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_pull_request_status; -DROP INDEX IF EXISTS idx_pull_request_author; -DROP INDEX IF EXISTS idx_pull_request_repo; -DROP TABLE IF EXISTS pull_request; diff --git a/libs/migrate/sql/pull_request/pull_request_up_01.sql b/libs/migrate/sql/pull_request/pull_request_up_01.sql deleted file mode 100644 index fd78d1f..0000000 --- a/libs/migrate/sql/pull_request/pull_request_up_01.sql +++ /dev/null @@ -1,27 +0,0 @@ -create table if not exists pull_request -( - repo uuid not null, - number bigint not null, - issue uuid not null, - title varchar(255) not null, - body text, - author uuid not null, - base varchar(255) not null, - head varchar(255) not null, - status varchar(255) not null, - merged_by uuid, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - merged_at timestamp with time zone, - created_by_ai boolean default false not null, - primary key (repo, number) -); - -create index if not exists idx_pull_request_repo - on pull_request (repo); - -create index if not exists idx_pull_request_author - on pull_request (author); - -create index if not exists idx_pull_request_status - on pull_request (status); diff --git a/libs/migrate/sql/pull_request_commit/pull_request_commit_down_01.sql b/libs/migrate/sql/pull_request_commit/pull_request_commit_down_01.sql deleted file mode 100644 index c98c3b5..0000000 --- a/libs/migrate/sql/pull_request_commit/pull_request_commit_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS pull_request_commit; diff --git a/libs/migrate/sql/pull_request_commit/pull_request_commit_up_01.sql b/libs/migrate/sql/pull_request_commit/pull_request_commit_up_01.sql deleted file mode 100644 index 3848b05..0000000 --- a/libs/migrate/sql/pull_request_commit/pull_request_commit_up_01.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table if not exists pull_request_commit -( - repo uuid not null, - number bigint not null, - commit varchar(255) not null, - message text not null, - author_name varchar(255) not null, - author_email varchar(255) not null, - authored_at timestamp with time zone not null, - committer_name varchar(255) not null, - committer_email varchar(255) not null, - committed_at timestamp with time zone not null, - created_at timestamp with time zone not null, - primary key (repo, number, commit) -); diff --git a/libs/migrate/sql/pull_request_review/pull_request_review_down_01.sql b/libs/migrate/sql/pull_request_review/pull_request_review_down_01.sql deleted file mode 100644 index abc5156..0000000 --- a/libs/migrate/sql/pull_request_review/pull_request_review_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS pull_request_review; diff --git a/libs/migrate/sql/pull_request_review/pull_request_review_up_01.sql b/libs/migrate/sql/pull_request_review/pull_request_review_up_01.sql deleted file mode 100644 index 2607b57..0000000 --- a/libs/migrate/sql/pull_request_review/pull_request_review_up_01.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table if not exists pull_request_review -( - repo uuid not null, - number bigint not null, - reviewer uuid not null, - state varchar(255) not null, - body text, - submitted_at timestamp with time zone, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - primary key (repo, number, reviewer) -); diff --git a/libs/migrate/sql/pull_request_review_comment/pull_request_review_comment_down_01.sql b/libs/migrate/sql/pull_request_review_comment/pull_request_review_comment_down_01.sql deleted file mode 100644 index 6148d86..0000000 --- a/libs/migrate/sql/pull_request_review_comment/pull_request_review_comment_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS pull_request_review_comment; diff --git a/libs/migrate/sql/pull_request_review_comment/pull_request_review_comment_up_01.sql b/libs/migrate/sql/pull_request_review_comment/pull_request_review_comment_up_01.sql deleted file mode 100644 index 471a92b..0000000 --- a/libs/migrate/sql/pull_request_review_comment/pull_request_review_comment_up_01.sql +++ /dev/null @@ -1,18 +0,0 @@ -create table if not exists pull_request_review_comment -( - repo uuid not null, - number bigint not null, - id bigint not null, - review uuid, - path text, - side varchar(255), - line bigint, - old_line bigint, - body text not null, - author uuid not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - resolved boolean default false not null, - in_reply_to bigint, - primary key (repo, number, id) -); diff --git a/libs/migrate/sql/pull_request_review_request/pull_request_review_request_down_01.sql b/libs/migrate/sql/pull_request_review_request/pull_request_review_request_down_01.sql deleted file mode 100644 index e9b8808..0000000 --- a/libs/migrate/sql/pull_request_review_request/pull_request_review_request_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS pull_request_review_request; diff --git a/libs/migrate/sql/pull_request_review_request/pull_request_review_request_up_01.sql b/libs/migrate/sql/pull_request_review_request/pull_request_review_request_up_01.sql deleted file mode 100644 index ecb1bc1..0000000 --- a/libs/migrate/sql/pull_request_review_request/pull_request_review_request_up_01.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table if not exists pull_request_review_request -( - repo uuid not null, - number bigint not null, - reviewer uuid not null, - requested_by uuid not null, - requested_at timestamp with time zone default now() not null, - dismissed_at timestamp with time zone, - dismissed_by uuid, - primary key (repo, number, reviewer) -); diff --git a/libs/migrate/sql/repo/repo_down_01.sql b/libs/migrate/sql/repo/repo_down_01.sql deleted file mode 100644 index 528631d..0000000 --- a/libs/migrate/sql/repo/repo_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_repo_name; -DROP INDEX IF EXISTS idx_repo_project; -DROP TABLE IF EXISTS repo; diff --git a/libs/migrate/sql/repo/repo_up_01.sql b/libs/migrate/sql/repo/repo_up_01.sql deleted file mode 100644 index c1a16a6..0000000 --- a/libs/migrate/sql/repo/repo_up_01.sql +++ /dev/null @@ -1,21 +0,0 @@ -create table if not exists repo -( - id uuid not null - primary key, - repo_name varchar(255) not null, - project uuid not null, - description text, - default_branch varchar(255) not null, - is_private boolean default false not null, - storage_path varchar(255) not null, - created_by uuid not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - ai_code_review_enabled boolean default false not null -); - -create index if not exists idx_repo_project - on repo (project); - -create index if not exists idx_repo_repo_name - on repo (repo_name); diff --git a/libs/migrate/sql/repo_branch/repo_branch_down_01.sql b/libs/migrate/sql/repo_branch/repo_branch_down_01.sql deleted file mode 100644 index fee65ce..0000000 --- a/libs/migrate/sql/repo_branch/repo_branch_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_branch_repo; -DROP TABLE IF EXISTS repo_branch; diff --git a/libs/migrate/sql/repo_branch/repo_branch_up_01.sql b/libs/migrate/sql/repo_branch/repo_branch_up_01.sql deleted file mode 100644 index 977fe4d..0000000 --- a/libs/migrate/sql/repo_branch/repo_branch_up_01.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table if not exists repo_branch -( - repo uuid not null, - name varchar(255) not null, - oid varchar(255) not null, - upstream varchar(255), - head boolean default false not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - primary key (repo, name) -); - -create index if not exists idx_repo_branch_repo - on repo_branch (repo); diff --git a/libs/migrate/sql/repo_branch_protect/repo_branch_protect_down_01.sql b/libs/migrate/sql/repo_branch_protect/repo_branch_protect_down_01.sql deleted file mode 100644 index 0d6a065..0000000 --- a/libs/migrate/sql/repo_branch_protect/repo_branch_protect_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_branch_protect_repo_branch; -DROP TABLE IF EXISTS repo_branch_protect; diff --git a/libs/migrate/sql/repo_branch_protect/repo_branch_protect_up_01.sql b/libs/migrate/sql/repo_branch_protect/repo_branch_protect_up_01.sql deleted file mode 100644 index b68eaeb..0000000 --- a/libs/migrate/sql/repo_branch_protect/repo_branch_protect_up_01.sql +++ /dev/null @@ -1,21 +0,0 @@ -create table if not exists repo_branch_protect -( - id bigserial - primary key, - repo_uuid uuid not null, - branch varchar(255) not null, - forbid_push boolean default false not null, - forbid_pull boolean default false not null, - forbid_merge boolean default false not null, - forbid_deletion boolean default false not null, - forbid_force_push boolean default false not null, - forbid_tag_push boolean default false not null, - required_approvals integer default 0 not null, - dismiss_stale_reviews boolean default false not null, - require_linear_history boolean default false not null, - allow_fork_syncing boolean default true not null, - unique (repo_uuid, branch) -); - -create unique index if not exists idx_repo_branch_protect_repo_branch - on repo_branch_protect (repo_uuid, branch); diff --git a/libs/migrate/sql/repo_collaborator/repo_collaborator_down_01.sql b/libs/migrate/sql/repo_collaborator/repo_collaborator_down_01.sql deleted file mode 100644 index 00f557d..0000000 --- a/libs/migrate/sql/repo_collaborator/repo_collaborator_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS repo_collaborator; diff --git a/libs/migrate/sql/repo_collaborator/repo_collaborator_up_01.sql b/libs/migrate/sql/repo_collaborator/repo_collaborator_up_01.sql deleted file mode 100644 index e8c8293..0000000 --- a/libs/migrate/sql/repo_collaborator/repo_collaborator_up_01.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table if not exists repo_collaborator -( - repo uuid not null, - "user" uuid not null, - scope varchar(255) not null, - created_at timestamp with time zone not null, - primary key (repo, "user") -); diff --git a/libs/migrate/sql/repo_commit/repo_commit_down_01.sql b/libs/migrate/sql/repo_commit/repo_commit_down_01.sql deleted file mode 100644 index d6bfd58..0000000 --- a/libs/migrate/sql/repo_commit/repo_commit_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_commit_oid; -DROP INDEX IF EXISTS idx_repo_commit_repo; -DROP TABLE IF EXISTS repo_commit; diff --git a/libs/migrate/sql/repo_commit/repo_commit_up_01.sql b/libs/migrate/sql/repo_commit/repo_commit_up_01.sql deleted file mode 100644 index fe18008..0000000 --- a/libs/migrate/sql/repo_commit/repo_commit_up_01.sql +++ /dev/null @@ -1,22 +0,0 @@ -create table if not exists repo_commit -( - id bigserial - primary key, - repo uuid not null, - oid varchar(255) not null, - author_name varchar(255) not null, - author_email varchar(255) not null, - author uuid, - commiter_name varchar(255) not null, - commiter_email varchar(255) not null, - commiter uuid, - message text not null, - parent jsonb not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_repo_commit_repo - on repo_commit (repo); - -create index if not exists idx_repo_commit_oid - on repo_commit (oid); diff --git a/libs/migrate/sql/repo_fork/repo_fork_down_01.sql b/libs/migrate/sql/repo_fork/repo_fork_down_01.sql deleted file mode 100644 index ac88101..0000000 --- a/libs/migrate/sql/repo_fork/repo_fork_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_fork_forked_repo; -DROP INDEX IF EXISTS idx_repo_fork_parent_repo; -DROP TABLE IF EXISTS repo_fork; diff --git a/libs/migrate/sql/repo_fork/repo_fork_up_01.sql b/libs/migrate/sql/repo_fork/repo_fork_up_01.sql deleted file mode 100644 index 2a3b2ef..0000000 --- a/libs/migrate/sql/repo_fork/repo_fork_up_01.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table if not exists repo_fork -( - id bigserial - primary key, - parent_repo uuid not null, - forked_repo uuid not null, - forked_by uuid not null, - forked_at timestamp with time zone not null -); - -create index if not exists idx_repo_fork_parent_repo - on repo_fork (parent_repo); - -create unique index if not exists idx_repo_fork_forked_repo - on repo_fork (forked_repo); diff --git a/libs/migrate/sql/repo_history_name/repo_history_name_down_01.sql b/libs/migrate/sql/repo_history_name/repo_history_name_down_01.sql deleted file mode 100644 index 7629167..0000000 --- a/libs/migrate/sql/repo_history_name/repo_history_name_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_history_name_repo; -DROP TABLE IF EXISTS repo_history_name; diff --git a/libs/migrate/sql/repo_history_name/repo_history_name_up_01.sql b/libs/migrate/sql/repo_history_name/repo_history_name_up_01.sql deleted file mode 100644 index 91fe282..0000000 --- a/libs/migrate/sql/repo_history_name/repo_history_name_up_01.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table if not exists repo_history_name -( - id bigserial - primary key, - repo_uuid uuid not null, - project_uid uuid not null, - name varchar(255) not null, - change_at timestamp with time zone not null -); - -create index if not exists idx_repo_history_name_repo - on repo_history_name (repo_uuid); diff --git a/libs/migrate/sql/repo_hook/repo_hook_down_01.sql b/libs/migrate/sql/repo_hook/repo_hook_down_01.sql deleted file mode 100644 index b3a3c9a..0000000 --- a/libs/migrate/sql/repo_hook/repo_hook_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_hook_repo; -DROP TABLE IF EXISTS repo_hook; diff --git a/libs/migrate/sql/repo_hook/repo_hook_up_01.sql b/libs/migrate/sql/repo_hook/repo_hook_up_01.sql deleted file mode 100644 index 60e2740..0000000 --- a/libs/migrate/sql/repo_hook/repo_hook_up_01.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table if not exists repo_hook -( - id bigserial - primary key, - repo_uuid uuid not null, - event jsonb not null, - script text not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_repo_hook_repo - on repo_hook (repo_uuid); diff --git a/libs/migrate/sql/repo_lfs_lock/repo_lfs_lock_down_01.sql b/libs/migrate/sql/repo_lfs_lock/repo_lfs_lock_down_01.sql deleted file mode 100644 index 4a31255..0000000 --- a/libs/migrate/sql/repo_lfs_lock/repo_lfs_lock_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS repo_lfs_lock; diff --git a/libs/migrate/sql/repo_lfs_lock/repo_lfs_lock_up_01.sql b/libs/migrate/sql/repo_lfs_lock/repo_lfs_lock_up_01.sql deleted file mode 100644 index 81ffb7b..0000000 --- a/libs/migrate/sql/repo_lfs_lock/repo_lfs_lock_up_01.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table if not exists repo_lfs_lock -( - repo_uuid uuid not null, - path varchar(255) not null, - lock_type varchar(255) not null, - locked_by uuid not null, - locked_at timestamp with time zone not null, - unlocked_at timestamp with time zone, - primary key (repo_uuid, path) -); diff --git a/libs/migrate/sql/repo_lfs_object/repo_lfs_object_down_01.sql b/libs/migrate/sql/repo_lfs_object/repo_lfs_object_down_01.sql deleted file mode 100644 index c16f140..0000000 --- a/libs/migrate/sql/repo_lfs_object/repo_lfs_object_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_lfs_object_repo_oid; -DROP TABLE IF EXISTS repo_lfs_object; diff --git a/libs/migrate/sql/repo_lfs_object/repo_lfs_object_up_01.sql b/libs/migrate/sql/repo_lfs_object/repo_lfs_object_up_01.sql deleted file mode 100644 index 5fee181..0000000 --- a/libs/migrate/sql/repo_lfs_object/repo_lfs_object_up_01.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table if not exists repo_lfs_object -( - id bigserial - primary key, - oid varchar(255) not null, - repo_uuid uuid not null, - size bigint not null, - storage_path varchar(255) not null, - uploaded_by uuid, - uploaded_at timestamp with time zone not null -); - -create index if not exists idx_repo_lfs_object_repo_oid - on repo_lfs_object (repo_uuid, oid); diff --git a/libs/migrate/sql/repo_lock/repo_lock_down_01.sql b/libs/migrate/sql/repo_lock/repo_lock_down_01.sql deleted file mode 100644 index cd0322e..0000000 --- a/libs/migrate/sql/repo_lock/repo_lock_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS repo_lock; diff --git a/libs/migrate/sql/repo_lock/repo_lock_up_01.sql b/libs/migrate/sql/repo_lock/repo_lock_up_01.sql deleted file mode 100644 index 9943ef5..0000000 --- a/libs/migrate/sql/repo_lock/repo_lock_up_01.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table if not exists repo_lock -( - repo_uuid uuid not null, - path varchar(255) not null, - lock_type varchar(255) not null, - locked_by uuid not null, - acquired_at timestamp with time zone not null, - released_at timestamp with time zone, - primary key (repo_uuid, path) -); diff --git a/libs/migrate/sql/repo_star/repo_star_down_01.sql b/libs/migrate/sql/repo_star/repo_star_down_01.sql deleted file mode 100644 index 1487f57..0000000 --- a/libs/migrate/sql/repo_star/repo_star_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_star_repo_user; -DROP TABLE IF EXISTS repo_star; diff --git a/libs/migrate/sql/repo_star/repo_star_up_01.sql b/libs/migrate/sql/repo_star/repo_star_up_01.sql deleted file mode 100644 index acf5363..0000000 --- a/libs/migrate/sql/repo_star/repo_star_up_01.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table if not exists repo_star -( - id bigserial - primary key, - repo_uuid uuid not null, - user_uuid uuid not null, - created_at timestamp with time zone not null, - unique (repo_uuid, user_uuid) -); - -create unique index if not exists idx_repo_star_repo_user - on repo_star (repo_uuid, user_uuid); diff --git a/libs/migrate/sql/repo_tag/repo_tag_down_01.sql b/libs/migrate/sql/repo_tag/repo_tag_down_01.sql deleted file mode 100644 index e4c5fa1..0000000 --- a/libs/migrate/sql/repo_tag/repo_tag_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS repo_tag; diff --git a/libs/migrate/sql/repo_tag/repo_tag_up_01.sql b/libs/migrate/sql/repo_tag/repo_tag_up_01.sql deleted file mode 100644 index 53e9dc8..0000000 --- a/libs/migrate/sql/repo_tag/repo_tag_up_01.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table if not exists repo_tag -( - repo_uuid uuid not null, - name varchar(255) not null, - oid varchar(255) not null, - color varchar(255), - description text, - created_at timestamp with time zone not null, - tagger_name varchar(255) not null, - tagger_email varchar(255) not null, - tagger_uuid uuid, - primary key (repo_uuid, name) -); diff --git a/libs/migrate/sql/repo_upstream/repo_upstream_down_01.sql b/libs/migrate/sql/repo_upstream/repo_upstream_down_01.sql deleted file mode 100644 index e21122d..0000000 --- a/libs/migrate/sql/repo_upstream/repo_upstream_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_upstream_repo; -DROP TABLE IF EXISTS repo_upstream; diff --git a/libs/migrate/sql/repo_upstream/repo_upstream_up_01.sql b/libs/migrate/sql/repo_upstream/repo_upstream_up_01.sql deleted file mode 100644 index 01436f7..0000000 --- a/libs/migrate/sql/repo_upstream/repo_upstream_up_01.sql +++ /dev/null @@ -1,18 +0,0 @@ -create table if not exists repo_upstream -( - id bigserial - primary key, - repo_uuid uuid not null - unique, - source_url varchar(255) not null, - direction varchar(255) not null, - schedule_cron varchar(255), - last_run_at timestamp with time zone, - next_run_at timestamp with time zone, - status varchar(255) not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); - -create unique index if not exists idx_repo_upstream_repo - on repo_upstream (repo_uuid); diff --git a/libs/migrate/sql/repo_watch/repo_watch_down_01.sql b/libs/migrate/sql/repo_watch/repo_watch_down_01.sql deleted file mode 100644 index 86056bd..0000000 --- a/libs/migrate/sql/repo_watch/repo_watch_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_watch_user_repo; -DROP TABLE IF EXISTS repo_watch; diff --git a/libs/migrate/sql/repo_watch/repo_watch_up_01.sql b/libs/migrate/sql/repo_watch/repo_watch_up_01.sql deleted file mode 100644 index a1ed147..0000000 --- a/libs/migrate/sql/repo_watch/repo_watch_up_01.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table if not exists repo_watch -( - id bigserial - primary key, - user_uuid uuid not null, - repo_uuid uuid not null, - show_dashboard boolean default false not null, - notify_email boolean default false not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - unique (user_uuid, repo_uuid) -); - -create unique index if not exists idx_repo_watch_user_repo - on repo_watch (user_uuid, repo_uuid); diff --git a/libs/migrate/sql/repo_webhook/repo_webhook_down_01.sql b/libs/migrate/sql/repo_webhook/repo_webhook_down_01.sql deleted file mode 100644 index 4796c2e..0000000 --- a/libs/migrate/sql/repo_webhook/repo_webhook_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_repo_webhook_repo; -DROP TABLE IF EXISTS repo_webhook; diff --git a/libs/migrate/sql/repo_webhook/repo_webhook_up_01.sql b/libs/migrate/sql/repo_webhook/repo_webhook_up_01.sql deleted file mode 100644 index 8ab1cf6..0000000 --- a/libs/migrate/sql/repo_webhook/repo_webhook_up_01.sql +++ /dev/null @@ -1,16 +0,0 @@ -create table if not exists repo_webhook -( - id bigserial - primary key, - repo_uuid uuid not null, - event jsonb not null, - url varchar(255), - access_key varchar(255), - secret_key varchar(255), - created_at timestamp with time zone not null, - last_delivered_at timestamp with time zone, - touch_count bigint default 0 not null -); - -create index if not exists idx_repo_webhook_repo - on repo_webhook (repo_uuid); diff --git a/libs/migrate/sql/room/room_down_01.sql b/libs/migrate/sql/room/room_down_01.sql deleted file mode 100644 index 654bf1d..0000000 --- a/libs/migrate/sql/room/room_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_room_category; -DROP INDEX IF EXISTS idx_room_project; -DROP TABLE IF EXISTS room; diff --git a/libs/migrate/sql/room/room_up_01.sql b/libs/migrate/sql/room/room_up_01.sql deleted file mode 100644 index 2043a27..0000000 --- a/libs/migrate/sql/room/room_up_01.sql +++ /dev/null @@ -1,18 +0,0 @@ -create table if not exists room -( - id uuid not null - primary key, - project uuid not null, - room_name varchar(255) not null, - public boolean default false not null, - category uuid, - created_by uuid not null, - created_at timestamp with time zone not null, - last_msg_at timestamp with time zone not null -); - -create index if not exists idx_room_project - on room (project); - -create index if not exists idx_room_category - on room (category); diff --git a/libs/migrate/sql/room_access/room_access_down_01.sql b/libs/migrate/sql/room_access/room_access_down_01.sql deleted file mode 100644 index e25b2c7..0000000 --- a/libs/migrate/sql/room_access/room_access_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_room_access_user; -DROP TABLE IF EXISTS room_access; diff --git a/libs/migrate/sql/room_access/room_access_up_01.sql b/libs/migrate/sql/room_access/room_access_up_01.sql deleted file mode 100644 index 89e0d32..0000000 --- a/libs/migrate/sql/room_access/room_access_up_01.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table if not exists room_access -( - room uuid not null, - "user" uuid not null, - granted_by uuid not null, - granted_at timestamp with time zone default now() not null, - primary key (room, "user") -); - -create index if not exists idx_room_access_user - on room_access ("user"); diff --git a/libs/migrate/sql/room_ai/room_ai_down_01.sql b/libs/migrate/sql/room_ai/room_ai_down_01.sql deleted file mode 100644 index 08e9509..0000000 --- a/libs/migrate/sql/room_ai/room_ai_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_room_ai_agent_type; -DROP TABLE IF EXISTS room_ai; diff --git a/libs/migrate/sql/room_ai/room_ai_up_01.sql b/libs/migrate/sql/room_ai/room_ai_up_01.sql deleted file mode 100644 index ec35d93..0000000 --- a/libs/migrate/sql/room_ai/room_ai_up_01.sql +++ /dev/null @@ -1,24 +0,0 @@ -create table if not exists room_ai -( - room uuid not null, - model uuid not null, - version uuid, - call_count bigint default 0 not null, - last_call_at timestamp with time zone, - history_limit bigint, - system_prompt text, - temperature double precision, - max_tokens bigint, - use_exact boolean default false not null, - think boolean default false not null, - min_score real, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - stream boolean default true not null, - agent_type varchar(50), - primary key (room, model) -); - -create index if not exists idx_room_ai_agent_type - on room_ai (agent_type) - where (agent_type IS NOT NULL); diff --git a/libs/migrate/sql/room_attachment/room_attachment_down_01.sql b/libs/migrate/sql/room_attachment/room_attachment_down_01.sql deleted file mode 100644 index f1a4cf4..0000000 --- a/libs/migrate/sql/room_attachment/room_attachment_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_room_attachment_uploader; -DROP INDEX IF EXISTS idx_room_attachment_message; -DROP INDEX IF EXISTS idx_room_attachment_room; -DROP TABLE IF EXISTS room_attachment; diff --git a/libs/migrate/sql/room_attachment/room_attachment_up_01.sql b/libs/migrate/sql/room_attachment/room_attachment_up_01.sql deleted file mode 100644 index 4512737..0000000 --- a/libs/migrate/sql/room_attachment/room_attachment_up_01.sql +++ /dev/null @@ -1,22 +0,0 @@ -create table if not exists room_attachment -( - id uuid not null - primary key, - room uuid not null, - message uuid not null, - uploader uuid not null, - file_name varchar(255) not null, - file_size bigint not null, - content_type varchar(100) not null, - s3_key varchar(500) not null, - created_at timestamp with time zone default now() not null -); - -create index if not exists idx_room_attachment_room - on room_attachment (room); - -create index if not exists idx_room_attachment_message - on room_attachment (message); - -create index if not exists idx_room_attachment_uploader - on room_attachment (uploader); diff --git a/libs/migrate/sql/room_category/room_category_down_01.sql b/libs/migrate/sql/room_category/room_category_down_01.sql deleted file mode 100644 index 3da6ce3..0000000 --- a/libs/migrate/sql/room_category/room_category_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_room_category_project; -DROP TABLE IF EXISTS room_category; diff --git a/libs/migrate/sql/room_category/room_category_up_01.sql b/libs/migrate/sql/room_category/room_category_up_01.sql deleted file mode 100644 index 3d03340..0000000 --- a/libs/migrate/sql/room_category/room_category_up_01.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table if not exists room_category -( - id uuid not null - primary key, - project_uuid uuid not null, - name varchar(255) not null, - position integer not null, - created_by uuid not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_room_category_project - on room_category (project_uuid); diff --git a/libs/migrate/sql/room_compact_summary/room_compact_summary_down_01.sql b/libs/migrate/sql/room_compact_summary/room_compact_summary_down_01.sql deleted file mode 100644 index 302fdb3..0000000 --- a/libs/migrate/sql/room_compact_summary/room_compact_summary_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_room_compact_summary_room_to_seq; -DROP TABLE IF EXISTS room_compact_summary; diff --git a/libs/migrate/sql/room_compact_summary/room_compact_summary_up_01.sql b/libs/migrate/sql/room_compact_summary/room_compact_summary_up_01.sql deleted file mode 100644 index 51dbcad..0000000 --- a/libs/migrate/sql/room_compact_summary/room_compact_summary_up_01.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS room_compact_summary -( - id uuid NOT NULL - PRIMARY KEY, - room uuid NOT NULL, - from_seq bigint NOT NULL, - to_seq bigint NOT NULL, - summary text NOT NULL, - message_count integer NOT NULL, - source_message_ids jsonb DEFAULT '[]'::jsonb NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL -); - -CREATE INDEX IF NOT EXISTS idx_room_compact_summary_room_to_seq - ON room_compact_summary (room, to_seq DESC); diff --git a/libs/migrate/sql/room_message/room_message_down_01.sql b/libs/migrate/sql/room_message/room_message_down_01.sql deleted file mode 100644 index c16b11f..0000000 --- a/libs/migrate/sql/room_message/room_message_down_01.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP INDEX IF EXISTS idx_room_message_model_id; -DROP INDEX IF EXISTS idx_room_message_content_tsv; -DROP INDEX IF EXISTS idx_room_message_send_at; -DROP INDEX IF EXISTS idx_room_message_thread; -DROP INDEX IF EXISTS idx_room_message_room_seq; -DROP TABLE IF EXISTS room_message; diff --git a/libs/migrate/sql/room_message/room_message_up_01.sql b/libs/migrate/sql/room_message/room_message_up_01.sql deleted file mode 100644 index 0440af6..0000000 --- a/libs/migrate/sql/room_message/room_message_up_01.sql +++ /dev/null @@ -1,36 +0,0 @@ -create table if not exists room_message -( - id uuid not null - primary key, - seq bigint not null, - room uuid not null, - sender_type varchar(255) not null, - sender_id uuid, - thread uuid, - content text not null, - content_type varchar(255) not null, - edited_at timestamp with time zone, - send_at timestamp with time zone not null, - revoked timestamp with time zone, - revoked_by uuid, - in_reply_to uuid, - content_tsv tsvector, - model_id uuid, - thinking_content text -); - -create index if not exists idx_room_message_room_seq - on room_message (room, seq); - -create index if not exists idx_room_message_thread - on room_message (thread); - -create index if not exists idx_room_message_send_at - on room_message (send_at); - -create index if not exists idx_room_message_content_tsv - on room_message using gin (content_tsv); - -create index if not exists idx_room_message_model_id - on room_message (model_id) - where (model_id IS NOT NULL); diff --git a/libs/migrate/sql/room_message_edit_history/room_message_edit_history_down_01.sql b/libs/migrate/sql/room_message_edit_history/room_message_edit_history_down_01.sql deleted file mode 100644 index 5cc7622..0000000 --- a/libs/migrate/sql/room_message_edit_history/room_message_edit_history_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_room_message_edit_history_user; -DROP INDEX IF EXISTS idx_room_message_edit_history_message; -DROP TABLE IF EXISTS room_message_edit_history; diff --git a/libs/migrate/sql/room_message_edit_history/room_message_edit_history_up_01.sql b/libs/migrate/sql/room_message_edit_history/room_message_edit_history_up_01.sql deleted file mode 100644 index 3804fc2..0000000 --- a/libs/migrate/sql/room_message_edit_history/room_message_edit_history_up_01.sql +++ /dev/null @@ -1,20 +0,0 @@ -create table if not exists room_message_edit_history -( - id uuid not null - primary key, - message uuid not null - references room_message - on delete cascade, - "user" uuid not null - references "user" - on delete cascade, - old_content text not null, - new_content text not null, - edited_at timestamp with time zone default now() not null -); - -create index if not exists idx_room_message_edit_history_message - on room_message_edit_history (message); - -create index if not exists idx_room_message_edit_history_user - on room_message_edit_history ("user"); diff --git a/libs/migrate/sql/room_message_reaction/room_message_reaction_down_01.sql b/libs/migrate/sql/room_message_reaction/room_message_reaction_down_01.sql deleted file mode 100644 index 7eab489..0000000 --- a/libs/migrate/sql/room_message_reaction/room_message_reaction_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_room_message_reaction_room; -DROP INDEX IF EXISTS idx_room_message_reaction_user; -DROP INDEX IF EXISTS idx_room_message_reaction_message; -DROP TABLE IF EXISTS room_message_reaction; diff --git a/libs/migrate/sql/room_message_reaction/room_message_reaction_up_01.sql b/libs/migrate/sql/room_message_reaction/room_message_reaction_up_01.sql deleted file mode 100644 index 58bbe2b..0000000 --- a/libs/migrate/sql/room_message_reaction/room_message_reaction_up_01.sql +++ /dev/null @@ -1,26 +0,0 @@ -create table if not exists room_message_reaction -( - id uuid not null - primary key, - room uuid not null - references room - on delete cascade, - message uuid not null - references room_message - on delete cascade, - "user" uuid not null - references "user" - on delete cascade, - emoji varchar(50) not null, - created_at timestamp with time zone default now() not null, - unique (message, "user", emoji) -); - -create index if not exists idx_room_message_reaction_message - on room_message_reaction (message); - -create index if not exists idx_room_message_reaction_user - on room_message_reaction ("user"); - -create index if not exists idx_room_message_reaction_room - on room_message_reaction (room); diff --git a/libs/migrate/sql/room_notifications/room_notifications_down_01.sql b/libs/migrate/sql/room_notifications/room_notifications_down_01.sql deleted file mode 100644 index 0ce0eb4..0000000 --- a/libs/migrate/sql/room_notifications/room_notifications_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_room_notifications_expires_at; -DROP INDEX IF EXISTS idx_room_notifications_user_id_created_at; -DROP INDEX IF EXISTS idx_room_notifications_user_id_is_read; -DROP TABLE IF EXISTS room_notifications; diff --git a/libs/migrate/sql/room_notifications/room_notifications_up_01.sql b/libs/migrate/sql/room_notifications/room_notifications_up_01.sql deleted file mode 100644 index fb3cfac..0000000 --- a/libs/migrate/sql/room_notifications/room_notifications_up_01.sql +++ /dev/null @@ -1,29 +0,0 @@ -create table if not exists room_notifications -( - id uuid not null - primary key, - room uuid, - project uuid, - user_id uuid, - notification_type varchar(255) not null, - related_message_id uuid, - related_user_id uuid, - related_room_id uuid, - title varchar(255) not null, - content text, - metadata jsonb, - is_read boolean default false not null, - is_archived boolean default false not null, - created_at timestamp with time zone not null, - read_at timestamp with time zone, - expires_at timestamp with time zone -); - -create index if not exists idx_room_notifications_user_id_is_read - on room_notifications (user_id, is_read); - -create index if not exists idx_room_notifications_user_id_created_at - on room_notifications (user_id, created_at); - -create index if not exists idx_room_notifications_expires_at - on room_notifications (expires_at); diff --git a/libs/migrate/sql/room_pin/room_pin_down_01.sql b/libs/migrate/sql/room_pin/room_pin_down_01.sql deleted file mode 100644 index 7bc3a35..0000000 --- a/libs/migrate/sql/room_pin/room_pin_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS room_pin; diff --git a/libs/migrate/sql/room_pin/room_pin_up_01.sql b/libs/migrate/sql/room_pin/room_pin_up_01.sql deleted file mode 100644 index 4536f70..0000000 --- a/libs/migrate/sql/room_pin/room_pin_up_01.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table if not exists room_pin -( - room uuid not null, - message uuid not null, - pinned_by uuid not null, - pinned_at timestamp with time zone not null, - primary key (room, message) -); diff --git a/libs/migrate/sql/room_thread/room_thread_down_01.sql b/libs/migrate/sql/room_thread/room_thread_down_01.sql deleted file mode 100644 index 3be93da..0000000 --- a/libs/migrate/sql/room_thread/room_thread_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_room_thread_room; -DROP TABLE IF EXISTS room_thread; diff --git a/libs/migrate/sql/room_thread/room_thread_up_01.sql b/libs/migrate/sql/room_thread/room_thread_up_01.sql deleted file mode 100644 index 149ff5e..0000000 --- a/libs/migrate/sql/room_thread/room_thread_up_01.sql +++ /dev/null @@ -1,16 +0,0 @@ -create table if not exists room_thread -( - id uuid not null - primary key, - room uuid not null, - parent bigint not null, - created_by uuid not null, - participants jsonb not null, - last_message_at timestamp with time zone not null, - last_message_preview text, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); - -create index if not exists idx_room_thread_room - on room_thread (room); diff --git a/libs/migrate/sql/room_user_state/room_user_state_down_01.sql b/libs/migrate/sql/room_user_state/room_user_state_down_01.sql deleted file mode 100644 index d61e9f1..0000000 --- a/libs/migrate/sql/room_user_state/room_user_state_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_room_user_state_user; -DROP TABLE IF EXISTS room_user_state; diff --git a/libs/migrate/sql/room_user_state/room_user_state_up_01.sql b/libs/migrate/sql/room_user_state/room_user_state_up_01.sql deleted file mode 100644 index 35e2c75..0000000 --- a/libs/migrate/sql/room_user_state/room_user_state_up_01.sql +++ /dev/null @@ -1,14 +0,0 @@ -create table if not exists room_user_state -( - room uuid not null, - "user" uuid not null, - last_read_seq bigint, - do_not_disturb boolean default false not null, - dnd_start_hour smallint, - dnd_end_hour smallint, - joined_at timestamp with time zone, - primary key (room, "user") -); - -create index if not exists idx_room_user_state_user - on room_user_state ("user"); diff --git a/libs/migrate/sql/user/user_down_01.sql b/libs/migrate/sql/user/user_down_01.sql deleted file mode 100644 index 8203674..0000000 --- a/libs/migrate/sql/user/user_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_user_username; -DROP TABLE IF EXISTS "user"; diff --git a/libs/migrate/sql/user/user_up_01.sql b/libs/migrate/sql/user/user_up_01.sql deleted file mode 100644 index 54d7dbc..0000000 --- a/libs/migrate/sql/user/user_up_01.sql +++ /dev/null @@ -1,16 +0,0 @@ -create table if not exists "user" -( - uid uuid not null - primary key, - username varchar(255) not null, - display_name varchar(255), - avatar_url varchar(255), - website_url varchar(255), - organization varchar(255), - last_sign_in_at timestamp with time zone, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); - -create index if not exists idx_user_username - on "user" (username); diff --git a/libs/migrate/sql/user_2fa/user_2fa_down_01.sql b/libs/migrate/sql/user_2fa/user_2fa_down_01.sql deleted file mode 100644 index b4dfa08..0000000 --- a/libs/migrate/sql/user_2fa/user_2fa_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS user_2fa; diff --git a/libs/migrate/sql/user_2fa/user_2fa_up_01.sql b/libs/migrate/sql/user_2fa/user_2fa_up_01.sql deleted file mode 100644 index 8602612..0000000 --- a/libs/migrate/sql/user_2fa/user_2fa_up_01.sql +++ /dev/null @@ -1,11 +0,0 @@ -create table if not exists user_2fa -( - "user" uuid not null - primary key, - method varchar(255) not null, - secret varchar(255), - backup_codes jsonb not null, - is_enabled boolean default false not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); diff --git a/libs/migrate/sql/user_activity_log/user_activity_log_down_01.sql b/libs/migrate/sql/user_activity_log/user_activity_log_down_01.sql deleted file mode 100644 index 40671c7..0000000 --- a/libs/migrate/sql/user_activity_log/user_activity_log_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_user_activity_log_created_at; -DROP INDEX IF EXISTS idx_user_activity_log_user_uid; -DROP TABLE IF EXISTS user_activity_log; diff --git a/libs/migrate/sql/user_activity_log/user_activity_log_up_01.sql b/libs/migrate/sql/user_activity_log/user_activity_log_up_01.sql deleted file mode 100644 index 11779e5..0000000 --- a/libs/migrate/sql/user_activity_log/user_activity_log_up_01.sql +++ /dev/null @@ -1,17 +0,0 @@ -create table if not exists user_activity_log -( - id bigserial - primary key, - user_uid uuid, - action varchar(255) not null, - ip_address varchar(255), - user_agent varchar(255), - details jsonb not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_user_activity_log_user_uid - on user_activity_log (user_uid); - -create index if not exists idx_user_activity_log_created_at - on user_activity_log (created_at); diff --git a/libs/migrate/sql/user_billing_history/user_billing_history_down_01.sql b/libs/migrate/sql/user_billing_history/user_billing_history_down_01.sql deleted file mode 100644 index b4c6f19..0000000 --- a/libs/migrate/sql/user_billing_history/user_billing_history_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_user_billing_history_created_at; -DROP INDEX IF EXISTS idx_user_billing_history_user; -DROP TABLE IF EXISTS user_billing_history; diff --git a/libs/migrate/sql/user_billing_history/user_billing_history_up_01.sql b/libs/migrate/sql/user_billing_history/user_billing_history_up_01.sql deleted file mode 100644 index 9b2cf7c..0000000 --- a/libs/migrate/sql/user_billing_history/user_billing_history_up_01.sql +++ /dev/null @@ -1,17 +0,0 @@ -create table if not exists user_billing_history -( - uid uuid not null - primary key, - user_uuid uuid not null, - amount numeric not null, - currency text not null, - reason text not null, - extra jsonb, - created_at timestamp with time zone not null -); - -create index if not exists idx_user_billing_history_user - on user_billing_history (user_uuid); - -create index if not exists idx_user_billing_history_created_at - on user_billing_history (created_at); diff --git a/libs/migrate/sql/user_email/user_email_down_01.sql b/libs/migrate/sql/user_email/user_email_down_01.sql deleted file mode 100644 index 4a88fba..0000000 --- a/libs/migrate/sql/user_email/user_email_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_user_email_email; -DROP TABLE IF EXISTS user_email; diff --git a/libs/migrate/sql/user_email/user_email_up_01.sql b/libs/migrate/sql/user_email/user_email_up_01.sql deleted file mode 100644 index d8c4d0f..0000000 --- a/libs/migrate/sql/user_email/user_email_up_01.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table if not exists user_email -( - "user" uuid not null - primary key, - email varchar(255) not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_user_email_email - on user_email (email); diff --git a/libs/migrate/sql/user_email_change/user_email_change_down_01.sql b/libs/migrate/sql/user_email_change/user_email_change_down_01.sql deleted file mode 100644 index 82f07db..0000000 --- a/libs/migrate/sql/user_email_change/user_email_change_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_user_email_change_user_uid; -DROP TABLE IF EXISTS user_email_change; diff --git a/libs/migrate/sql/user_email_change/user_email_change_up_01.sql b/libs/migrate/sql/user_email_change/user_email_change_up_01.sql deleted file mode 100644 index 86541fd..0000000 --- a/libs/migrate/sql/user_email_change/user_email_change_up_01.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table if not exists user_email_change -( - token varchar(255) not null - primary key, - user_uid uuid not null, - new_email varchar(255) not null, - expires_at timestamp with time zone not null, - used boolean default false not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_user_email_change_user_uid - on user_email_change (user_uid); diff --git a/libs/migrate/sql/user_notification/user_notification_down_01.sql b/libs/migrate/sql/user_notification/user_notification_down_01.sql deleted file mode 100644 index da983f1..0000000 --- a/libs/migrate/sql/user_notification/user_notification_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS user_notification; diff --git a/libs/migrate/sql/user_notification/user_notification_up_01.sql b/libs/migrate/sql/user_notification/user_notification_up_01.sql deleted file mode 100644 index 177880f..0000000 --- a/libs/migrate/sql/user_notification/user_notification_up_01.sql +++ /dev/null @@ -1,20 +0,0 @@ -create table if not exists user_notification -( - "user" uuid not null - primary key, - email_enabled boolean default false not null, - in_app_enabled boolean default true not null, - push_enabled boolean default false not null, - digest_mode varchar(255) not null, - dnd_enabled boolean default false not null, - dnd_start_minute integer, - dnd_end_minute integer, - marketing_enabled boolean default true not null, - security_enabled boolean default true not null, - product_enabled boolean default true not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null, - push_subscription_endpoint text, - push_subscription_keys_p256dh text, - push_subscription_keys_auth text -); diff --git a/libs/migrate/sql/user_password/user_password_down_01.sql b/libs/migrate/sql/user_password/user_password_down_01.sql deleted file mode 100644 index 6a3da9c..0000000 --- a/libs/migrate/sql/user_password/user_password_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS user_password; diff --git a/libs/migrate/sql/user_password/user_password_up_01.sql b/libs/migrate/sql/user_password/user_password_up_01.sql deleted file mode 100644 index 93268c3..0000000 --- a/libs/migrate/sql/user_password/user_password_up_01.sql +++ /dev/null @@ -1,10 +0,0 @@ -create table if not exists user_password -( - "user" uuid not null - primary key, - password_hash varchar(255) not null, - password_salt varchar(255), - is_active boolean default true not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); diff --git a/libs/migrate/sql/user_password_reset/user_password_reset_down_01.sql b/libs/migrate/sql/user_password_reset/user_password_reset_down_01.sql deleted file mode 100644 index a805236..0000000 --- a/libs/migrate/sql/user_password_reset/user_password_reset_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_user_password_reset_user_uid; -DROP TABLE IF EXISTS user_password_reset; diff --git a/libs/migrate/sql/user_password_reset/user_password_reset_up_01.sql b/libs/migrate/sql/user_password_reset/user_password_reset_up_01.sql deleted file mode 100644 index a6a5bf7..0000000 --- a/libs/migrate/sql/user_password_reset/user_password_reset_up_01.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table if not exists user_password_reset -( - token varchar(255) not null - primary key, - user_uid uuid not null, - expires_at timestamp with time zone not null, - used boolean default false not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_user_password_reset_user_uid - on user_password_reset (user_uid); diff --git a/libs/migrate/sql/user_preferences/user_preferences_down_01.sql b/libs/migrate/sql/user_preferences/user_preferences_down_01.sql deleted file mode 100644 index f3b2897..0000000 --- a/libs/migrate/sql/user_preferences/user_preferences_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS user_preferences; diff --git a/libs/migrate/sql/user_preferences/user_preferences_up_01.sql b/libs/migrate/sql/user_preferences/user_preferences_up_01.sql deleted file mode 100644 index 8e3b09e..0000000 --- a/libs/migrate/sql/user_preferences/user_preferences_up_01.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table if not exists user_preferences -( - "user" uuid not null - primary key, - language varchar(255) not null, - theme varchar(255) not null, - timezone varchar(255) not null, - email_notifications boolean default true not null, - in_app_notifications boolean default true not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); diff --git a/libs/migrate/sql/user_relation/user_relation_down_01.sql b/libs/migrate/sql/user_relation/user_relation_down_01.sql deleted file mode 100644 index ca8fadd..0000000 --- a/libs/migrate/sql/user_relation/user_relation_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_user_relation_target; -DROP INDEX IF EXISTS idx_user_relation_user; -DROP TABLE IF EXISTS user_relation; diff --git a/libs/migrate/sql/user_relation/user_relation_up_01.sql b/libs/migrate/sql/user_relation/user_relation_up_01.sql deleted file mode 100644 index eec9250..0000000 --- a/libs/migrate/sql/user_relation/user_relation_up_01.sql +++ /dev/null @@ -1,15 +0,0 @@ -create table if not exists user_relation -( - id bigserial - primary key, - "user" uuid not null, - target uuid not null, - relation_type varchar(255) not null, - created_at timestamp with time zone not null -); - -create index if not exists idx_user_relation_user - on user_relation ("user"); - -create index if not exists idx_user_relation_target - on user_relation (target); diff --git a/libs/migrate/sql/user_ssh_key/user_ssh_key_down_01.sql b/libs/migrate/sql/user_ssh_key/user_ssh_key_down_01.sql deleted file mode 100644 index 751b198..0000000 --- a/libs/migrate/sql/user_ssh_key/user_ssh_key_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_user_ssh_key_user; -DROP TABLE IF EXISTS user_ssh_key; diff --git a/libs/migrate/sql/user_ssh_key/user_ssh_key_up_01.sql b/libs/migrate/sql/user_ssh_key/user_ssh_key_up_01.sql deleted file mode 100644 index 3921b5d..0000000 --- a/libs/migrate/sql/user_ssh_key/user_ssh_key_up_01.sql +++ /dev/null @@ -1,20 +0,0 @@ -create table if not exists user_ssh_key -( - id bigserial - primary key, - "user" uuid not null, - title varchar(255) not null, - public_key text not null, - fingerprint varchar(255) not null, - key_type varchar(255) not null, - key_bits integer, - is_verified boolean default false not null, - last_used_at timestamp with time zone, - expires_at timestamp with time zone, - is_revoked boolean default false not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); - -create index if not exists idx_user_ssh_key_user - on user_ssh_key ("user"); diff --git a/libs/migrate/sql/user_token/user_token_down_01.sql b/libs/migrate/sql/user_token/user_token_down_01.sql deleted file mode 100644 index fca281b..0000000 --- a/libs/migrate/sql/user_token/user_token_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_user_token_user; -DROP TABLE IF EXISTS user_token; diff --git a/libs/migrate/sql/user_token/user_token_up_01.sql b/libs/migrate/sql/user_token/user_token_up_01.sql deleted file mode 100644 index c34401d..0000000 --- a/libs/migrate/sql/user_token/user_token_up_01.sql +++ /dev/null @@ -1,16 +0,0 @@ -create table if not exists user_token -( - id bigserial - primary key, - "user" uuid not null, - name varchar(255) not null, - token_hash varchar(255) not null, - scopes jsonb not null, - expires_at timestamp with time zone, - is_revoked boolean default false not null, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); - -create index if not exists idx_user_token_user - on user_token ("user"); diff --git a/libs/migrate/sql/workspace/workspace_down_01.sql b/libs/migrate/sql/workspace/workspace_down_01.sql deleted file mode 100644 index d6d10b0..0000000 --- a/libs/migrate/sql/workspace/workspace_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS workspace; diff --git a/libs/migrate/sql/workspace/workspace_up_01.sql b/libs/migrate/sql/workspace/workspace_up_01.sql deleted file mode 100644 index 65da288..0000000 --- a/libs/migrate/sql/workspace/workspace_up_01.sql +++ /dev/null @@ -1,17 +0,0 @@ -create table if not exists workspace -( - id uuid not null - primary key, - slug varchar(255) not null, - name varchar(255) not null, - description text, - avatar_url varchar(255), - plan varchar(50) default 'free'::character varying not null, - billing_email varchar(255), - stripe_customer_id varchar(255), - stripe_subscription_id varchar(255), - plan_expires_at timestamp with time zone, - deleted_at timestamp with time zone, - created_at timestamp with time zone not null, - updated_at timestamp with time zone not null -); diff --git a/libs/migrate/sql/workspace_alert_config/workspace_alert_config_down_01.sql b/libs/migrate/sql/workspace_alert_config/workspace_alert_config_down_01.sql deleted file mode 100644 index abb0486..0000000 --- a/libs/migrate/sql/workspace_alert_config/workspace_alert_config_down_01.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_alert_config_workspace; -DROP TABLE IF EXISTS workspace_alert_config; diff --git a/libs/migrate/sql/workspace_alert_config/workspace_alert_config_up_01.sql b/libs/migrate/sql/workspace_alert_config/workspace_alert_config_up_01.sql deleted file mode 100644 index b7fc50f..0000000 --- a/libs/migrate/sql/workspace_alert_config/workspace_alert_config_up_01.sql +++ /dev/null @@ -1,17 +0,0 @@ -create table if not exists workspace_alert_config -( - id serial - primary key, - workspace_id uuid not null, - alert_type varchar(32) not null, - threshold numeric(10, 4) not null, - email_enabled boolean default true, - enabled boolean default true, - created_by integer, - created_at timestamp with time zone default now(), - updated_at timestamp with time zone default now(), - unique (workspace_id, alert_type) -); - -create index if not exists idx_alert_config_workspace - on workspace_alert_config (workspace_id); diff --git a/libs/migrate/sql/workspace_billing/workspace_billing_down_01.sql b/libs/migrate/sql/workspace_billing/workspace_billing_down_01.sql deleted file mode 100644 index 9ad0e1f..0000000 --- a/libs/migrate/sql/workspace_billing/workspace_billing_down_01.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS workspace_billing; diff --git a/libs/migrate/sql/workspace_billing/workspace_billing_up_01.sql b/libs/migrate/sql/workspace_billing/workspace_billing_up_01.sql deleted file mode 100644 index 48af1ad..0000000 --- a/libs/migrate/sql/workspace_billing/workspace_billing_up_01.sql +++ /dev/null @@ -1,13 +0,0 @@ -create table if not exists workspace_billing -( - workspace_id uuid not null - primary key - references workspace - on delete cascade, - balance numeric(20, 4) default 0 not null, - currency varchar(10) default 'USD'::character varying not null, - monthly_quota numeric(20, 4) default 0 not null, - total_spent numeric(20, 4) default 0 not null, - updated_at timestamp with time zone not null, - created_at timestamp with time zone not null -); diff --git a/libs/migrate/sql/workspace_billing_history/workspace_billing_history_down_01.sql b/libs/migrate/sql/workspace_billing_history/workspace_billing_history_down_01.sql deleted file mode 100644 index cb708e6..0000000 --- a/libs/migrate/sql/workspace_billing_history/workspace_billing_history_down_01.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_wsbh_created_at; -DROP INDEX IF EXISTS idx_wsbh_workspace_id; -DROP TABLE IF EXISTS workspace_billing_history; diff --git a/libs/migrate/sql/workspace_billing_history/workspace_billing_history_up_01.sql b/libs/migrate/sql/workspace_billing_history/workspace_billing_history_up_01.sql deleted file mode 100644 index 7830b32..0000000 --- a/libs/migrate/sql/workspace_billing_history/workspace_billing_history_up_01.sql +++ /dev/null @@ -1,20 +0,0 @@ -create table if not exists workspace_billing_history -( - uid uuid not null - primary key, - workspace_id uuid not null - references workspace - on delete cascade, - user_id uuid, - amount numeric(20, 4) not null, - currency varchar(10) default 'USD'::character varying not null, - reason varchar(100) not null, - extra jsonb, - created_at timestamp with time zone not null -); - -create index if not exists idx_wsbh_workspace_id - on workspace_billing_history (workspace_id); - -create index if not exists idx_wsbh_created_at - on workspace_billing_history (created_at desc); diff --git a/libs/migrate/sql/workspace_membership/workspace_membership_down_01.sql b/libs/migrate/sql/workspace_membership/workspace_membership_down_01.sql deleted file mode 100644 index d611f8f..0000000 --- a/libs/migrate/sql/workspace_membership/workspace_membership_down_01.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_workspace_membership_invite_token; -DROP INDEX IF EXISTS idx_workspace_membership_user; -DROP INDEX IF EXISTS idx_workspace_membership_ws_user; -DROP TABLE IF EXISTS workspace_membership; diff --git a/libs/migrate/sql/workspace_membership/workspace_membership_up_01.sql b/libs/migrate/sql/workspace_membership/workspace_membership_up_01.sql deleted file mode 100644 index 44c4ded..0000000 --- a/libs/migrate/sql/workspace_membership/workspace_membership_up_01.sql +++ /dev/null @@ -1,24 +0,0 @@ -create table if not exists workspace_membership -( - id bigserial - primary key, - workspace_id uuid not null, - user_id uuid not null, - role varchar(50) default 'member'::character varying not null, - status varchar(50) default 'active'::character varying not null, - invited_by uuid, - joined_at timestamp with time zone not null, - invite_token varchar(255), - invite_expires_at timestamp with time zone, - unique (workspace_id, user_id) -); - -create unique index if not exists idx_workspace_membership_ws_user - on workspace_membership (workspace_id, user_id); - -create index if not exists idx_workspace_membership_user - on workspace_membership (user_id); - -create index if not exists idx_workspace_membership_invite_token - on workspace_membership (invite_token) - where (invite_token IS NOT NULL); diff --git a/libs/models/Cargo.toml b/libs/models/Cargo.toml deleted file mode 100644 index cccd42f..0000000 --- a/libs/models/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "models" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true -[lib] -path = "lib.rs" -name = "models" -[dependencies] -sea-orm = { workspace = true, features = ["with-chrono", "with-uuid", "with-rust_decimal", "with-json", "rust_decimal"] } -serde = { workspace = true, features = ["derive"] } -chrono = { workspace = true, features = ["serde"] } -uuid = { workspace = true, features = ["serde"] } -rust_decimal = { workspace = true, features = ["serde"] } -serde_json = { workspace = true } -utoipa = { workspace = true, features = ["chrono", "uuid"] } - - -[lints] -workspace = true diff --git a/libs/models/agent_task/mod.rs b/libs/models/agent_task/mod.rs deleted file mode 100644 index ea4aa9d..0000000 --- a/libs/models/agent_task/mod.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! Agent task model — sub-agents and sub-tasks as a unified concept. -//! -//! An `agent_task` represents either: -//! - A **root task** (parent_id = NULL): initiated by a user or system event. -//! The parent agent (Supervisor) spawns sub-tasks and coordinates their results. -//! - A **sub-task** (parent_id = set): a unit of work executed by a sub-agent. -//! -//! Status lifecycle: `pending` → `running` → `done` | `failed` -//! -//! Sub-agents are represented as `agent_task` records with a parent reference, -//! allowing hierarchical task trees and result aggregation. - -use crate::{DateTimeUtc, IssueId, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Agent task type — the kind of agent that executes this task. -#[derive( - Clone, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize, sea_orm::DeriveActiveEnum, -)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -pub enum AgentType { - #[sea_orm(string_value = "React")] - React, - #[sea_orm(string_value = "Chat")] - Chat, -} - -impl Default for AgentType { - fn default() -> Self { - AgentType::React - } -} - -impl std::fmt::Display for AgentType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AgentType::React => write!(f, "React"), - AgentType::Chat => write!(f, "Chat"), - } - } -} - -/// Task status lifecycle. -#[derive( - Clone, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize, sea_orm::DeriveActiveEnum, -)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -pub enum TaskStatus { - #[sea_orm(string_value = "Pending")] - Pending, - #[sea_orm(string_value = "Running")] - Running, - #[sea_orm(string_value = "Paused")] - Paused, - #[sea_orm(string_value = "Done")] - Done, - #[sea_orm(string_value = "Failed")] - Failed, - #[sea_orm(string_value = "Cancelled")] - Cancelled, -} - -impl Default for TaskStatus { - fn default() -> Self { - TaskStatus::Pending - } -} - -impl std::fmt::Display for TaskStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TaskStatus::Pending => write!(f, "Pending"), - TaskStatus::Running => write!(f, "Running"), - TaskStatus::Paused => write!(f, "Paused"), - TaskStatus::Done => write!(f, "Done"), - TaskStatus::Failed => write!(f, "Failed"), - TaskStatus::Cancelled => write!(f, "Cancelled"), - } - } -} - -/// Agent task record — represents both root tasks and sub-tasks. -#[derive(Clone, Debug, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "agent_task")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - - /// Project this task belongs to. - pub project_uuid: ProjectId, - - /// Parent task (NULL for root tasks, set for sub-tasks). - #[sea_orm(nullable)] - pub parent_id: Option, - - /// Issue this task is bound to (optional). - #[sea_orm(nullable)] - pub issue_id: Option, - - /// Agent type that executes this task. - #[sea_orm(column_type = "String(StringLen::None)", default = "React")] - pub agent_type: AgentType, - - /// Current task status. - #[sea_orm(column_type = "String(StringLen::None)", default = "Pending")] - pub status: TaskStatus, - - /// Human-readable task title / goal description. - #[sea_orm(nullable)] - pub title: Option, - - /// Task input — the prompt or goal text. - pub input: String, - - /// Task output — populated when status = done. - #[sea_orm(nullable)] - pub output: Option, - - /// Error message — populated when status = failed. - #[sea_orm(nullable)] - pub error: Option, - - /// User who initiated this task. - #[sea_orm(nullable)] - pub created_by: Option, - - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, - - /// When execution started (status → running). - #[sea_orm(nullable)] - pub started_at: Option, - - /// When execution completed (status → done | failed). - #[sea_orm(nullable)] - pub done_at: Option, - - /// Current progress description (e.g., "step 2/5: analyzing code"). - #[sea_orm(nullable)] - pub progress: Option, - - /// Number of times this task has been retried. - #[sea_orm(nullable)] - pub retry_count: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(belongs_to = "Entity", from = "Column::ParentId", to = "Column::Id")] - ParentTask, -} - -impl ActiveModelBehavior for ActiveModel {} - -impl Model { - pub fn is_root_task(&self) -> bool { - self.parent_id.is_none() - } - - pub fn is_done(&self) -> bool { - matches!( - self.status, - TaskStatus::Done | TaskStatus::Failed | TaskStatus::Cancelled - ) - } - - pub fn is_running(&self) -> bool { - matches!( - self.status, - TaskStatus::Running | TaskStatus::Pending | TaskStatus::Paused - ) - } -} diff --git a/libs/models/agents/mod.rs b/libs/models/agents/mod.rs deleted file mode 100644 index aa1f970..0000000 --- a/libs/models/agents/mod.rs +++ /dev/null @@ -1,176 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Model modality. Stored as `"text"`, `"image"`, `"audio"`, or `"multimodal"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ModelModality { - Text, - Image, - Audio, - Multimodal, -} - -impl std::fmt::Display for ModelModality { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ModelModality::Text => write!(f, "text"), - ModelModality::Image => write!(f, "image"), - ModelModality::Audio => write!(f, "audio"), - ModelModality::Multimodal => write!(f, "multimodal"), - } - } -} - -impl std::str::FromStr for ModelModality { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "text" => Ok(ModelModality::Text), - "image" => Ok(ModelModality::Image), - "audio" => Ok(ModelModality::Audio), - "multimodal" => Ok(ModelModality::Multimodal), - _ => Err("unknown model modality"), - } - } -} - -/// Primary model capability. Stored as `"chat"`, `"completion"`, `"embedding"`, -/// or `"code"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ModelCapability { - Chat, - Completion, - Embedding, - Code, -} - -impl std::fmt::Display for ModelCapability { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ModelCapability::Chat => write!(f, "chat"), - ModelCapability::Completion => write!(f, "completion"), - ModelCapability::Embedding => write!(f, "embedding"), - ModelCapability::Code => write!(f, "code"), - } - } -} - -impl std::str::FromStr for ModelCapability { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "chat" => Ok(ModelCapability::Chat), - "completion" => Ok(ModelCapability::Completion), - "embedding" => Ok(ModelCapability::Embedding), - "code" => Ok(ModelCapability::Code), - _ => Err("unknown model capability"), - } - } -} - -/// Model or model-version availability status. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ModelStatus { - /// Model is available and can be used. - Active, - /// Model is no longer available from the provider. - Deprecated, - /// Model was not found in the last upstream sync (offline). - Offline, -} - -impl std::fmt::Display for ModelStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ModelStatus::Active => write!(f, "active"), - ModelStatus::Deprecated => write!(f, "deprecated"), - ModelStatus::Offline => write!(f, "offline"), - } - } -} - -impl std::str::FromStr for ModelStatus { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "active" => Ok(ModelStatus::Active), - "deprecated" => Ok(ModelStatus::Deprecated), - "offline" => Ok(ModelStatus::Offline), - _ => Err("unknown model status"), - } - } -} - -/// Capability type for per-version capability records. Stored as -/// `"function_call"`, `"tool_use"`, `"vision"`, or `"reasoning"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum CapabilityType { - FunctionCall, - ToolUse, - Vision, - Reasoning, -} - -impl std::fmt::Display for CapabilityType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - CapabilityType::FunctionCall => write!(f, "function_call"), - CapabilityType::ToolUse => write!(f, "tool_use"), - CapabilityType::Vision => write!(f, "vision"), - CapabilityType::Reasoning => write!(f, "reasoning"), - } - } -} - -impl std::str::FromStr for CapabilityType { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "function_call" => Ok(CapabilityType::FunctionCall), - "tool_use" => Ok(CapabilityType::ToolUse), - "vision" => Ok(CapabilityType::Vision), - "reasoning" => Ok(CapabilityType::Reasoning), - _ => Err("unknown capability type"), - } - } -} - -/// Pricing currency. Stored as `"USD"` or `"CNY"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum PricingCurrency { - Usd, - Cny, -} - -impl std::fmt::Display for PricingCurrency { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PricingCurrency::Usd => write!(f, "USD"), - PricingCurrency::Cny => write!(f, "CNY"), - } - } -} - -impl std::str::FromStr for PricingCurrency { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "USD" => Ok(PricingCurrency::Usd), - "CNY" => Ok(PricingCurrency::Cny), - _ => Err("unknown pricing currency"), - } - } -} - -pub use model::Entity as Model; -pub use model_capability::Entity as ModelCapabilityRecord; -pub use model_parameter_profile::Entity as ModelParameterProfile; -pub use model_pricing::Entity as ModelPricing; -pub use model_provider::Entity as ModelProvider; -pub use model_version::Entity as ModelVersion; - -pub mod model; -pub mod model_capability; -pub mod model_parameter_profile; -pub mod model_pricing; -pub mod model_provider; -pub mod model_version; diff --git a/libs/models/agents/model.rs b/libs/models/agents/model.rs deleted file mode 100644 index 05f3d18..0000000 --- a/libs/models/agents/model.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::{DateTimeUtc, ModelId, ModelProviderId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::{ModelCapability, ModelModality, ModelStatus}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_model")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ModelId, - pub provider_id: ModelProviderId, - pub name: String, - pub modality: String, - pub capability: String, - pub context_length: i64, - pub max_output_tokens: Option, - pub training_cutoff: Option, - pub is_open_source: bool, - pub status: String, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -impl Model { - pub fn modality_enum(&self) -> Result { - self.modality.parse() - } - - pub fn capability_enum(&self) -> Result { - self.capability.parse() - } - - pub fn status_enum(&self) -> Result { - self.status.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/agents/model_capability.rs b/libs/models/agents/model_capability.rs deleted file mode 100644 index 6071144..0000000 --- a/libs/models/agents/model_capability.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::DateTimeUtc; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::CapabilityType; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_model_capability")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub model_version_id: i64, - pub capability: String, - pub is_supported: bool, - pub created_at: DateTimeUtc, -} - -impl Model { - pub fn capability_enum(&self) -> Result { - self.capability.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/agents/model_parameter_profile.rs b/libs/models/agents/model_parameter_profile.rs deleted file mode 100644 index 105a1d9..0000000 --- a/libs/models/agents/model_parameter_profile.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::ModelVersionId; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_model_parameter_profile")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub model_version_id: ModelVersionId, - pub temperature_min: f64, - pub temperature_max: f64, - pub top_p_min: f64, - pub top_p_max: f64, - pub frequency_penalty_supported: bool, - pub presence_penalty_supported: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/agents/model_pricing.rs b/libs/models/agents/model_pricing.rs deleted file mode 100644 index a8b4ce1..0000000 --- a/libs/models/agents/model_pricing.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::{DateTimeUtc, ModelVersionId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::PricingCurrency; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_model_pricing")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub model_version_id: ModelVersionId, - pub input_price_per_1k_tokens: String, - pub output_price_per_1k_tokens: String, - pub currency: String, - pub effective_from: DateTimeUtc, -} - -impl Model { - pub fn currency_enum(&self) -> Result { - self.currency.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/agents/model_provider.rs b/libs/models/agents/model_provider.rs deleted file mode 100644 index aa167a4..0000000 --- a/libs/models/agents/model_provider.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::{DateTimeUtc, ModelProviderId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::ModelStatus; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_model_provider")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ModelProviderId, - pub name: String, - pub display_name: String, - pub website: Option, - pub status: String, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -impl Model { - pub fn status_enum(&self) -> Result { - self.status.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/agents/model_version.rs b/libs/models/agents/model_version.rs deleted file mode 100644 index 72bea36..0000000 --- a/libs/models/agents/model_version.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{DateTimeUtc, ModelId, ModelVersionId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::ModelStatus; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_model_version")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ModelVersionId, - pub model_id: ModelId, - pub version: String, - pub release_date: Option, - pub change_log: Option, - pub is_default: bool, - pub status: String, - pub created_at: DateTimeUtc, -} - -impl Model { - pub fn status_enum(&self) -> Result { - self.status.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/ai_conversation.rs b/libs/models/ai/ai_conversation.rs deleted file mode 100644 index c8e35db..0000000 --- a/libs/models/ai/ai_conversation.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] -#[sea_orm(table_name = "ai_conversation")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub user_id: UserId, - pub project_id: Option, - pub scope: String, - pub title: Option, - pub model: String, - pub model_config: Option, - pub status: String, - pub root_message_id: Option, - pub fork_count: i32, - pub is_shared: bool, - pub message_count: i32, - pub token_usage_total: Option, - /// Who can see this chat: "owner" | "admin" | "member" - pub access_visibility: String, - /// Who can send messages: "owner" | "admin" | "member" - pub can_ask: String, - /// Project-unique sequential number for this chat - pub project_uid: Option, - /// AI model UUID selected for this chat - pub model_uid: Option, - /// AI model display name (e.g. "Claude Sonnet 4") - pub model_name: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/ai_message.rs b/libs/models/ai/ai_message.rs deleted file mode 100644 index 97b07a1..0000000 --- a/libs/models/ai/ai_message.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::{DateTimeUtc, Uuid}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] -#[sea_orm(table_name = "ai_message")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub conversation_id: Uuid, - pub parent_message_id: Option, - pub role: String, - pub content: Json, - pub model: Option, - pub is_fork_origin: bool, - pub stop_reason: Option, - pub input_tokens: Option, - pub output_tokens: Option, - pub latency_ms: Option, - pub metadata: Option, - pub room_id: Option, - /// Groups all versions of the same message (original + edits). - /// For new messages, this equals the message id itself. - pub version_group_id: Option, - /// Sequential version number within the group (1 = original, 2 = first edit, etc.) - pub version_number: i32, - /// Whether this is the current active version in the group. - pub is_latest: bool, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/ai_message_fork.rs b/libs/models/ai/ai_message_fork.rs deleted file mode 100644 index 1d913e5..0000000 --- a/libs/models/ai/ai_message_fork.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::DateTimeUtc; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] -#[sea_orm(table_name = "ai_message_fork")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub conversation_id: Option, - pub source_message_id: Uuid, - pub fork_message_id: Uuid, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/ai_session.rs b/libs/models/ai/ai_session.rs deleted file mode 100644 index e3dd0fc..0000000 --- a/libs/models/ai/ai_session.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::{AiSessionId, DateTimeUtc, ModelId, ModelVersionId, RoomId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_session")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: AiSessionId, - pub room: RoomId, - pub model: ModelId, - pub version: ModelVersionId, - pub token_input: i64, - pub token_output: i64, - pub latency_ms: Option, - pub cost: Option, - pub currency: Option, - pub error_message: Option, - pub error_code: Option, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/ai_shared_conversation.rs b/libs/models/ai/ai_shared_conversation.rs deleted file mode 100644 index 5ffb8b4..0000000 --- a/libs/models/ai/ai_shared_conversation.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] -#[sea_orm(table_name = "ai_shared_conversation")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub conversation_id: Uuid, - pub share_token: String, - pub created_by: UserId, - pub view_count: i32, - pub created_at: DateTimeUtc, - pub expires_at: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/ai_subagent_session.rs b/libs/models/ai/ai_subagent_session.rs deleted file mode 100644 index 38f1c31..0000000 --- a/libs/models/ai/ai_subagent_session.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::{DateTimeUtc, Uuid}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_subagent_session")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub conversation_id: Uuid, - pub message_id: Uuid, - pub children_id: String, - pub role: String, - pub task: String, - #[sea_orm(column_type = "Text")] - pub output: String, - pub input_tokens: i64, - pub output_tokens: i64, - pub model_name: Option, - pub status: String, - #[sea_orm(column_type = "Text", nullable)] - pub error_message: Option, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file diff --git a/libs/models/ai/ai_token_usage.rs b/libs/models/ai/ai_token_usage.rs deleted file mode 100644 index e090b7e..0000000 --- a/libs/models/ai/ai_token_usage.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, DeriveEntityModel)] -#[sea_orm(table_name = "ai_token_usage")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub user_id: UserId, - pub conversation_id: Option, - pub model: String, - pub input_tokens: i32, - pub output_tokens: i32, - pub cost_usd: Option, - pub recorded_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/ai_tool_auth.rs b/libs/models/ai/ai_tool_auth.rs deleted file mode 100644 index 19d7f62..0000000 --- a/libs/models/ai/ai_tool_auth.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::{AiSessionId, DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_tool_auth")] -pub struct Model { - #[sea_orm(primary_key)] - pub session: AiSessionId, - #[sea_orm(primary_key)] - pub tool_call_id: String, - pub method: String, - pub arguments: String, - pub decision: bool, - pub reason: String, - pub decision_by: UserId, - pub decision_comment: Option, - pub logs: sea_orm::JsonValue, - pub expires_at: Option, - pub authorized_at: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/ai_tool_call.rs b/libs/models/ai/ai_tool_call.rs deleted file mode 100644 index 4cf78b7..0000000 --- a/libs/models/ai/ai_tool_call.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::{AiSessionId, DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::ToolCallStatus; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "ai_tool_call")] -pub struct Model { - #[sea_orm(primary_key)] - pub tool_call_id: String, - #[sea_orm(primary_key)] - pub session: AiSessionId, - pub tool_name: String, - pub caller: UserId, - pub arguments: sea_orm::JsonValue, - pub result: sea_orm::JsonValue, - pub status: String, - pub execution_time_ms: Option, - pub error_message: Option, - pub error_stack: Option, - pub retry_count: i32, - pub created_at: DateTimeUtc, - pub completed_at: Option, - pub updated_at: DateTimeUtc, -} - -impl Model { - pub fn status_enum(&self) -> Result { - self.status.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/billing_error.rs b/libs/models/ai/billing_error.rs deleted file mode 100644 index 562d847..0000000 --- a/libs/models/ai/billing_error.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::DateTimeUtc; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// Persisted billing error that requires user attention. -/// Created when an AI request fails due to insufficient balance or quota. -/// Front-end reads unresolved errors to show warning banners. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "billing_error")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub id: Uuid, - /// "user" or "project" - #[sea_orm(column_type = "Text")] - pub scope: String, - /// user_uuid or project_uuid - pub scope_id: Uuid, - /// "insufficient_balance" or "over_quota" - #[sea_orm(column_type = "Text")] - pub error_type: String, - #[sea_orm(column_type = "Text")] - pub message: String, - pub details: Option, - #[sea_orm(default_value = false)] - pub resolved: bool, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/ai/mod.rs b/libs/models/ai/mod.rs deleted file mode 100644 index 9fe0026..0000000 --- a/libs/models/ai/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// AI tool call execution status. Stored as `"pending"`, `"running"`, `"success"`, -/// `"failed"`, or `"retrying"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ToolCallStatus { - Pending, - Running, - Success, - Failed, - Retrying, -} - -impl std::fmt::Display for ToolCallStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ToolCallStatus::Pending => write!(f, "pending"), - ToolCallStatus::Running => write!(f, "running"), - ToolCallStatus::Success => write!(f, "success"), - ToolCallStatus::Failed => write!(f, "failed"), - ToolCallStatus::Retrying => write!(f, "retrying"), - } - } -} - -impl std::str::FromStr for ToolCallStatus { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "pending" => Ok(ToolCallStatus::Pending), - "running" => Ok(ToolCallStatus::Running), - "success" => Ok(ToolCallStatus::Success), - "failed" => Ok(ToolCallStatus::Failed), - "retrying" => Ok(ToolCallStatus::Retrying), - _ => Err("unknown tool call status"), - } - } -} - -pub use ai_conversation::Entity as AiConversation; -pub use ai_message::Entity as AiMessage; -pub use ai_message_fork::Entity as AiMessageFork; -pub use ai_shared_conversation::Entity as AiSharedConversation; -pub use ai_subagent_session::Entity as AiSubAgentSession; -pub use ai_token_usage::Entity as AiTokenUsage; -pub use billing_error::Entity as BillingError; -pub use subscription::Entity as Subscription; - -pub mod ai_conversation; -pub mod ai_message; -pub mod ai_message_fork; -pub mod ai_session; -pub mod ai_shared_conversation; -pub mod ai_subagent_session; -pub mod ai_token_usage; -pub mod ai_tool_auth; -pub mod ai_tool_call; -pub mod billing_error; -pub mod subscription; diff --git a/libs/models/ai/subscription.rs b/libs/models/ai/subscription.rs deleted file mode 100644 index b2ff856..0000000 --- a/libs/models/ai/subscription.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::DateTimeUtc; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Subscription record — tracks paid plans for users or projects. -/// Each subscription defines quota limits (6h, weekly, monthly) and payment metadata. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "subscription")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - /// Who is subscribed: "user" or "project" - pub scope: String, - /// The UUID of the user or project - #[sea_orm(column_name = "scope_id")] - pub scope_id: Uuid, - /// Subscription start time - pub start_at: DateTimeUtc, - /// Subscription end (expiry) time - pub end_at: DateTimeUtc, - /// External order / transaction ID from payment platform - #[sea_orm(column_type = "Text", nullable)] - pub order_id: Option, - /// Payment platform identifier (e.g. "stripe", "alipay", "wechat") - pub platform: String, - /// Human-readable plan name (e.g. "Pro Monthly", "Team Annual") - #[sea_orm(column_type = "Text", nullable)] - pub plan_name: Option, - /// 6-hour quota limit (auto-reset every 6 hours) - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub quota_6h: Decimal, - /// 6-hour quota used so far in current window - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub quota_6h_used: Decimal, - /// Next 6-hour reset timestamp - pub quota_6h_reset_at: Option, - /// Weekly quota limit - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub quota_weekly: Decimal, - /// Weekly quota used so far - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub quota_weekly_used: Decimal, - /// Next weekly reset timestamp - pub quota_weekly_reset_at: Option, - /// Monthly quota limit - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub quota_monthly: Decimal, - /// Monthly quota used so far - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub quota_monthly_used: Decimal, - /// Whether subscription is currently active - #[sea_orm(default_value = false)] - pub is_active: bool, - #[sea_orm(column_type = "Text")] - pub currency: String, - pub updated_at: DateTimeUtc, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue.rs b/libs/models/issues/issue.rs deleted file mode 100644 index 8313b49..0000000 --- a/libs/models/issues/issue.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::{DateTimeUtc, IssueId, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::IssueState; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue")] -pub struct Model { - /// UUID for global uniqueness / API exposure. - #[sea_orm(primary_key)] - pub id: IssueId, - /// Project this issue belongs to. - pub project: ProjectId, - /// Sequential issue number within the project. Composite with `project` for uniqueness. - #[sea_orm(primary_key)] - pub number: i64, - pub title: String, - pub body: Option, - /// `"open"` or `"closed"`. - pub state: String, - pub author: UserId, - pub milestone: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, - pub closed_at: Option, - pub created_by_ai: bool, -} - -impl Model { - pub fn state_enum(&self) -> Result { - self.state.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue_assignee.rs b/libs/models/issues/issue_assignee.rs deleted file mode 100644 index a50ac10..0000000 --- a/libs/models/issues/issue_assignee.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{DateTimeUtc, IssueId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue_assignee")] -pub struct Model { - #[sea_orm(primary_key)] - pub issue: IssueId, - #[sea_orm(primary_key)] - pub user: UserId, - pub assigned_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue_comment.rs b/libs/models/issues/issue_comment.rs deleted file mode 100644 index 4b06993..0000000 --- a/libs/models/issues/issue_comment.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{DateTimeUtc, IssueId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue_comment")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub issue: IssueId, - pub author: UserId, - pub body: String, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue_comment_reaction.rs b/libs/models/issues/issue_comment_reaction.rs deleted file mode 100644 index b5d4bb3..0000000 --- a/libs/models/issues/issue_comment_reaction.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::ReactionType; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue_comment_reaction")] -pub struct Model { - #[sea_orm(primary_key)] - pub comment: i64, - #[sea_orm(primary_key, column_name = "user_uuid")] - pub user: UserId, - #[sea_orm(primary_key)] - pub reaction: String, - pub created_at: DateTimeUtc, -} - -impl Model { - pub fn reaction_enum(&self) -> Result { - self.reaction.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue_label.rs b/libs/models/issues/issue_label.rs deleted file mode 100644 index eda090a..0000000 --- a/libs/models/issues/issue_label.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{DateTimeUtc, IssueId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue_label")] -pub struct Model { - #[sea_orm(primary_key)] - pub issue: IssueId, - #[sea_orm(primary_key)] - pub label: i64, - pub relation_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue_pull_request.rs b/libs/models/issues/issue_pull_request.rs deleted file mode 100644 index f5c5e87..0000000 --- a/libs/models/issues/issue_pull_request.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{DateTimeUtc, IssueId, RepoId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue_pull_request")] -pub struct Model { - #[sea_orm(primary_key)] - pub issue: IssueId, - #[sea_orm(primary_key)] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub number: i64, - pub relation_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue_reaction.rs b/libs/models/issues/issue_reaction.rs deleted file mode 100644 index 58f90de..0000000 --- a/libs/models/issues/issue_reaction.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::{DateTimeUtc, IssueId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::ReactionType; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue_reaction")] -pub struct Model { - #[sea_orm(primary_key, column_name = "issue_uuid")] - pub issue: IssueId, - #[sea_orm(primary_key, column_name = "user_uuid")] - pub user: UserId, - #[sea_orm(primary_key)] - pub reaction: String, - pub created_at: DateTimeUtc, -} - -impl Model { - pub fn reaction_enum(&self) -> Result { - self.reaction.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue_repo.rs b/libs/models/issues/issue_repo.rs deleted file mode 100644 index 77881f1..0000000 --- a/libs/models/issues/issue_repo.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{DateTimeUtc, IssueId, RepoId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue_repo")] -pub struct Model { - #[sea_orm(primary_key)] - pub issue: IssueId, - #[sea_orm(primary_key)] - pub repo: RepoId, - pub relation_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/issue_subscriber.rs b/libs/models/issues/issue_subscriber.rs deleted file mode 100644 index d615cdb..0000000 --- a/libs/models/issues/issue_subscriber.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::{DateTimeUtc, IssueId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "issue_subscriber")] -pub struct Model { - #[sea_orm(primary_key)] - pub issue: IssueId, - #[sea_orm(primary_key)] - pub user: UserId, - pub subscribed: bool, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/issues/mod.rs b/libs/models/issues/mod.rs deleted file mode 100644 index 64d4c65..0000000 --- a/libs/models/issues/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Issue state. Stored as `"open"` or `"closed"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum IssueState { - Open, - Closed, -} - -impl std::fmt::Display for IssueState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - IssueState::Open => write!(f, "open"), - IssueState::Closed => write!(f, "closed"), - } - } -} - -impl std::str::FromStr for IssueState { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "open" => Ok(IssueState::Open), - "closed" => Ok(IssueState::Closed), - _ => Err("unknown issue state"), - } - } -} - -/// Reaction / emoji type. Stored as `"thumbs_up"`, `"eyes"`, `"heart"`, `"party"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ReactionType { - ThumbsUp, - Eyes, - Heart, - Party, -} - -impl std::fmt::Display for ReactionType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ReactionType::ThumbsUp => write!(f, "thumbs_up"), - ReactionType::Eyes => write!(f, "eyes"), - ReactionType::Heart => write!(f, "heart"), - ReactionType::Party => write!(f, "party"), - } - } -} - -impl std::str::FromStr for ReactionType { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "thumbs_up" => Ok(ReactionType::ThumbsUp), - "eyes" => Ok(ReactionType::Eyes), - "heart" => Ok(ReactionType::Heart), - "party" => Ok(ReactionType::Party), - _ => Err("unknown reaction type"), - } - } -} - -pub use issue::Entity as Issue; -pub use issue_assignee::Entity as IssueAssignee; -pub use issue_comment::Entity as IssueComment; -pub use issue_comment_reaction::Entity as IssueCommentReaction; -pub use issue_label::Entity as IssueLabel; -pub use issue_pull_request::Entity as IssuePullRequest; -pub use issue_reaction::Entity as IssueReaction; -pub use issue_repo::Entity as IssueRepo; -pub use issue_subscriber::Entity as IssueSubscriber; - -pub mod issue; -pub mod issue_assignee; -pub mod issue_comment; -pub mod issue_comment_reaction; -pub mod issue_label; -pub mod issue_pull_request; -pub mod issue_reaction; -pub mod issue_repo; -pub mod issue_subscriber; diff --git a/libs/models/lib.rs b/libs/models/lib.rs deleted file mode 100644 index af062fb..0000000 --- a/libs/models/lib.rs +++ /dev/null @@ -1,45 +0,0 @@ -pub use sea_orm::entity::prelude::*; - -pub mod agent_task; -pub mod agents; -pub mod ai; -pub mod issues; -pub mod projects; -pub mod pull_request; -pub mod repos; -pub mod rooms; -pub mod system; -pub mod users; -pub use chrono::Utc as UtcClock; - -pub use agent_task::{AgentType, TaskStatus}; -pub type AgentTaskId = i64; -pub type UserId = Uuid; -pub type ProjectId = Uuid; -pub type RepoId = Uuid; -pub type RepoUpStreamId = Uuid; -pub type IssueId = Uuid; -pub type PullRequestId = Uuid; -pub type ModelId = Uuid; -pub type ModelVersionId = Uuid; -pub type ModelProviderId = Uuid; -pub type RoomId = Uuid; -pub type RoomCategoryId = Uuid; -pub type MessageId = Uuid; -pub type RoomThreadId = Uuid; -pub type AiSessionId = Uuid; -pub type AiConversationId = Uuid; -pub type AiMessageId = Uuid; -pub type Seq = i64; -pub type LabelId = i64; -pub type DateTimeUtc = chrono::DateTime; - -/// Input for batch tag embedding — shared between git hooks and agent embed service. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct TagEmbedInput { - pub repo_id: String, - pub repo_name: String, - pub project_id: String, - pub name: String, - pub description: Option, -} diff --git a/libs/models/projects/mod.rs b/libs/models/projects/mod.rs deleted file mode 100644 index 45c4fb4..0000000 --- a/libs/models/projects/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -/// Project member role. Stored as `"owner"`, `"admin"`, or `"member"` in the database. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -pub enum MemberRole { - Owner, - Admin, - Member, -} - -impl std::fmt::Display for MemberRole { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MemberRole::Owner => write!(f, "owner"), - MemberRole::Admin => write!(f, "admin"), - MemberRole::Member => write!(f, "member"), - } - } -} - -impl std::str::FromStr for MemberRole { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "owner" => Ok(MemberRole::Owner), - "admin" => Ok(MemberRole::Admin), - "member" => Ok(MemberRole::Member), - _ => Err("unknown member role"), - } - } -} - -pub use project::Entity as Project; -pub use project_access_log::Entity as ProjectAccessLog; -pub use project_context_setting::Entity as ProjectContextSetting; -pub mod project_context_setting; -pub use project_activity::Entity as ProjectActivity; -pub use project_audit_log::Entity as ProjectAuditLog; -pub use project_billing::Entity as ProjectBilling; -pub use project_billing_history::Entity as ProjectBillingHistory; -pub use project_board::Entity as ProjectBoard; -pub use project_board_card::Entity as ProjectBoardCard; -pub use project_board_column::Entity as ProjectBoardColumn; -pub use project_follow::Entity as ProjectFollow; -pub use project_history_name::Entity as ProjectHistoryName; -pub use project_label::Entity as ProjectLabel; -pub use project_skill::{Entity as ProjectSkill, SkillMetadata, SkillSource}; -pub mod project_skill; -pub use project_member_invitations::Entity as ProjectMemberInvitations; -pub use project_member_join_answers::Entity as ProjectMemberJoinAnswers; -pub use project_member_join_request::Entity as ProjectMemberJoinRequest; -pub use project_member_join_settings::Entity as ProjectMemberJoinSettings; -pub use project_members::Entity as ProjectMember; -pub use project_message_favorite::Entity as ProjectMessageFavorite; -pub use project_role_priority::Entity as ProjectRolePriority; -pub use project_watch::Entity as ProjectWatch; - -pub mod project; -pub mod project_access_log; -pub mod project_activity; -pub mod project_audit_log; -pub mod project_billing; -pub mod project_billing_history; -pub mod project_board; -pub mod project_board_card; -pub mod project_board_column; -pub mod project_follow; -pub mod project_history_name; -pub mod project_label; -pub mod project_like; -pub mod project_member_invitations; -pub mod project_member_join_answers; -pub mod project_member_join_request; -pub mod project_member_join_settings; -pub mod project_members; -pub mod project_message_favorite; -pub mod project_role_priority; -pub mod project_watch; diff --git a/libs/models/projects/project.rs b/libs/models/projects/project.rs deleted file mode 100644 index 1766fbb..0000000 --- a/libs/models/projects/project.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: ProjectId, - pub name: String, - pub display_name: String, - pub avatar_url: Option, - pub description: Option, - pub is_public: bool, - pub created_by: UserId, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_access_log.rs b/libs/models/projects/project_access_log.rs deleted file mode 100644 index 9dba021..0000000 --- a/libs/models/projects/project_access_log.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Stored as `"create"`, `"read"`, `"update"`, `"delete"`, `"transfer"` etc. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum AccessAction { - Create, - Read, - Update, - Delete, - Transfer, - Invite, - RemoveMember, -} - -impl std::fmt::Display for AccessAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AccessAction::Create => write!(f, "create"), - AccessAction::Read => write!(f, "read"), - AccessAction::Update => write!(f, "update"), - AccessAction::Delete => write!(f, "delete"), - AccessAction::Transfer => write!(f, "transfer"), - AccessAction::Invite => write!(f, "invite"), - AccessAction::RemoveMember => write!(f, "remove_member"), - } - } -} - -impl std::str::FromStr for AccessAction { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "create" => Ok(AccessAction::Create), - "read" => Ok(AccessAction::Read), - "update" => Ok(AccessAction::Update), - "delete" => Ok(AccessAction::Delete), - "transfer" => Ok(AccessAction::Transfer), - "invite" => Ok(AccessAction::Invite), - "remove_member" => Ok(AccessAction::RemoveMember), - _ => Err("unknown access action"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_access_log")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub project: ProjectId, - pub actor_uid: Option, - pub action: String, - pub ip_address: Option, - pub user_agent: Option, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_activity.rs b/libs/models/projects/project_activity.rs deleted file mode 100644 index 22bfb5a..0000000 --- a/libs/models/projects/project_activity.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// All possible event types for project activity feed. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ActivityEventType { - // Git / Repo - CommitPush, - BranchCreate, - BranchDelete, - BranchRename, - TagCreate, - TagDelete, - RepoCreate, - RepoUpdate, - RepoDelete, - RepoStar, - RepoUnstar, - RepoWatch, - RepoUnwatch, - IssueOpen, - IssueClose, - IssueReopen, - IssueUpdate, - IssueDelete, - IssueComment, - IssueLabelAdd, - IssueLabelRemove, - IssueAssigneeAdd, - IssueAssigneeRemove, - // Pull Requests - PrOpen, - PrMerge, - PrClose, - PrUpdate, - PrReview, - PrReviewComment, - RoomMessage, - RoomCreate, - RoomDelete, - RoomUpdate, - RoomPin, - RoomThread, - ProjectStar, - ProjectUnstar, - ProjectWatch, - ProjectUnwatch, - MemberAdd, - MemberRemove, - MemberRoleChange, - LabelCreate, - LabelUpdate, - LabelDelete, -} - -impl std::fmt::Display for ActivityEventType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - ActivityEventType::CommitPush => "commit_push", - ActivityEventType::BranchCreate => "branch_create", - ActivityEventType::BranchDelete => "branch_delete", - ActivityEventType::BranchRename => "branch_rename", - ActivityEventType::TagCreate => "tag_create", - ActivityEventType::TagDelete => "tag_delete", - ActivityEventType::RepoCreate => "repo_create", - ActivityEventType::RepoUpdate => "repo_update", - ActivityEventType::RepoDelete => "repo_delete", - ActivityEventType::RepoStar => "repo_star", - ActivityEventType::RepoUnstar => "repo_unstar", - ActivityEventType::RepoWatch => "repo_watch", - ActivityEventType::RepoUnwatch => "repo_unwatch", - ActivityEventType::IssueOpen => "issue_open", - ActivityEventType::IssueClose => "issue_close", - ActivityEventType::IssueReopen => "issue_reopen", - ActivityEventType::IssueUpdate => "issue_update", - ActivityEventType::IssueDelete => "issue_delete", - ActivityEventType::IssueComment => "issue_comment", - ActivityEventType::IssueLabelAdd => "issue_label_add", - ActivityEventType::IssueLabelRemove => "issue_label_remove", - ActivityEventType::IssueAssigneeAdd => "issue_assignee_add", - ActivityEventType::IssueAssigneeRemove => "issue_assignee_remove", - ActivityEventType::PrOpen => "pr_open", - ActivityEventType::PrMerge => "pr_merge", - ActivityEventType::PrClose => "pr_close", - ActivityEventType::PrUpdate => "pr_update", - ActivityEventType::PrReview => "pr_review", - ActivityEventType::PrReviewComment => "pr_review_comment", - ActivityEventType::RoomMessage => "room_message", - ActivityEventType::RoomCreate => "room_create", - ActivityEventType::RoomDelete => "room_delete", - ActivityEventType::RoomUpdate => "room_update", - ActivityEventType::RoomPin => "room_pin", - ActivityEventType::RoomThread => "room_thread", - ActivityEventType::ProjectStar => "project_star", - ActivityEventType::ProjectUnstar => "project_unstar", - ActivityEventType::ProjectWatch => "project_watch", - ActivityEventType::ProjectUnwatch => "project_unwatch", - ActivityEventType::MemberAdd => "member_add", - ActivityEventType::MemberRemove => "member_remove", - ActivityEventType::MemberRoleChange => "member_role_change", - ActivityEventType::LabelCreate => "label_create", - ActivityEventType::LabelUpdate => "label_update", - ActivityEventType::LabelDelete => "label_delete", - }; - write!(f, "{}", s) - } -} - -impl std::str::FromStr for ActivityEventType { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "commit_push" => Ok(ActivityEventType::CommitPush), - "branch_create" => Ok(ActivityEventType::BranchCreate), - "branch_delete" => Ok(ActivityEventType::BranchDelete), - "branch_rename" => Ok(ActivityEventType::BranchRename), - "tag_create" => Ok(ActivityEventType::TagCreate), - "tag_delete" => Ok(ActivityEventType::TagDelete), - "repo_create" => Ok(ActivityEventType::RepoCreate), - "repo_update" => Ok(ActivityEventType::RepoUpdate), - "repo_delete" => Ok(ActivityEventType::RepoDelete), - "repo_star" => Ok(ActivityEventType::RepoStar), - "repo_unstar" => Ok(ActivityEventType::RepoUnstar), - "repo_watch" => Ok(ActivityEventType::RepoWatch), - "repo_unwatch" => Ok(ActivityEventType::RepoUnwatch), - "issue_open" => Ok(ActivityEventType::IssueOpen), - "issue_close" => Ok(ActivityEventType::IssueClose), - "issue_reopen" => Ok(ActivityEventType::IssueReopen), - "issue_update" => Ok(ActivityEventType::IssueUpdate), - "issue_delete" => Ok(ActivityEventType::IssueDelete), - "issue_comment" => Ok(ActivityEventType::IssueComment), - "issue_label_add" => Ok(ActivityEventType::IssueLabelAdd), - "issue_label_remove" => Ok(ActivityEventType::IssueLabelRemove), - "issue_assignee_add" => Ok(ActivityEventType::IssueAssigneeAdd), - "issue_assignee_remove" => Ok(ActivityEventType::IssueAssigneeRemove), - "pr_open" => Ok(ActivityEventType::PrOpen), - "pr_merge" => Ok(ActivityEventType::PrMerge), - "pr_close" => Ok(ActivityEventType::PrClose), - "pr_update" => Ok(ActivityEventType::PrUpdate), - "pr_review" => Ok(ActivityEventType::PrReview), - "pr_review_comment" => Ok(ActivityEventType::PrReviewComment), - "room_message" => Ok(ActivityEventType::RoomMessage), - "room_create" => Ok(ActivityEventType::RoomCreate), - "room_delete" => Ok(ActivityEventType::RoomDelete), - "room_update" => Ok(ActivityEventType::RoomUpdate), - "room_pin" => Ok(ActivityEventType::RoomPin), - "room_thread" => Ok(ActivityEventType::RoomThread), - "project_star" => Ok(ActivityEventType::ProjectStar), - "project_unstar" => Ok(ActivityEventType::ProjectUnstar), - "project_watch" => Ok(ActivityEventType::ProjectWatch), - "project_unwatch" => Ok(ActivityEventType::ProjectUnwatch), - "member_add" => Ok(ActivityEventType::MemberAdd), - "member_remove" => Ok(ActivityEventType::MemberRemove), - "member_role_change" => Ok(ActivityEventType::MemberRoleChange), - "label_create" => Ok(ActivityEventType::LabelCreate), - "label_update" => Ok(ActivityEventType::LabelUpdate), - "label_delete" => Ok(ActivityEventType::LabelDelete), - _ => Err("unknown activity event type"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_activity")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = true)] - pub id: i64, - pub project: ProjectId, - #[sea_orm(nullable)] - pub repo: Option, - pub actor: UserId, - #[sea_orm(column_type = "Text")] - pub event_type: String, - #[sea_orm(nullable)] - pub event_id: Option, - #[sea_orm(nullable)] - pub event_sub_id: Option, - #[sea_orm(column_type = "Text")] - pub title: String, - #[sea_orm(nullable, column_type = "Text")] - pub content: Option, - #[sea_orm(nullable, column_type = "JsonBinary")] - pub metadata: Option, - pub is_private: bool, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_audit_log.rs b/libs/models/projects/project_audit_log.rs deleted file mode 100644 index 2bbe1e3..0000000 --- a/libs/models/projects/project_audit_log.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Audit action types, stored as strings like `"create"`, `"update"`, `"delete"`, `"transfer"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum AuditAction { - Create, - Update, - Delete, - Transfer, - Rename, - SettingsChange, -} - -impl std::fmt::Display for AuditAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AuditAction::Create => write!(f, "create"), - AuditAction::Update => write!(f, "update"), - AuditAction::Delete => write!(f, "delete"), - AuditAction::Transfer => write!(f, "transfer"), - AuditAction::Rename => write!(f, "rename"), - AuditAction::SettingsChange => write!(f, "settings_change"), - } - } -} - -impl std::str::FromStr for AuditAction { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "create" => Ok(AuditAction::Create), - "update" => Ok(AuditAction::Update), - "delete" => Ok(AuditAction::Delete), - "transfer" => Ok(AuditAction::Transfer), - "rename" => Ok(AuditAction::Rename), - "settings_change" => Ok(AuditAction::SettingsChange), - _ => Err("unknown audit action"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_audit_log")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = true)] - pub id: i64, - pub project: ProjectId, - pub actor: UserId, - #[sea_orm(column_type = "Text")] - pub action: String, - #[sea_orm(column_type = "JsonBinary", nullable)] - pub details: Option, - pub ip_address: Option, - pub user_agent: Option, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_billing.rs b/libs/models/projects/project_billing.rs deleted file mode 100644 index e3df5f6..0000000 --- a/libs/models/projects/project_billing.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Per-project billing account holding the current balance. -/// First project per user gets $20; subsequent projects get $0. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_billing")] -pub struct Model { - #[sea_orm(primary_key)] - #[sea_orm(column_name = "project_uuid")] - pub project: ProjectId, - #[sea_orm(column_type = "Decimal(Some((20, 4)))")] - pub balance: Decimal, - #[sea_orm(column_type = "Text")] - pub currency: String, - #[sea_orm(column_name = "user_uuid")] - pub user: Option, - /// Whether the initial credit (first project $20 bonus) was granted. - #[sea_orm(default_value = false)] - pub initial_credit_granted: bool, - /// Whether this project belongs to a pro user (monthly quota applies). - #[sea_orm(default_value = false)] - pub is_pro: bool, - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub monthly_quota: Decimal, - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub month_used: Decimal, - pub cycle_start: Option, - pub cycle_end: Option, - pub updated_at: DateTimeUtc, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_billing_history.rs b/libs/models/projects/project_billing_history.rs deleted file mode 100644 index c456aee..0000000 --- a/libs/models/projects/project_billing_history.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Billing transaction history for a project. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_billing_history")] -pub struct Model { - #[sea_orm(primary_key)] - pub uid: Uuid, - pub project: ProjectId, - pub user: Option, - #[sea_orm(column_type = "Decimal(Some((20, 4)))")] - pub amount: Decimal, - #[sea_orm(column_type = "Text")] - pub currency: String, - #[sea_orm(column_type = "Text")] - pub reason: String, - #[sea_orm(column_type = "JsonBinary", nullable)] - pub extra: Option, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_board.rs b/libs/models/projects/project_board.rs deleted file mode 100644 index 7ceff66..0000000 --- a/libs/models/projects/project_board.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_board")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - #[sea_orm(column_name = "project_uuid")] - pub project: ProjectId, - pub name: String, - pub description: Option, - pub created_by: UserId, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_board_card.rs b/libs/models/projects/project_board_card.rs deleted file mode 100644 index 7351f77..0000000 --- a/libs/models/projects/project_board_card.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_board_card")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - #[sea_orm(column_name = "column_uuid")] - pub column: Uuid, - pub issue_id: Option, - pub project: Option, - pub title: String, - pub description: Option, - pub position: i32, - pub assignee_id: Option, - pub due_date: Option, - pub priority: Option, - pub created_by: UserId, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_board_column.rs b/libs/models/projects/project_board_column.rs deleted file mode 100644 index 75259a9..0000000 --- a/libs/models/projects/project_board_column.rs +++ /dev/null @@ -1,20 +0,0 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_board_column")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - #[sea_orm(column_name = "board_uuid")] - pub board: Uuid, - pub name: String, - pub position: i32, - pub wip_limit: Option, - pub color: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_context_setting.rs b/libs/models/projects/project_context_setting.rs deleted file mode 100644 index e3cd348..0000000 --- a/libs/models/projects/project_context_setting.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::{DateTimeUtc, ProjectId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Project-level context management settings. -/// Controls compaction (sliding window) and RAG behavior. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, DeriveEntityModel)] -#[sea_orm(table_name = "project_context_setting")] -pub struct Model { - #[sea_orm(primary_key)] - pub project_id: ProjectId, - /// Context window size in tokens (e.g. 128000 for Claude). - /// When actual usage reaches this * compaction_threshold, trigger compaction. - pub context_window_tokens: i32, - /// Trigger compaction when usage exceeds this fraction of context_window_tokens. - /// Default 0.8 (80%). Valid range: 0.5 - 0.9. - pub compaction_threshold: f32, - /// Compressed summary must not exceed this fraction of context_window_tokens. - /// Default 0.2 (20%). Valid range: 0.05 - 0.3. - pub compaction_max_summary_ratio: f32, - /// Enable RAG for room conversations. - pub rag_enabled: bool, - /// RAG cross-session: room messages searchable across conversations. - pub rag_cross_session: bool, - /// Maximum RAG results to include in context. - pub rag_max_results: i32, - /// Minimum relevance score for RAG results (0.0 - 1.0). - pub rag_min_score: f32, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_follow.rs b/libs/models/projects/project_follow.rs deleted file mode 100644 index 047fb79..0000000 --- a/libs/models/projects/project_follow.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_follow")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub project: ProjectId, - pub user: UserId, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_history_name.rs b/libs/models/projects/project_history_name.rs deleted file mode 100644 index 5745dab..0000000 --- a/libs/models/projects/project_history_name.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{DateTimeUtc, ProjectId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_history_name")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub project_uid: ProjectId, - pub history_name: String, - pub changed_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_label.rs b/libs/models/projects/project_label.rs deleted file mode 100644 index 52c8eb9..0000000 --- a/libs/models/projects/project_label.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{DateTimeUtc, ProjectId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_label")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "project_uuid")] - pub project: ProjectId, - #[sea_orm(column_name = "label_id")] - pub label: i64, - pub relation_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_like.rs b/libs/models/projects/project_like.rs deleted file mode 100644 index dd6f94f..0000000 --- a/libs/models/projects/project_like.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::{ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[sea_orm::model] -#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_like")] -pub struct Model { - #[sea_orm(primary_key, auto_increment = false)] - pub project: ProjectId, - #[sea_orm(primary_key, auto_increment = false)] - pub user: UserId, - pub created_at: DateTimeUtc, -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_member_invitations.rs b/libs/models/projects/project_member_invitations.rs deleted file mode 100644 index 5a63966..0000000 --- a/libs/models/projects/project_member_invitations.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::MemberRole; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_member_invitations")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub project: ProjectId, - pub user: UserId, - pub invited_by: UserId, - pub scope: String, - pub accepted: bool, - pub accepted_at: Option, - pub rejected: bool, - pub rejected_at: Option, - pub created_at: DateTimeUtc, -} - -impl Model { - pub fn scope_role(&self) -> Result { - self.scope.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_member_join_answers.rs b/libs/models/projects/project_member_join_answers.rs deleted file mode 100644 index 8314fc1..0000000 --- a/libs/models/projects/project_member_join_answers.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_member_join_answers")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub project: ProjectId, - pub user: UserId, - pub request_id: i64, - pub question: String, - pub answer: String, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_member_join_request.rs b/libs/models/projects/project_member_join_request.rs deleted file mode 100644 index 3a8536f..0000000 --- a/libs/models/projects/project_member_join_request.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Stored as `"pending"`, `"approved"`, `"rejected"`, or `"cancelled"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum JoinRequestStatus { - Pending, - Approved, - Rejected, - Cancelled, -} - -impl std::fmt::Display for JoinRequestStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - JoinRequestStatus::Pending => write!(f, "pending"), - JoinRequestStatus::Approved => write!(f, "approved"), - JoinRequestStatus::Rejected => write!(f, "rejected"), - JoinRequestStatus::Cancelled => write!(f, "cancelled"), - } - } -} - -impl std::str::FromStr for JoinRequestStatus { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "pending" => Ok(JoinRequestStatus::Pending), - "approved" => Ok(JoinRequestStatus::Approved), - "rejected" => Ok(JoinRequestStatus::Rejected), - "cancelled" => Ok(JoinRequestStatus::Cancelled), - _ => Err("unknown join request status"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_member_join_request")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub project: ProjectId, - pub user: UserId, - pub status: String, - pub message: Option, - pub processed_by: Option, - pub processed_at: Option, - pub reject_reason: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_member_join_settings.rs b/libs/models/projects/project_member_join_settings.rs deleted file mode 100644 index 352ca4e..0000000 --- a/libs/models/projects/project_member_join_settings.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{DateTimeUtc, ProjectId}; -use sea_orm::JsonValue; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_member_join_settings")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub project: ProjectId, - pub require_approval: bool, - pub require_questions: bool, - pub questions: JsonValue, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_members.rs b/libs/models/projects/project_members.rs deleted file mode 100644 index 32b7ce7..0000000 --- a/libs/models/projects/project_members.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::MemberRole; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_members")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "project_uuid")] - pub project: ProjectId, - #[sea_orm(column_name = "user_uuid")] - pub user: UserId, - pub scope: String, - pub joined_at: DateTimeUtc, -} - -impl Model { - pub fn scope_role(&self) -> Result { - self.scope.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_message_favorite.rs b/libs/models/projects/project_message_favorite.rs deleted file mode 100644 index 6c6ee55..0000000 --- a/libs/models/projects/project_message_favorite.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{DateTimeUtc, MessageId, ProjectId, RoomId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// User-saved room messages scoped to a project. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_message_favorite")] -pub struct Model { - #[sea_orm(primary_key)] - pub uid: Uuid, - pub project: ProjectId, - pub room: RoomId, - pub message: MessageId, - #[sea_orm(column_name = "user_uuid")] - pub user: UserId, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_role_priority.rs b/libs/models/projects/project_role_priority.rs deleted file mode 100644 index 96354ee..0000000 --- a/libs/models/projects/project_role_priority.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{DateTimeUtc, ProjectId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_role_priority")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "project_uuid")] - pub project: ProjectId, - pub role_key: String, - pub display_name: String, - pub priority: i32, - pub color: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/projects/project_skill.rs b/libs/models/projects/project_skill.rs deleted file mode 100644 index 0a8c004..0000000 --- a/libs/models/projects/project_skill.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Skill registered to a project. -//! -//! Skills can be sourced manually (by project admin) or auto-discovered from -//! repositories within the project (via `.claude/skills/` directory scanning). - -use crate::{DateTimeUtc, ProjectId, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Skill source: `"manual"` or `"repo"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum SkillSource { - Manual, - Repo, -} - -impl std::fmt::Display for SkillSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SkillSource::Manual => write!(f, "manual"), - SkillSource::Repo => write!(f, "repo"), - } - } -} - -impl std::str::FromStr for SkillSource { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "manual" => Ok(SkillSource::Manual), - "repo" => Ok(SkillSource::Repo), - _ => Err("unknown skill source"), - } - } -} - -/// Parsed frontmatter from a skill's SKILL.md file. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct SkillMetadata { - /// Human-readable name (falls back to slug/folder name). - #[serde(default)] - pub name: Option, - /// Short description of what the skill does. - #[serde(default)] - pub description: Option, - /// SPDX license identifier. - #[serde(default)] - pub license: Option, - /// Compatibility notes (e.g. "Requires openspec CLI"). - #[serde(default)] - pub compatibility: Option, - /// Free-form metadata from the frontmatter. - #[serde(default)] - pub metadata: serde_json::Value, -} - -impl From for SkillMetadata { - fn from(v: serde_json::Value) -> Self { - serde_json::from_value(v).unwrap_or_default() - } -} - -/// Skill record persisted in the `project_skill` table. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_skill")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - /// Project this skill belongs to. - pub project_uuid: ProjectId, - /// URL-safe identifier, unique within a project. - pub slug: String, - /// Display name (extracted from frontmatter or folder name). - pub name: String, - /// Optional short description. - pub description: Option, - /// `"manual"` or `"repo"`. - pub source: String, - /// If source=repo, the repo this skill was discovered from. - #[sea_orm(nullable)] - pub repo_id: Option, - /// If source=repo, the commit SHA where the skill was found. - #[sea_orm(nullable)] - pub commit_sha: Option, - /// If source=repo, the blob SHA of the SKILL.md file. - #[sea_orm(nullable)] - pub blob_hash: Option, - /// Raw markdown content (SKILL.md body after frontmatter). - pub content: String, - /// Full frontmatter as JSON. - #[sea_orm(column_type = "JsonBinary")] - pub metadata: serde_json::Value, - /// Whether this skill is currently active. - pub enabled: bool, - /// Who added this skill (null for repo-sourced skills). - #[sea_orm(nullable)] - pub created_by: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} - -impl Model { - pub fn source_enum(&self) -> Result { - self.source.parse() - } - - pub fn metadata_parsed(&self) -> SkillMetadata { - SkillMetadata::from(self.metadata.clone()) - } -} diff --git a/libs/models/projects/project_watch.rs b/libs/models/projects/project_watch.rs deleted file mode 100644 index ca2794f..0000000 --- a/libs/models/projects/project_watch.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "project_watch")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub project: ProjectId, - pub user: UserId, - pub notifications_enabled: bool, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/pull_request/mod.rs b/libs/models/pull_request/mod.rs deleted file mode 100644 index 3d3ea48..0000000 --- a/libs/models/pull_request/mod.rs +++ /dev/null @@ -1,93 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Overall PR status. Combines review decision and merge readiness into a -/// single enum for quick querying. -/// -/// Stored as `"draft"`, `"open"`, `"approved"`, `"changes_requested"`, -/// `"conflict"`, `"merged"`, or `"closed"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum PrStatus { - Draft, - Open, - Approved, - ChangesRequested, - Conflict, - Merged, - Closed, -} - -impl std::fmt::Display for PrStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PrStatus::Draft => write!(f, "draft"), - PrStatus::Open => write!(f, "open"), - PrStatus::Approved => write!(f, "approved"), - PrStatus::ChangesRequested => write!(f, "changes_requested"), - PrStatus::Conflict => write!(f, "conflict"), - PrStatus::Merged => write!(f, "merged"), - PrStatus::Closed => write!(f, "closed"), - } - } -} - -impl std::str::FromStr for PrStatus { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "draft" => Ok(PrStatus::Draft), - "open" => Ok(PrStatus::Open), - "approved" => Ok(PrStatus::Approved), - "changes_requested" => Ok(PrStatus::ChangesRequested), - "conflict" => Ok(PrStatus::Conflict), - "merged" => Ok(PrStatus::Merged), - "closed" => Ok(PrStatus::Closed), - _ => Err("unknown PR status"), - } - } -} - -/// Overall review decision from a single reviewer. Stored as `"pending"`, -/// `"approved"`, `"changes_requested"`, or `"comment"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum ReviewState { - Pending, - Approved, - ChangesRequested, - Comment, -} - -impl std::fmt::Display for ReviewState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ReviewState::Pending => write!(f, "pending"), - ReviewState::Approved => write!(f, "approved"), - ReviewState::ChangesRequested => write!(f, "changes_requested"), - ReviewState::Comment => write!(f, "comment"), - } - } -} - -impl std::str::FromStr for ReviewState { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "pending" => Ok(ReviewState::Pending), - "approved" => Ok(ReviewState::Approved), - "changes_requested" => Ok(ReviewState::ChangesRequested), - "comment" => Ok(ReviewState::Comment), - _ => Err("unknown review state"), - } - } -} - -pub use pull_request::Entity as PullRequest; -pub use pull_request_commit::Entity as PullRequestCommit; -pub use pull_request_review::Entity as PullRequestReview; -pub use pull_request_review_comment::Entity as PullRequestReviewComment; -pub use pull_request_review_request::Entity as PullRequestReviewRequest; - -pub mod pull_request; -pub mod pull_request_commit; -pub mod pull_request_review; -pub mod pull_request_review_comment; -pub mod pull_request_review_request; diff --git a/libs/models/pull_request/pull_request.rs b/libs/models/pull_request/pull_request.rs deleted file mode 100644 index f878069..0000000 --- a/libs/models/pull_request/pull_request.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::{DateTimeUtc, IssueId, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::PrStatus; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "pull_request")] -pub struct Model { - #[sea_orm(primary_key)] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub number: i64, - pub issue: IssueId, - pub title: String, - pub body: Option, - pub author: UserId, - pub base: String, - pub head: String, - pub status: String, - pub merged_by: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, - pub merged_at: Option, - pub created_by_ai: bool, -} - -impl Model { - pub fn status_enum(&self) -> Result { - self.status.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/pull_request/pull_request_commit.rs b/libs/models/pull_request/pull_request_commit.rs deleted file mode 100644 index 95b6ccd..0000000 --- a/libs/models/pull_request/pull_request_commit.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{DateTimeUtc, RepoId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "pull_request_commit")] -pub struct Model { - #[sea_orm(primary_key)] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub number: i64, - #[sea_orm(primary_key)] - pub commit: String, - pub message: String, - pub author_name: String, - pub author_email: String, - pub authored_at: DateTimeUtc, - pub committer_name: String, - pub committer_email: String, - pub committed_at: DateTimeUtc, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/pull_request/pull_request_review.rs b/libs/models/pull_request/pull_request_review.rs deleted file mode 100644 index 58af978..0000000 --- a/libs/models/pull_request/pull_request_review.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::ReviewState; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "pull_request_review")] -pub struct Model { - #[sea_orm(primary_key)] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub number: i64, - #[sea_orm(primary_key)] - pub reviewer: UserId, - pub state: String, - pub body: Option, - pub submitted_at: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -impl Model { - pub fn state_enum(&self) -> Result { - self.state.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/pull_request/pull_request_review_comment.rs b/libs/models/pull_request/pull_request_review_comment.rs deleted file mode 100644 index e6659d5..0000000 --- a/libs/models/pull_request/pull_request_review_comment.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "pull_request_review_comment")] -pub struct Model { - #[sea_orm(primary_key)] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub number: i64, - #[sea_orm(primary_key)] - pub id: i64, - /// Optional reviewer association. - pub review: Option, - /// File path for inline comments. - pub path: Option, - /// "LEFT" or "RIGHT" for side-by-side positioning. - pub side: Option, - /// Line number in the new (right) file. - pub line: Option, - /// Line number in the old (left) file. - pub old_line: Option, - pub body: String, - pub author: UserId, - /// Whether this comment thread has been resolved. - pub resolved: bool, - /// ID of the parent comment this replies to (null = root comment). - pub in_reply_to: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/pull_request/pull_request_review_request.rs b/libs/models/pull_request/pull_request_review_request.rs deleted file mode 100644 index a60f229..0000000 --- a/libs/models/pull_request/pull_request_review_request.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Tracks a review request: a PR author has asked a specific user to review. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "pull_request_review_request")] -pub struct Model { - #[sea_orm(primary_key)] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub number: i64, - #[sea_orm(primary_key)] - pub reviewer: UserId, - /// Who requested this review. - pub requested_by: UserId, - pub requested_at: DateTimeUtc, - /// When the reviewer was dismissed (null = still pending). - pub dismissed_at: Option, - /// Who dismissed the request (null if not dismissed). - pub dismissed_by: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/mod.rs b/libs/models/repos/mod.rs deleted file mode 100644 index 20fdd90..0000000 --- a/libs/models/repos/mod.rs +++ /dev/null @@ -1,155 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Repository collaborator role. Stored as `"read"`, `"write"`, or `"admin"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum CollabRole { - Read, - Write, - Admin, -} - -impl std::fmt::Display for CollabRole { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - CollabRole::Read => write!(f, "read"), - CollabRole::Write => write!(f, "write"), - CollabRole::Admin => write!(f, "admin"), - } - } -} - -impl std::str::FromStr for CollabRole { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "read" => Ok(CollabRole::Read), - "write" => Ok(CollabRole::Write), - "admin" => Ok(CollabRole::Admin), - _ => Err("unknown collaborator role"), - } - } -} - -/// LFS lock type. Stored as `"rd"`, `"rn"` or `"rw"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum LockType { - Rd, - Rn, - Rw, -} - -impl std::fmt::Display for LockType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LockType::Rd => write!(f, "rd"), - LockType::Rn => write!(f, "rn"), - LockType::Rw => write!(f, "rw"), - } - } -} - -impl std::str::FromStr for LockType { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "rd" => Ok(LockType::Rd), - "rn" => Ok(LockType::Rn), - "rw" => Ok(LockType::Rw), - _ => Err("unknown lock type"), - } - } -} - -/// Upstream sync direction. Stored as `"push"` or `"pull"`. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum SyncDirection { - Push, - Pull, -} - -impl std::fmt::Display for SyncDirection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SyncDirection::Push => write!(f, "push"), - SyncDirection::Pull => write!(f, "pull"), - } - } -} - -impl std::str::FromStr for SyncDirection { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "push" => Ok(SyncDirection::Push), - "pull" => Ok(SyncDirection::Pull), - _ => Err("unknown sync direction"), - } - } -} - -/// Upstream sync status. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum SyncStatus { - Idle, - Syncing, - Success, - Failed, -} - -impl std::fmt::Display for SyncStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SyncStatus::Idle => write!(f, "idle"), - SyncStatus::Syncing => write!(f, "syncing"), - SyncStatus::Success => write!(f, "success"), - SyncStatus::Failed => write!(f, "failed"), - } - } -} - -impl std::str::FromStr for SyncStatus { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "idle" => Ok(SyncStatus::Idle), - "syncing" => Ok(SyncStatus::Syncing), - "success" => Ok(SyncStatus::Success), - "failed" => Ok(SyncStatus::Failed), - _ => Err("unknown sync status"), - } - } -} - -pub use repo::Entity as Repo; -pub use repo_branch::Entity as RepoBranch; -pub use repo_branch_protect::Entity as RepoBranchProtect; -pub use repo_collaborator::Entity as RepoCollaborator; -pub use repo_commit::Entity as RepoCommit; -pub use repo_fork::Entity as RepoFork; -pub use repo_history_name::Entity as RepoHistoryName; -pub use repo_hook::Entity as RepoHook; -pub use repo_lfs_lock::Entity as RepoLfsLock; -pub use repo_lfs_object::Entity as RepoLfsObject; -pub use repo_lock::Entity as RepoLock; -pub use repo_star::Entity as RepoStar; -pub use repo_tag::Entity as RepoTag; -pub use repo_upstream::Entity as RepoUpstream; -pub use repo_watch::Entity as RepoWatch; -pub use repo_webhook::Entity as RepoWebhook; - -pub mod repo; -pub mod repo_branch; -pub mod repo_branch_protect; -pub mod repo_collaborator; -pub mod repo_commit; -pub mod repo_fork; -pub mod repo_history_name; -pub mod repo_hook; -pub mod repo_lfs_lock; -pub mod repo_lfs_object; -pub mod repo_lock; -pub mod repo_star; -pub mod repo_tag; -pub mod repo_upstream; -pub mod repo_watch; -pub mod repo_webhook; diff --git a/libs/models/repos/repo.rs b/libs/models/repos/repo.rs deleted file mode 100644 index 96ebe3d..0000000 --- a/libs/models/repos/repo.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: RepoId, - pub repo_name: String, - pub project: ProjectId, - pub description: Option, - pub default_branch: String, - pub is_private: bool, - pub storage_path: String, - pub created_by: UserId, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, - #[sea_orm(default)] - pub ai_code_review_enabled: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_branch.rs b/libs/models/repos/repo_branch.rs deleted file mode 100644 index 0fdd343..0000000 --- a/libs/models/repos/repo_branch.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{DateTimeUtc, RepoId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_branch")] -pub struct Model { - #[sea_orm(primary_key)] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub name: String, - pub oid: String, - pub upstream: Option, - pub head: bool, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_branch_protect.rs b/libs/models/repos/repo_branch_protect.rs deleted file mode 100644 index 695f627..0000000 --- a/libs/models/repos/repo_branch_protect.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::RepoId; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_branch_protect")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - pub branch: String, - pub forbid_push: bool, - pub forbid_pull: bool, - pub forbid_merge: bool, - pub forbid_deletion: bool, - pub forbid_force_push: bool, - pub forbid_tag_push: bool, - pub required_approvals: i32, - pub dismiss_stale_reviews: bool, - pub require_linear_history: bool, - pub allow_fork_syncing: bool, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_collaborator.rs b/libs/models/repos/repo_collaborator.rs deleted file mode 100644 index 979b0f7..0000000 --- a/libs/models/repos/repo_collaborator.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::CollabRole; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_collaborator")] -pub struct Model { - #[sea_orm(primary_key)] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub user: UserId, - pub scope: String, - pub created_at: DateTimeUtc, -} - -impl Model { - pub fn scope_role(&self) -> Result { - self.scope.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_commit.rs b/libs/models/repos/repo_commit.rs deleted file mode 100644 index b1fb016..0000000 --- a/libs/models/repos/repo_commit.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::JsonValue; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_commit")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub repo: RepoId, - pub oid: String, - pub author_name: String, - pub author_email: String, - pub author: Option, - pub commiter_name: String, - pub commiter_email: String, - pub commiter: Option, - pub message: String, - pub parent: JsonValue, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_fork.rs b/libs/models/repos/repo_fork.rs deleted file mode 100644 index 82e5b46..0000000 --- a/libs/models/repos/repo_fork.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_fork")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub parent_repo: RepoId, - pub forked_repo: RepoId, - pub forked_by: UserId, - pub forked_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_history_name.rs b/libs/models/repos/repo_history_name.rs deleted file mode 100644 index 11487ab..0000000 --- a/libs/models/repos/repo_history_name.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, RepoId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_history_name")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - #[sea_orm(column_name = "project_uid")] - pub project: ProjectId, - pub name: String, - pub change_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_hook.rs b/libs/models/repos/repo_hook.rs deleted file mode 100644 index 25d6b40..0000000 --- a/libs/models/repos/repo_hook.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::{DateTimeUtc, RepoId}; -use sea_orm::JsonValue; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_hook")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - pub event: JsonValue, - pub script: String, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_lfs_lock.rs b/libs/models/repos/repo_lfs_lock.rs deleted file mode 100644 index 0a32d62..0000000 --- a/libs/models/repos/repo_lfs_lock.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::LockType; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_lfs_lock")] -pub struct Model { - #[sea_orm(primary_key)] - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub path: String, - pub lock_type: String, - #[sea_orm(column_name = "locked_by")] - pub locked_by: UserId, - pub locked_at: DateTimeUtc, - pub unlocked_at: Option, -} - -impl Model { - pub fn lock_type_enum(&self) -> Result { - self.lock_type.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_lfs_object.rs b/libs/models/repos/repo_lfs_object.rs deleted file mode 100644 index 6382087..0000000 --- a/libs/models/repos/repo_lfs_object.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_lfs_object")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub oid: String, - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - pub size: i64, - pub storage_path: String, - #[sea_orm(column_name = "uploaded_by")] - pub uploaded_by: Option, - pub uploaded_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_lock.rs b/libs/models/repos/repo_lock.rs deleted file mode 100644 index 31382e2..0000000 --- a/libs/models/repos/repo_lock.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::LockType; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_lock")] -pub struct Model { - #[sea_orm(primary_key)] - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub path: String, - pub lock_type: String, - #[sea_orm(column_name = "locked_by")] - pub locked_by: UserId, - pub acquired_at: DateTimeUtc, - pub released_at: Option, -} - -impl Model { - pub fn lock_type_enum(&self) -> Result { - self.lock_type.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_star.rs b/libs/models/repos/repo_star.rs deleted file mode 100644 index 9df706d..0000000 --- a/libs/models/repos/repo_star.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_star")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - #[sea_orm(column_name = "user_uuid")] - pub user: UserId, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_tag.rs b/libs/models/repos/repo_tag.rs deleted file mode 100644 index 4f5d18b..0000000 --- a/libs/models/repos/repo_tag.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_tag")] -pub struct Model { - #[sea_orm(primary_key)] - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - #[sea_orm(primary_key)] - pub name: String, - pub oid: String, - pub color: Option, - pub description: Option, - pub created_at: DateTimeUtc, - pub tagger_name: String, - pub tagger_email: String, - #[sea_orm(column_name = "tagger_uuid")] - pub tagger: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_upstream.rs b/libs/models/repos/repo_upstream.rs deleted file mode 100644 index 828c4f5..0000000 --- a/libs/models/repos/repo_upstream.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::{DateTimeUtc, RepoId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_upstream")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - pub source_url: String, - pub direction: String, - pub schedule_cron: Option, - pub last_run_at: Option, - pub next_run_at: Option, - pub status: String, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -impl Model { - pub fn direction_enum(&self) -> Result { - self.direction.parse() - } - pub fn status_enum(&self) -> Result { - self.status.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_watch.rs b/libs/models/repos/repo_watch.rs deleted file mode 100644 index 048e3fa..0000000 --- a/libs/models/repos/repo_watch.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{DateTimeUtc, RepoId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_watch")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "user_uuid")] - pub user: UserId, - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - pub show_dashboard: bool, - pub notify_email: bool, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/repos/repo_webhook.rs b/libs/models/repos/repo_webhook.rs deleted file mode 100644 index 2f457eb..0000000 --- a/libs/models/repos/repo_webhook.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{DateTimeUtc, RepoId}; -use sea_orm::JsonValue; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "repo_webhook")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "repo_uuid")] - pub repo: RepoId, - pub event: JsonValue, - pub url: Option, - pub access_key: Option, - pub secret_key: Option, - pub created_at: DateTimeUtc, - pub last_delivered_at: Option, - pub touch_count: i64, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/mod.rs b/libs/models/rooms/mod.rs deleted file mode 100644 index af5689d..0000000 --- a/libs/models/rooms/mod.rs +++ /dev/null @@ -1,109 +0,0 @@ -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -pub enum MessageSenderType { - #[sea_orm(string_value = "user")] - User, - #[sea_orm(string_value = "ai")] - Ai, - #[sea_orm(string_value = "system")] - System, - #[sea_orm(string_value = "webhook")] - Webhook, - #[sea_orm(string_value = "tool")] - Tool, -} - -impl std::fmt::Display for MessageSenderType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MessageSenderType::User => write!(f, "user"), - MessageSenderType::Ai => write!(f, "ai"), - MessageSenderType::System => write!(f, "system"), - MessageSenderType::Webhook => write!(f, "webhook"), - MessageSenderType::Tool => write!(f, "tool"), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -pub enum MessageContentType { - #[sea_orm(string_value = "text")] - Text, - #[sea_orm(string_value = "image")] - Image, - #[sea_orm(string_value = "audio")] - Audio, - #[sea_orm(string_value = "video")] - Video, - #[sea_orm(string_value = "file")] - File, -} - -impl std::fmt::Display for MessageContentType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MessageContentType::Text => write!(f, "text"), - MessageContentType::Image => write!(f, "image"), - MessageContentType::Audio => write!(f, "audio"), - MessageContentType::Video => write!(f, "video"), - MessageContentType::File => write!(f, "file"), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -pub enum ToolCallStatus { - #[sea_orm(string_value = "pending")] - Pending, - #[sea_orm(string_value = "running")] - Running, - #[sea_orm(string_value = "success")] - Success, - #[sea_orm(string_value = "failed")] - Failed, - #[sea_orm(string_value = "retrying")] - Retrying, -} - -impl std::fmt::Display for ToolCallStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ToolCallStatus::Pending => write!(f, "pending"), - ToolCallStatus::Running => write!(f, "running"), - ToolCallStatus::Success => write!(f, "success"), - ToolCallStatus::Failed => write!(f, "failed"), - ToolCallStatus::Retrying => write!(f, "retrying"), - } - } -} - -pub use room::Entity as Room; -pub use room_access::Entity as RoomAccess; -pub use room_ai::Entity as RoomAi; -pub use room_attachment::Entity as RoomAttachment; -pub use room_category::Entity as RoomCategory; -pub use room_message::Entity as RoomMessage; -pub use room_message_edit_history::Entity as RoomMessageEditHistory; -pub use room_message_reaction::Entity as RoomMessageReaction; -pub use room_notifications::Entity as RoomNotification; -pub use room_notifications::NotificationType; -pub use room_pin::Entity as RoomPin; -pub use room_thread::Entity as RoomThread; -pub use room_user_state::Entity as RoomUserState; -pub mod room; -pub mod room_access; -pub mod room_ai; -pub mod room_attachment; -pub mod room_category; -pub mod room_message; -pub mod room_message_edit_history; -pub mod room_message_reaction; -pub mod room_notifications; -pub mod room_pin; -pub mod room_thread; -pub mod room_user_state; diff --git a/libs/models/rooms/room.rs b/libs/models/rooms/room.rs deleted file mode 100644 index a469445..0000000 --- a/libs/models/rooms/room.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, RoomCategoryId, RoomId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: RoomId, - pub project: ProjectId, - pub room_name: String, - #[sea_orm(column_name = "public")] - pub public: bool, - pub category: Option, - pub created_by: UserId, - pub created_at: DateTimeUtc, - pub last_msg_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room_category::Entity", - from = "Column::Category", - to = "super::room_category::Column::Id" - )] - Category, - #[sea_orm(has_many = "super::room_message::Entity")] - Messages, - #[sea_orm(has_many = "super::room_user_state::Entity")] - UserStates, - #[sea_orm(has_many = "super::room_thread::Entity")] - Threads, - #[sea_orm(has_many = "super::room_ai::Entity")] - Ais, - #[sea_orm(has_many = "super::room_pin::Entity")] - Pins, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Category.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Messages.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::UserStates.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Threads.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Ais.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Pins.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_access.rs b/libs/models/rooms/room_access.rs deleted file mode 100644 index 6f16482..0000000 --- a/libs/models/rooms/room_access.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::{DateTimeUtc, RoomId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Explicit access grant for private rooms. -/// Public rooms do not need rows here — project membership suffices. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_access")] -pub struct Model { - #[sea_orm(primary_key)] - pub room: RoomId, - #[sea_orm(primary_key)] - pub user: UserId, - pub granted_by: UserId, - pub granted_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::Room", - to = "super::room::Column::Id" - )] - Room, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_ai.rs b/libs/models/rooms/room_ai.rs deleted file mode 100644 index f37c278..0000000 --- a/libs/models/rooms/room_ai.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::{DateTimeUtc, ModelId, ModelVersionId, RoomId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_ai")] -pub struct Model { - #[sea_orm(primary_key)] - pub room: RoomId, - #[sea_orm(primary_key)] - pub model: ModelId, - pub version: Option, - pub call_count: i64, - pub last_call_at: Option, - pub history_limit: Option, - pub system_prompt: Option, - pub temperature: Option, - pub max_tokens: Option, - pub use_exact: bool, - pub think: bool, - pub stream: bool, - pub min_score: Option, - /// Agent type: "chat" (default), "react" (ReAct reasoning), - /// "cot" (Chain-of-Thought), "rewoo" (Plan→Execute→Synthesize), - /// or "reflexion" (Generate→Critique→Revise). - pub agent_type: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::Room", - to = "super::room::Column::Id" - )] - Room, - #[sea_orm( - belongs_to = "super::super::agents::model::Entity", - from = "Column::Model", - to = "super::super::agents::model::Column::Id" - )] - AiModel, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::AiModel.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_attachment.rs b/libs/models/rooms/room_attachment.rs deleted file mode 100644 index 3be74e9..0000000 --- a/libs/models/rooms/room_attachment.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{DateTimeUtc, MessageId, RoomId, UserId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "room_attachment")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub room: RoomId, - pub message: MessageId, - pub uploader: UserId, - pub file_name: String, - pub file_size: i64, - pub content_type: String, - pub s3_key: String, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_category.rs b/libs/models/rooms/room_category.rs deleted file mode 100644 index d131dbc..0000000 --- a/libs/models/rooms/room_category.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{DateTimeUtc, ProjectId, RoomCategoryId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_category")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: RoomCategoryId, - #[sea_orm(column_name = "project_uuid")] - pub project: ProjectId, - pub name: String, - pub position: i32, - pub created_by: UserId, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm(has_many = "super::room::Entity")] - Rooms, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Rooms.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_message.rs b/libs/models/rooms/room_message.rs deleted file mode 100644 index 9c9dc81..0000000 --- a/libs/models/rooms/room_message.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::{DateTimeUtc, MessageId, RoomId, RoomThreadId, Seq, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -use super::{MessageContentType, MessageSenderType}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_message")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: MessageId, - pub seq: Seq, - pub room: RoomId, - pub sender_type: MessageSenderType, - pub sender_id: Option, - /// AI model ID — set when sender_type = "ai", used for display name lookups. - pub model_id: Option, - pub thread: Option, - pub in_reply_to: Option, - pub content: String, - pub content_type: MessageContentType, - /// Accumulated AI reasoning/thinking text. - pub thinking_content: Option, - pub edited_at: Option, - pub send_at: DateTimeUtc, - pub revoked: Option, - pub revoked_by: Option, - #[sea_orm(ignore)] - pub content_tsv: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::Room", - to = "super::room::Column::Id" - )] - Room, - #[sea_orm( - belongs_to = "super::room_thread::Entity", - from = "Column::Thread", - to = "super::room_thread::Column::Id" - )] - Thread, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Thread.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_message_edit_history.rs b/libs/models/rooms/room_message_edit_history.rs deleted file mode 100644 index 6085bea..0000000 --- a/libs/models/rooms/room_message_edit_history.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::{DateTimeUtc, MessageId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_message_edit_history")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub message: MessageId, - pub user: UserId, - /// Previous content before edit - pub old_content: String, - /// New content after edit - pub new_content: String, - pub edited_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room_message::Entity", - from = "Column::Message", - to = "super::room_message::Column::Id" - )] - Message, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Message.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_message_reaction.rs b/libs/models/rooms/room_message_reaction.rs deleted file mode 100644 index 28af258..0000000 --- a/libs/models/rooms/room_message_reaction.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::{DateTimeUtc, MessageId, RoomId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_message_reaction")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub room: RoomId, - pub message: MessageId, - pub user: UserId, - pub emoji: String, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::Room", - to = "super::room::Column::Id" - )] - Room, - #[sea_orm( - belongs_to = "super::room_message::Entity", - from = "Column::Message", - to = "super::room_message::Column::Id" - )] - Message, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Message.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} - -/// Aggregated reaction counts per message for API responses -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MessageReactionSummary { - pub message_id: UserId, - /// emoji -> { count, reacted_by_current_user } - pub reactions: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReactionGroup { - pub emoji: String, - pub count: i64, - pub reacted_by_me: bool, - /// Sample of users who reacted (first 3) - pub users: Vec, -} diff --git a/libs/models/rooms/room_notifications.rs b/libs/models/rooms/room_notifications.rs deleted file mode 100644 index 4ec9e69..0000000 --- a/libs/models/rooms/room_notifications.rs +++ /dev/null @@ -1,88 +0,0 @@ -use chrono::{DateTime, Utc}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] -#[serde(rename_all = "snake_case")] -pub enum NotificationType { - #[sea_orm(string_value = "mention")] - Mention, - #[sea_orm(string_value = "invitation")] - Invitation, - #[sea_orm(string_value = "role_change")] - RoleChange, - #[sea_orm(string_value = "room_created")] - RoomCreated, - #[sea_orm(string_value = "room_deleted")] - RoomDeleted, - #[sea_orm(string_value = "system_announcement")] - SystemAnnouncement, - #[sea_orm(string_value = "project_invitation")] - ProjectInvitation, -} - -impl std::fmt::Display for NotificationType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - NotificationType::Mention => "mention", - NotificationType::Invitation => "invitation", - NotificationType::RoleChange => "role_change", - NotificationType::RoomCreated => "room_created", - NotificationType::RoomDeleted => "room_deleted", - NotificationType::SystemAnnouncement => "system_announcement", - NotificationType::ProjectInvitation => "project_invitation", - }; - write!(f, "{}", s) - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_notifications")] -#[serde(rename_all = "camelCase")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - #[sea_orm(nullable)] - pub room: Option, - #[sea_orm(nullable)] - pub project: Option, - #[sea_orm(nullable)] - pub user_id: Option, - #[sea_orm(column_name = "notification_type")] - pub notification_type: NotificationType, - #[sea_orm(nullable)] - pub related_message_id: Option, - #[sea_orm(nullable)] - pub related_user_id: Option, - #[sea_orm(nullable)] - pub related_room_id: Option, - #[sea_orm(column_name = "title")] - pub title: String, - #[sea_orm(column_name = "content", nullable)] - pub content: Option, - #[sea_orm(column_name = "metadata", nullable)] - pub metadata: Option, - #[sea_orm(column_name = "is_read")] - pub is_read: bool, - #[sea_orm(column_name = "is_archived")] - pub is_archived: bool, - #[sea_orm(column_name = "created_at")] - pub created_at: DateTime, - #[sea_orm(column_name = "read_at", nullable)] - pub read_at: Option>, - #[sea_orm(column_name = "expires_at", nullable)] - pub expires_at: Option>, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::Room", - to = "super::room::Column::Id" - )] - Room, -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_pin.rs b/libs/models/rooms/room_pin.rs deleted file mode 100644 index 842d753..0000000 --- a/libs/models/rooms/room_pin.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::{DateTimeUtc, MessageId, RoomId, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_pin")] -pub struct Model { - #[sea_orm(primary_key)] - pub room: RoomId, - #[sea_orm(primary_key)] - pub message: MessageId, - pub pinned_by: UserId, - pub pinned_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::Room", - to = "super::room::Column::Id" - )] - Room, - #[sea_orm( - belongs_to = "super::room_message::Entity", - from = "Column::Message", - to = "super::room_message::Column::Id" - )] - Message, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Message.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_thread.rs b/libs/models/rooms/room_thread.rs deleted file mode 100644 index cc5820f..0000000 --- a/libs/models/rooms/room_thread.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::{DateTimeUtc, RoomId, RoomThreadId, Seq, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_thread")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: RoomThreadId, - pub room: RoomId, - pub parent: Seq, - pub created_by: UserId, - pub participants: sea_orm::JsonValue, - pub last_message_at: DateTimeUtc, - pub last_message_preview: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::Room", - to = "super::room::Column::Id" - )] - Room, - #[sea_orm(has_many = "super::room_message::Entity")] - Messages, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Messages.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/rooms/room_user_state.rs b/libs/models/rooms/room_user_state.rs deleted file mode 100644 index a5abb4d..0000000 --- a/libs/models/rooms/room_user_state.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::{DateTimeUtc, RoomId, Seq, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Per-user per-room state: read position, notification preferences, etc. -/// Not membership — any user who interacts with a room gets a state row (lazy init). -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "room_user_state")] -pub struct Model { - #[sea_orm(primary_key)] - pub room: RoomId, - #[sea_orm(primary_key)] - pub user: UserId, - pub last_read_seq: Option, - /// Do Not Disturb: if true, suppress notifications for this room - pub do_not_disturb: bool, - /// DND start hour (0-23, local time). None means no scheduled DND. - pub dnd_start_hour: Option, - /// DND end hour (0-23, local time). None means no scheduled DND. - pub dnd_end_hour: Option, - pub joined_at: Option, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "super::room::Entity", - from = "Column::Room", - to = "super::room::Column::Id" - )] - Room, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::Room.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/system/label.rs b/libs/models/system/label.rs deleted file mode 100644 index 250e5d6..0000000 --- a/libs/models/system/label.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::ProjectId; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "label")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "project_uuid")] - pub project: ProjectId, - pub name: String, - pub color: String, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/system/mod.rs b/libs/models/system/mod.rs deleted file mode 100644 index 5c0951b..0000000 --- a/libs/models/system/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub use label::Entity as Label; -pub use notify::Entity as Notify; - -pub mod label; -pub mod notify; diff --git a/libs/models/system/notify.rs b/libs/models/system/notify.rs deleted file mode 100644 index 9e917b8..0000000 --- a/libs/models/system/notify.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "notify")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "user_uuid")] - pub user: UserId, - pub title: String, - pub description: Option, - pub content: String, - pub url: Option, - pub kind: i32, - pub read_at: Option, - pub deleted_at: Option, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/mod.rs b/libs/models/users/mod.rs deleted file mode 100644 index aa0fa28..0000000 --- a/libs/models/users/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub use user::Entity as User; -pub use user_2fa::Entity as User2fa; -pub use user_activity_log::Entity as UserActivityLog; -pub use user_billing::Entity as UserBilling; -pub use user_billing_history::Entity as UserBillingHistory; -pub use user_email::Entity as UserEmail; -pub use user_email_change::Entity as UserEmailChange; -pub use user_notification::Entity as UserNotification; -pub use user_password::Entity as UserPassword; -pub use user_password_reset::Entity as UserPasswordReset; -pub use user_preferences::Entity as UserPreferences; -pub use user_relation::Entity as UserRelation; -pub use user_ssh_key::Entity as UserSshKey; -pub use user_token::Entity as UserToken; - -pub mod user; -pub mod user_2fa; -pub mod user_activity_log; -pub mod user_billing; -pub mod user_billing_history; -pub mod user_email; -pub mod user_email_change; -pub mod user_notification; -pub mod user_password; -pub mod user_password_reset; -pub mod user_preferences; -pub mod user_relation; -pub mod user_ssh_key; -pub mod user_token; diff --git a/libs/models/users/user.rs b/libs/models/users/user.rs deleted file mode 100644 index aa412f3..0000000 --- a/libs/models/users/user.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user")] -pub struct Model { - #[sea_orm(primary_key)] - pub uid: UserId, - pub username: String, - pub display_name: Option, - pub avatar_url: Option, - pub website_url: Option, - pub organization: Option, - pub last_sign_in_at: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_2fa.rs b/libs/models/users/user_2fa.rs deleted file mode 100644 index d5ec61c..0000000 --- a/libs/models/users/user_2fa.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Stored as `"totp"` or `"webauthn"` in the database. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum TwoFactorMethod { - Totp, - WebAuthn, -} - -impl std::fmt::Display for TwoFactorMethod { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TwoFactorMethod::Totp => write!(f, "totp"), - TwoFactorMethod::WebAuthn => write!(f, "webauthn"), - } - } -} - -impl std::str::FromStr for TwoFactorMethod { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "totp" => Ok(TwoFactorMethod::Totp), - "webauthn" => Ok(TwoFactorMethod::WebAuthn), - _ => Err("unknown two-factor method"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_2fa")] -pub struct Model { - #[sea_orm(primary_key)] - pub user: UserId, - pub method: String, - pub secret: Option, - #[sea_orm(column_type = "Json")] - pub backup_codes: serde_json::Value, - pub is_enabled: bool, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_activity_log.rs b/libs/models/users/user_activity_log.rs deleted file mode 100644 index 58e796f..0000000 --- a/libs/models/users/user_activity_log.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::JsonValue; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Auth action types: login, logout, register, password_change, password_reset, 2fa_enabled, etc. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum AuthAction { - Login, - Logout, - Register, - PasswordChange, - PasswordReset, - TwoFactorEnabled, - TwoFactorDisabled, - TwoFactorBackupCodesRegenerated, -} - -impl std::fmt::Display for AuthAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AuthAction::Login => write!(f, "login"), - AuthAction::Logout => write!(f, "logout"), - AuthAction::Register => write!(f, "register"), - AuthAction::PasswordChange => write!(f, "password_change"), - AuthAction::PasswordReset => write!(f, "password_reset"), - AuthAction::TwoFactorEnabled => write!(f, "2fa_enabled"), - AuthAction::TwoFactorDisabled => write!(f, "2fa_disabled"), - AuthAction::TwoFactorBackupCodesRegenerated => { - write!(f, "2fa_backup_codes_regenerated") - } - } - } -} - -impl std::str::FromStr for AuthAction { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "login" => Ok(AuthAction::Login), - "logout" => Ok(AuthAction::Logout), - "register" => Ok(AuthAction::Register), - "password_change" => Ok(AuthAction::PasswordChange), - "password_reset" => Ok(AuthAction::PasswordReset), - "2fa_enabled" => Ok(AuthAction::TwoFactorEnabled), - "2fa_disabled" => Ok(AuthAction::TwoFactorDisabled), - "2fa_backup_codes_regenerated" => Ok(AuthAction::TwoFactorBackupCodesRegenerated), - _ => Err("unknown auth action"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_activity_log")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub user_uid: Option, - pub action: String, - pub ip_address: Option, - pub user_agent: Option, - pub details: JsonValue, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_billing.rs b/libs/models/users/user_billing.rs deleted file mode 100644 index a268147..0000000 --- a/libs/models/users/user_billing.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Per-user billing account. -/// Each user gets a default $10 balance on creation. -/// Pro users additionally have a monthly quota that resets each cycle. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_billing")] -pub struct Model { - #[sea_orm(primary_key)] - #[sea_orm(column_name = "user_uuid")] - pub user: UserId, - #[sea_orm(column_type = "Decimal(Some((20, 4)))")] - pub balance: Decimal, - #[sea_orm(column_type = "Text", default_value = "USD")] - pub currency: String, - #[sea_orm(default_value = false)] - pub is_pro: bool, - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub monthly_quota: Decimal, - #[sea_orm(column_type = "Decimal(Some((20, 4)))", default_value = 0)] - pub month_used: Decimal, - pub cycle_start: Option, - pub cycle_end: Option, - pub updated_at: DateTimeUtc, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "crate::users::user::Entity", - from = "Column::User", - to = "crate::users::user::Column::Uid" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_billing_history.rs b/libs/models/users/user_billing_history.rs deleted file mode 100644 index 21f95a5..0000000 --- a/libs/models/users/user_billing_history.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Billing transaction history for a user's personal account. -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_billing_history")] -pub struct Model { - #[sea_orm(primary_key)] - pub uid: Uuid, - #[sea_orm(column_name = "user_uuid")] - pub user: UserId, - #[sea_orm(column_type = "Decimal(Some((20, 4)))")] - pub amount: Decimal, - #[sea_orm(column_type = "Text")] - pub currency: String, - #[sea_orm(column_type = "Text")] - pub reason: String, - #[sea_orm(column_type = "JsonBinary", nullable)] - pub extra: Option, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation { - #[sea_orm( - belongs_to = "crate::users::user::Entity", - from = "Column::User", - to = "crate::users::user::Column::Uid" - )] - User, -} - -impl Related for Entity { - fn to() -> RelationDef { - Relation::User.def() - } -} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_email.rs b/libs/models/users/user_email.rs deleted file mode 100644 index af63e4f..0000000 --- a/libs/models/users/user_email.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_email")] -pub struct Model { - #[sea_orm(primary_key)] - pub user: UserId, - pub email: String, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_email_change.rs b/libs/models/users/user_email_change.rs deleted file mode 100644 index 9e2934b..0000000 --- a/libs/models/users/user_email_change.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_email_change")] -pub struct Model { - #[sea_orm(primary_key)] - pub token: String, - pub user_uid: UserId, - pub new_email: String, - pub expires_at: DateTimeUtc, - pub used: bool, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_notification.rs b/libs/models/users/user_notification.rs deleted file mode 100644 index bce3b8a..0000000 --- a/libs/models/users/user_notification.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Stored as `"daily"`, `"weekly"`, or `"never"` in the database. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum DigestMode { - Daily, - Weekly, - Never, -} - -impl std::fmt::Display for DigestMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DigestMode::Daily => write!(f, "daily"), - DigestMode::Weekly => write!(f, "weekly"), - DigestMode::Never => write!(f, "never"), - } - } -} - -impl std::str::FromStr for DigestMode { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "daily" => Ok(DigestMode::Daily), - "weekly" => Ok(DigestMode::Weekly), - "never" => Ok(DigestMode::Never), - _ => Err("unknown digest mode"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_notification")] -pub struct Model { - #[sea_orm(primary_key)] - pub user: UserId, - pub email_enabled: bool, - pub in_app_enabled: bool, - pub push_enabled: bool, - pub digest_mode: String, - pub dnd_enabled: bool, - pub dnd_start_minute: Option, - pub dnd_end_minute: Option, - pub marketing_enabled: bool, - pub security_enabled: bool, - pub product_enabled: bool, - pub push_subscription_endpoint: Option, - pub push_subscription_keys_p256dh: Option, - pub push_subscription_keys_auth: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_password.rs b/libs/models/users/user_password.rs deleted file mode 100644 index 76d6c81..0000000 --- a/libs/models/users/user_password.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_password")] -pub struct Model { - #[sea_orm(primary_key)] - pub user: UserId, - pub password_hash: String, - pub password_salt: Option, - pub is_active: bool, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_password_reset.rs b/libs/models/users/user_password_reset.rs deleted file mode 100644 index 55fbf85..0000000 --- a/libs/models/users/user_password_reset.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_password_reset")] -pub struct Model { - #[sea_orm(primary_key)] - pub token: String, - pub user_uid: UserId, - pub expires_at: DateTimeUtc, - pub used: bool, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_preferences.rs b/libs/models/users/user_preferences.rs deleted file mode 100644 index ac133fd..0000000 --- a/libs/models/users/user_preferences.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_preferences")] -pub struct Model { - #[sea_orm(primary_key)] - pub user: UserId, - pub language: String, - pub theme: String, - pub timezone: String, - pub email_notifications: bool, - pub in_app_notifications: bool, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_relation.rs b/libs/models/users/user_relation.rs deleted file mode 100644 index 95373b4..0000000 --- a/libs/models/users/user_relation.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Stored as `"follow"` or `"block"` in the database. -/// Use `FromStr` / `ToString` for type-safe access. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum RelationType { - Follow, - Block, -} - -impl std::fmt::Display for RelationType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RelationType::Follow => write!(f, "follow"), - RelationType::Block => write!(f, "block"), - } - } -} - -impl std::str::FromStr for RelationType { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "follow" => Ok(RelationType::Follow), - "block" => Ok(RelationType::Block), - _ => Err("unknown relation type"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_relation")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub user: UserId, - pub target: UserId, - pub relation_type: String, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_ssh_key.rs b/libs/models/users/user_ssh_key.rs deleted file mode 100644 index 1c93426..0000000 --- a/libs/models/users/user_ssh_key.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Stored as `"ed25519"`, `"rsa"`, or `"ecdsa"` in the database. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum KeyType { - Ed25519, - Rsa, - Ecdsa, -} - -impl std::fmt::Display for KeyType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - KeyType::Ed25519 => write!(f, "ed25519"), - KeyType::Rsa => write!(f, "rsa"), - KeyType::Ecdsa => write!(f, "ecdsa"), - } - } -} - -impl std::str::FromStr for KeyType { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "ed25519" => Ok(KeyType::Ed25519), - "rsa" => Ok(KeyType::Rsa), - "ecdsa" => Ok(KeyType::Ecdsa), - _ => Err("unknown key type"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_ssh_key")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub user: UserId, - pub title: String, - pub public_key: String, - pub fingerprint: String, - pub key_type: String, - pub key_bits: Option, - pub is_verified: bool, - pub last_used_at: Option, - pub expires_at: Option, - pub is_revoked: bool, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/users/user_token.rs b/libs/models/users/user_token.rs deleted file mode 100644 index 6069ef5..0000000 --- a/libs/models/users/user_token.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{DateTimeUtc, UserId}; -use sea_orm::JsonValue; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "user_token")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - pub user: UserId, - pub name: String, - pub token_hash: String, - pub scopes: JsonValue, - pub expires_at: Option, - pub is_revoked: bool, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/workspaces/mod.rs b/libs/models/workspaces/mod.rs deleted file mode 100644 index 5b736d8..0000000 --- a/libs/models/workspaces/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod workspace; -pub mod workspace_alert_config; -pub mod workspace_billing; -pub mod workspace_billing_history; -pub mod workspace_membership; - -pub use workspace::Entity as Workspace; -pub use workspace_alert_config::Entity as WorkspaceAlertConfig; -pub use workspace_billing::Entity as WorkspaceBilling; -pub use workspace_billing_history::Entity as WorkspaceBillingHistory; -pub use workspace_membership::WorkspaceRole; diff --git a/libs/models/workspaces/workspace.rs b/libs/models/workspaces/workspace.rs deleted file mode 100644 index 8ab9f2c..0000000 --- a/libs/models/workspaces/workspace.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::DateTimeUtc; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "workspace")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: Uuid, - pub slug: String, - pub name: String, - pub description: Option, - pub avatar_url: Option, - pub plan: String, - pub billing_email: Option, - pub stripe_customer_id: Option, - pub stripe_subscription_id: Option, - pub plan_expires_at: Option, - pub deleted_at: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/workspaces/workspace_alert_config.rs b/libs/models/workspaces/workspace_alert_config.rs deleted file mode 100644 index d25ef36..0000000 --- a/libs/models/workspaces/workspace_alert_config.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{DateTimeUtc, Decimal}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "workspace_alert_config")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - #[sea_orm(column_name = "workspace_id")] - pub workspace_id: Uuid, - #[sea_orm(column_type = "Text")] - pub alert_type: String, - /// Threshold value (e.g. 10.0 = balance < $10, 0.8 = 80% of quota used) - #[sea_orm(column_type = "Decimal(Some((10, 4)))")] - pub threshold: Decimal, - #[sea_orm(default_value = "true")] - pub email_enabled: bool, - #[sea_orm(default_value = "true")] - pub enabled: bool, - /// admin_user.id (NULL if created via API without user context) - #[sea_orm(nullable)] - pub created_by: Option, - pub created_at: DateTimeUtc, - pub updated_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/workspaces/workspace_billing.rs b/libs/models/workspaces/workspace_billing.rs deleted file mode 100644 index 83dc76d..0000000 --- a/libs/models/workspaces/workspace_billing.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{DateTimeUtc, Decimal, WorkspaceId}; -use sea_orm::entity::prelude::*; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "workspace_billing")] -pub struct Model { - #[sea_orm(primary_key, column_name = "workspace_id")] - pub workspace_id: WorkspaceId, - #[sea_orm(column_type = "Decimal(Some((20, 4)))")] - pub balance: Decimal, - #[sea_orm(column_type = "Text")] - pub currency: String, - #[sea_orm(column_type = "Decimal(Some((20, 4)))")] - pub monthly_quota: Decimal, - #[sea_orm(column_type = "Decimal(Some((20, 4)))")] - pub total_spent: Decimal, - pub updated_at: DateTimeUtc, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/workspaces/workspace_billing_history.rs b/libs/models/workspaces/workspace_billing_history.rs deleted file mode 100644 index 9a06a7e..0000000 --- a/libs/models/workspaces/workspace_billing_history.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{DateTimeUtc, Decimal, UserId, WorkspaceId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "workspace_billing_history")] -pub struct Model { - #[sea_orm(primary_key, column_name = "uid")] - pub uid: Uuid, - #[sea_orm(column_name = "workspace_id")] - pub workspace_id: WorkspaceId, - #[sea_orm(column_name = "user_id")] - pub user_id: Option, - #[sea_orm(column_type = "Decimal(Some((20, 4)))")] - pub amount: Decimal, - #[sea_orm(column_type = "Text")] - pub currency: String, - #[sea_orm(column_type = "Text")] - pub reason: String, - pub extra: Option, - pub created_at: DateTimeUtc, -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/models/workspaces/workspace_membership.rs b/libs/models/workspaces/workspace_membership.rs deleted file mode 100644 index 1f85196..0000000 --- a/libs/models/workspaces/workspace_membership.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::{DateTimeUtc, UserId, WorkspaceId}; -use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; - -/// Workspace membership role. Values: "owner", "admin", "member" -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum WorkspaceRole { - Owner, - Admin, - Member, -} - -impl std::fmt::Display for WorkspaceRole { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - WorkspaceRole::Owner => write!(f, "owner"), - WorkspaceRole::Admin => write!(f, "admin"), - WorkspaceRole::Member => write!(f, "member"), - } - } -} - -impl std::str::FromStr for WorkspaceRole { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s { - "owner" => Ok(WorkspaceRole::Owner), - "admin" => Ok(WorkspaceRole::Admin), - "member" => Ok(WorkspaceRole::Member), - _ => Err("unknown workspace role"), - } - } -} - -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] -#[sea_orm(table_name = "workspace_membership")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i64, - #[sea_orm(column_name = "workspace_id")] - pub workspace_id: WorkspaceId, - #[sea_orm(column_name = "user_id")] - pub user_id: UserId, - pub role: String, - pub status: String, - pub invited_by: Option, - pub joined_at: DateTimeUtc, - pub invite_token: Option, - pub invite_expires_at: Option, -} - -impl Model { - pub fn role_enum(&self) -> Result { - self.role.parse() - } -} - -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} - -impl ActiveModelBehavior for ActiveModel {} diff --git a/libs/observability/Cargo.toml b/libs/observability/Cargo.toml deleted file mode 100644 index aa5068e..0000000 --- a/libs/observability/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "observability" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[dependencies] -actix-web = { workspace = true } -futures = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -chrono = { workspace = true } -once_cell = { workspace = true } -hostname = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true, features = ["derive"] } -tokio = { workspace = true, features = ["rt"] } -anyhow = { workspace = true } - -# Prometheus metrics export -metrics = "0.22" -metrics-exporter-prometheus = "0.13" - -# OTLP tracing (Phase 6) -opentelemetry = { workspace = true } -opentelemetry_sdk = { workspace = true } -opentelemetry-otlp = { workspace = true, features = ["reqwest"] } -opentelemetry-http = { workspace = true } -tracing-opentelemetry = { workspace = true } -thiserror = { workspace = true } -reqwest = { workspace = true, features = ["json"] } -sysinfo = { workspace = true } - -[lints] -workspace = true diff --git a/libs/observability/src/business_metrics.rs b/libs/observability/src/business_metrics.rs deleted file mode 100644 index 327b690..0000000 --- a/libs/observability/src/business_metrics.rs +++ /dev/null @@ -1,293 +0,0 @@ -//! Business-level metrics for platform operations. -//! -//! Provides `metrics::counter!` and `metrics::gauge!` calls for all -//! business events (projects, issues, PRs, rooms, repos, billing). -//! Counter names are registered in `install_recorder()` so they appear -//! in Prometheus exposition format. -//! -//! Service layer code should call `incr!()` after successful operations. - -/// Increment a business counter by 1. -/// Calls `metrics::counter!` from within the observability crate so -/// calling crates don't need `metrics` as a direct dependency. -#[macro_export] -macro_rules! incr { - ($name:expr) => { - $crate::increment_business_counter($name) - }; -} - -/// Function wrapper so the `metrics::counter!` macro is only invoked -/// inside the observability crate (which depends on `metrics`). -pub fn increment_business_counter(name: &'static str) { - metrics::counter!(name); -} - -// ── Counter names (constants) ────────────────────────────────────────────────── - -// Project -pub const PROJECTS_CREATED_TOTAL: &str = "projects_created_total"; -pub const PROJECTS_DELETED_TOTAL: &str = "projects_deleted_total"; -pub const PROJECT_MEMBERS_ADDED_TOTAL: &str = "project_members_added_total"; -pub const PROJECT_MEMBERS_REMOVED_TOTAL: &str = "project_members_removed_total"; -pub const PROJECT_MEMBERS_ROLE_CHANGED_TOTAL: &str = "project_members_role_changed_total"; -pub const PROJECT_LIKES_TOTAL: &str = "project_likes_total"; -pub const PROJECT_UNLIKES_TOTAL: &str = "project_unlikes_total"; -pub const PROJECT_WATCHES_TOTAL: &str = "project_watches_total"; -pub const PROJECT_UNWATCHES_TOTAL: &str = "project_unwatches_total"; - -// Issue -pub const ISSUES_OPENED_TOTAL: &str = "issues_opened_total"; -pub const ISSUES_CLOSED_TOTAL: &str = "issues_closed_total"; -pub const ISSUES_REOPENED_TOTAL: &str = "issues_reopened_total"; -pub const ISSUES_DELETED_TOTAL: &str = "issues_deleted_total"; -pub const ISSUES_UPDATED_TOTAL: &str = "issues_updated_total"; -pub const ISSUE_COMMENTS_CREATED_TOTAL: &str = "issue_comments_created_total"; -pub const ISSUE_COMMENTS_DELETED_TOTAL: &str = "issue_comments_deleted_total"; - -// Pull Request -pub const PRS_OPENED_TOTAL: &str = "prs_opened_total"; -pub const PRS_MERGED_TOTAL: &str = "prs_merged_total"; -pub const PRS_CLOSED_TOTAL: &str = "prs_closed_total"; -pub const PRS_UPDATED_TOTAL: &str = "prs_updated_total"; -pub const PR_REVIEWS_SUBMITTED_TOTAL: &str = "pr_reviews_submitted_total"; -pub const PR_REVIEW_COMMENTS_TOTAL: &str = "pr_review_comments_total"; - -// Room -pub const ROOMS_CREATED_TOTAL: &str = "rooms_created_total"; -pub const ROOMS_DELETED_TOTAL: &str = "rooms_deleted_total"; -pub const ROOMS_UPDATED_TOTAL: &str = "rooms_updated_total"; -pub const ROOM_MESSAGES_SENT_TOTAL: &str = "room_messages_sent_total"; -pub const ROOM_MESSAGES_AI_TOTAL: &str = "room_messages_ai_total"; -pub const ROOM_THREADS_CREATED_TOTAL: &str = "room_threads_created_total"; - -// Repo / Git -pub const REPOS_CREATED_TOTAL: &str = "repos_created_total"; -pub const GIT_COMMITS_PUSHED_TOTAL: &str = "git_commits_pushed_total"; -pub const GIT_BRANCHES_CREATED_TOTAL: &str = "git_branches_created_total"; -pub const GIT_BRANCHES_DELETED_TOTAL: &str = "git_branches_deleted_total"; -pub const GIT_TAGS_CREATED_TOTAL: &str = "git_tags_created_total"; -pub const GIT_TAGS_DELETED_TOTAL: &str = "git_tags_deleted_total"; -pub const GIT_CLONES_TOTAL: &str = "git_clones_total"; - -// Billing -pub const BILLING_CREDITS_USED_TOTAL: &str = "billing_credits_used_total"; -pub const BILLING_ERRORS_TOTAL: &str = "billing_errors_total"; -pub const BILLING_CREDITS_ADDED_TOTAL: &str = "billing_credits_added_total"; - -// AI (supplements existing ai_* counters) -pub const AI_ROOM_CALLS_TOTAL: &str = "ai_room_calls_total"; -pub const AI_CHAT_CONVERSATIONS_CREATED: &str = "ai_chat_conversations_created_total"; -pub const AI_CHAT_MESSAGES_SENT: &str = "ai_chat_messages_sent_total"; - -// ── Gauge names ───────────────────────────────────────────────────────────────── - -pub const ACTIVE_CONNECTIONS: &str = "active_connections"; -pub const ACTIVE_ROOM_PARTICIPANTS: &str = "active_room_participants"; - -/// Register all business metric descriptions. -/// Called from `install_recorder()` in `prometheus_exporter.rs`. -pub fn describe_business_metrics() { - // Project - metrics::describe_counter!( - PROJECTS_CREATED_TOTAL, - metrics::Unit::Count, - "Projects created" - ); - metrics::describe_counter!( - PROJECTS_DELETED_TOTAL, - metrics::Unit::Count, - "Projects deleted" - ); - metrics::describe_counter!( - PROJECT_MEMBERS_ADDED_TOTAL, - metrics::Unit::Count, - "Project members added" - ); - metrics::describe_counter!( - PROJECT_MEMBERS_REMOVED_TOTAL, - metrics::Unit::Count, - "Project members removed" - ); - metrics::describe_counter!( - PROJECT_MEMBERS_ROLE_CHANGED_TOTAL, - metrics::Unit::Count, - "Project member role changes" - ); - metrics::describe_counter!(PROJECT_LIKES_TOTAL, metrics::Unit::Count, "Project likes"); - metrics::describe_counter!( - PROJECT_UNLIKES_TOTAL, - metrics::Unit::Count, - "Project unlikes" - ); - metrics::describe_counter!( - PROJECT_WATCHES_TOTAL, - metrics::Unit::Count, - "Project watches" - ); - metrics::describe_counter!( - PROJECT_UNWATCHES_TOTAL, - metrics::Unit::Count, - "Project unwatches" - ); - - // Issue - metrics::describe_counter!(ISSUES_OPENED_TOTAL, metrics::Unit::Count, "Issues opened"); - metrics::describe_counter!(ISSUES_CLOSED_TOTAL, metrics::Unit::Count, "Issues closed"); - metrics::describe_counter!( - ISSUES_REOPENED_TOTAL, - metrics::Unit::Count, - "Issues reopened" - ); - metrics::describe_counter!(ISSUES_DELETED_TOTAL, metrics::Unit::Count, "Issues deleted"); - metrics::describe_counter!(ISSUES_UPDATED_TOTAL, metrics::Unit::Count, "Issues updated"); - metrics::describe_counter!( - ISSUE_COMMENTS_CREATED_TOTAL, - metrics::Unit::Count, - "Issue comments created" - ); - metrics::describe_counter!( - ISSUE_COMMENTS_DELETED_TOTAL, - metrics::Unit::Count, - "Issue comments deleted" - ); - - // Pull Request - metrics::describe_counter!( - PRS_OPENED_TOTAL, - metrics::Unit::Count, - "Pull requests opened" - ); - metrics::describe_counter!( - PRS_MERGED_TOTAL, - metrics::Unit::Count, - "Pull requests merged" - ); - metrics::describe_counter!( - PRS_CLOSED_TOTAL, - metrics::Unit::Count, - "Pull requests closed (without merge)" - ); - metrics::describe_counter!( - PRS_UPDATED_TOTAL, - metrics::Unit::Count, - "Pull requests updated" - ); - metrics::describe_counter!( - PR_REVIEWS_SUBMITTED_TOTAL, - metrics::Unit::Count, - "PR reviews submitted" - ); - metrics::describe_counter!( - PR_REVIEW_COMMENTS_TOTAL, - metrics::Unit::Count, - "PR review comments" - ); - - // Room - metrics::describe_counter!( - ROOMS_CREATED_TOTAL, - metrics::Unit::Count, - "Chat rooms created" - ); - metrics::describe_counter!( - ROOMS_DELETED_TOTAL, - metrics::Unit::Count, - "Chat rooms deleted" - ); - metrics::describe_counter!( - ROOMS_UPDATED_TOTAL, - metrics::Unit::Count, - "Chat rooms updated" - ); - metrics::describe_counter!( - ROOM_MESSAGES_SENT_TOTAL, - metrics::Unit::Count, - "Room messages sent (human)" - ); - metrics::describe_counter!( - ROOM_MESSAGES_AI_TOTAL, - metrics::Unit::Count, - "Room messages sent (AI)" - ); - metrics::describe_counter!( - ROOM_THREADS_CREATED_TOTAL, - metrics::Unit::Count, - "Room threads created" - ); - - // Repo / Git - metrics::describe_counter!(REPOS_CREATED_TOTAL, metrics::Unit::Count, "Repos created"); - metrics::describe_counter!( - GIT_COMMITS_PUSHED_TOTAL, - metrics::Unit::Count, - "Git commits pushed" - ); - metrics::describe_counter!( - GIT_BRANCHES_CREATED_TOTAL, - metrics::Unit::Count, - "Git branches created" - ); - metrics::describe_counter!( - GIT_BRANCHES_DELETED_TOTAL, - metrics::Unit::Count, - "Git branches deleted" - ); - metrics::describe_counter!( - GIT_TAGS_CREATED_TOTAL, - metrics::Unit::Count, - "Git tags created" - ); - metrics::describe_counter!( - GIT_TAGS_DELETED_TOTAL, - metrics::Unit::Count, - "Git tags deleted" - ); - metrics::describe_counter!( - GIT_CLONES_TOTAL, - metrics::Unit::Count, - "Git clone/fetch operations" - ); - - // Billing - metrics::describe_counter!( - BILLING_CREDITS_USED_TOTAL, - metrics::Unit::Count, - "Billing credits consumed" - ); - metrics::describe_counter!(BILLING_ERRORS_TOTAL, metrics::Unit::Count, "Billing errors"); - metrics::describe_counter!( - BILLING_CREDITS_ADDED_TOTAL, - metrics::Unit::Count, - "Billing credits added (top-up)" - ); - - // AI - metrics::describe_counter!( - AI_ROOM_CALLS_TOTAL, - metrics::Unit::Count, - "AI calls in room context" - ); - metrics::describe_counter!( - AI_CHAT_CONVERSATIONS_CREATED, - metrics::Unit::Count, - "AI chat conversations created" - ); - metrics::describe_counter!( - AI_CHAT_MESSAGES_SENT, - metrics::Unit::Count, - "AI chat messages sent" - ); - - // Gauges - metrics::describe_gauge!( - ACTIVE_CONNECTIONS, - metrics::Unit::Count, - "Active WebSocket connections" - ); - metrics::describe_gauge!( - ACTIVE_ROOM_PARTICIPANTS, - metrics::Unit::Count, - "Active room participants" - ); -} diff --git a/libs/observability/src/lib.rs b/libs/observability/src/lib.rs deleted file mode 100644 index 15a1e70..0000000 --- a/libs/observability/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Observability primitives: tracing subscriber, metrics, OTLP export. -//! -//! Call `observability::init_tracing_subscriber(level)` once at startup. -//! All services then use `tracing::info!`, `tracing::warn!`, etc. directly. - -pub mod business_metrics; -pub mod metrics_middleware; -pub mod msg_json_fmt; -pub mod otlp; -pub mod prometheus_exporter; -pub mod push; -pub mod tracing_fmt; -pub mod tracing_init; -pub mod tracing_middleware; - -pub use metrics_middleware::{HttpMetrics, MetricsMiddleware}; -pub use msg_json_fmt::set_span_msg; -pub use prometheus_exporter::{ - HttpMetricsSnapshot, HttpSnapshotGuard, install_recorder, prometheus_handler, - render_to_hashmap, spawn_http_metrics_poller, -}; -pub use tracing_fmt::{init_tracing_subscriber, instance_id}; -pub type PrometheusHandle = metrics_exporter_prometheus::PrometheusHandle; -pub use business_metrics::*; -pub use otlp::{OtelGuard, init_otlp}; -pub use tracing_middleware::TracingSpanMiddleware; diff --git a/libs/observability/src/metrics_middleware.rs b/libs/observability/src/metrics_middleware.rs deleted file mode 100644 index d92fd16..0000000 --- a/libs/observability/src/metrics_middleware.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Actix-web metrics middleware: counts requests and measures latency. -//! -//! Registers metrics into a shared atomic counter exposed as structured fields -//! on every request. No external metrics endpoint — logs are the export path. - -use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; -use futures::future::{LocalBoxFuture, Ready, ok}; -use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, RwLock}; -use std::task::{Context, Poll}; -use std::time::Instant; - -/// HTTP metrics collected by this middleware. -#[derive(Debug, Default)] -pub struct HttpMetrics { - /// Total number of requests processed. - pub request_count: AtomicU64, - /// Sum of all request durations in milliseconds. - pub total_duration_ms: AtomicU64, - /// Number of 2xx responses. - pub status_2xx: AtomicU64, - /// Number of 4xx responses. - pub status_4xx: AtomicU64, - /// Number of 5xx responses. - pub status_5xx: AtomicU64, - /// Per-endpoint request counters. Key format: "GET /api/room/{id}" or "POST /api/git/commit" - pub endpoint_counts: RwLock>, -} - -impl HttpMetrics { - /// Creates a new instance with all counters initialised to zero. - pub fn new() -> Self { - Self::default() - } - - /// Increment the counter for a specific HTTP endpoint (method + path). - pub fn incr_endpoint(&self, method: &str, path: &str) { - let key = format!("{} {}", method, path); - let mut map = self - .endpoint_counts - .write() - .unwrap_or_else(|e| e.into_inner()); - let counter = map.entry(key).or_insert_with(|| AtomicU64::new(0)); - counter.fetch_add(1, Ordering::Relaxed); - } - - /// Returns a snapshot of all current counter values. - pub fn snapshot(&self) -> HashMap { - let mut m = HashMap::new(); - m.insert( - "http_requests_total".into(), - serde_json::json!(self.request_count.load(Ordering::Relaxed)), - ); - m.insert( - "http_request_duration_ms_total".into(), - serde_json::json!(self.total_duration_ms.load(Ordering::Relaxed)), - ); - m.insert( - "http_requests_2xx".into(), - serde_json::json!(self.status_2xx.load(Ordering::Relaxed)), - ); - m.insert( - "http_requests_4xx".into(), - serde_json::json!(self.status_4xx.load(Ordering::Relaxed)), - ); - m.insert( - "http_requests_5xx".into(), - serde_json::json!(self.status_5xx.load(Ordering::Relaxed)), - ); - - // Per-endpoint counters - let map = self - .endpoint_counts - .read() - .unwrap_or_else(|e| e.into_inner()); - for (key, counter) in map.iter() { - // Sanitize key for use as metric name: replace spaces and slashes with underscores - let sanitized = key.replace([' ', '/'], "_").to_lowercase(); - let metric_key = format!("http_endpoint_{}", sanitized); - m.insert( - metric_key, - serde_json::json!(counter.load(Ordering::Relaxed)), - ); - } - - m - } -} - -/// Actix-web middleware that collects per-request metrics and exposes them -/// via structured fields on every log line. -pub struct MetricsMiddleware { - metrics: Arc, -} - -impl MetricsMiddleware { - /// Constructs a new `MetricsMiddleware` wrapping the shared `HttpMetrics`. - pub fn new(metrics: Arc) -> Self { - Self { metrics } - } -} - -impl Transform for MetricsMiddleware -where - S: Service, Error = actix_web::Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Transform = MetricsMiddlewareService; - type InitError = (); - type Future = Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - ok(MetricsMiddlewareService { - service: Arc::new(service), - metrics: self.metrics.clone(), - }) - } -} - -pub struct MetricsMiddlewareService { - service: Arc, - metrics: Arc, -} - -impl Clone for MetricsMiddlewareService { - fn clone(&self) -> Self { - Self { - service: self.service.clone(), - metrics: self.metrics.clone(), - } - } -} - -impl Service for MetricsMiddlewareService -where - S: Service, Error = actix_web::Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - let started = Instant::now(); - let service = self.service.clone(); - let metrics = self.metrics.clone(); - let method = req.method().as_str().to_string(); - let path = req.path().to_string(); - - Box::pin(async move { - let res = service.call(req).await?; - let elapsed_ms = started.elapsed().as_millis() as u64; - let status_code = res.status().as_u16(); - - // Update counters atomically. - metrics.request_count.fetch_add(1, Ordering::Relaxed); - metrics - .total_duration_ms - .fetch_add(elapsed_ms, Ordering::Relaxed); - metrics.incr_endpoint(&method, &path); - - match status_code { - 200..=299 => { - metrics.status_2xx.fetch_add(1, Ordering::Relaxed); - } - 400..=499 => { - metrics.status_4xx.fetch_add(1, Ordering::Relaxed); - } - 500..=599 => { - metrics.status_5xx.fetch_add(1, Ordering::Relaxed); - } - _ => {} - } - - Ok(res) - }) - } -} diff --git a/libs/observability/src/msg_json_fmt.rs b/libs/observability/src/msg_json_fmt.rs deleted file mode 100644 index df8306b..0000000 --- a/libs/observability/src/msg_json_fmt.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! Custom JSON formatter that injects `_msg` as the first field. -//! -//! VictoriaLogs requires every log line to have a `_msg` field as the message -//! subject for full-text search. This formatter ensures `_msg` is always the -//! first key in the serialized JSON object. -//! -//! `_msg` derivation rules: -//! - If the event has a `message` field → use it -//! - If a parent span stored a `_msg` in the thread-local buffer (set by the -//! HTTP middleware) → use it -//! - Fallback → use the event target (module path) - -use serde::Serialize; -use std::cell::RefCell; -use std::collections::HashMap; -use std::fmt; -use tracing::field::{Field, Visit}; -use tracing_subscriber::fmt::format::FormatEvent; - -// Thread-local buffer holding the current span's `_msg` value. -// Set by the HTTP middleware via `set_span_msg()` at span creation time. -// The formatter reads this value when the event message is empty (span close). -thread_local! { - static SPAN_MSG: RefCell = const { RefCell::new(String::new()) }; -} - -/// Set the `_msg` for the current span. Called by middleware at span creation. -pub fn set_span_msg(msg: String) { - SPAN_MSG.with(|cell| cell.borrow_mut().clone_from(&msg)); -} - -/// Read and clear the current span's `_msg`. Called by the formatter. -fn take_span_msg() -> Option { - SPAN_MSG.with(|cell| { - let mut s = cell.borrow_mut(); - if s.is_empty() { - None - } else { - let out = s.clone(); - s.clear(); - Some(out) - } - }) -} - -/// Capture all event fields via the visitor pattern. -struct FieldCollector { - message: Option, - fields: HashMap, -} - -impl FieldCollector { - fn new() -> Self { - Self { - message: None, - fields: HashMap::new(), - } - } -} - -impl Visit for FieldCollector { - fn record_str(&mut self, field: &Field, value: &str) { - if field.name() == "message" { - self.message = Some(value.to_string()); - } else { - self.fields.insert( - field.name().to_string(), - serde_json::Value::String(value.to_string()), - ); - } - } - - fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { - self.fields.insert( - field.name().to_string(), - serde_json::Value::String(format!("{:?}", value)), - ); - } - - fn record_i64(&mut self, field: &Field, value: i64) { - self.fields.insert( - field.name().to_string(), - serde_json::Value::Number(value.into()), - ); - } - - fn record_u64(&mut self, field: &Field, value: u64) { - use serde_json::Number; - self.fields.insert( - field.name().to_string(), - serde_json::Value::Number(Number::from(value)), - ); - } - - fn record_bool(&mut self, field: &Field, value: bool) { - self.fields - .insert(field.name().to_string(), serde_json::Value::Bool(value)); - } -} - -/// Custom `FormatEvent` that outputs JSON with `_msg` as the first field. -pub struct MsgJsonFormat; - -impl FormatEvent for MsgJsonFormat -where - S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, - N: for<'a> tracing_subscriber::fmt::format::FormatFields<'a> + 'static, -{ - fn format_event( - &self, - _ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>, - mut writer: tracing_subscriber::fmt::format::Writer<'_>, - event: &tracing::Event<'_>, - ) -> std::fmt::Result { - let mut collector = FieldCollector::new(); - event.record(&mut collector); - - // Derive _msg: event message → thread-local span msg → target fallback - let msg = collector - .message - .clone() - .or_else(take_span_msg) - .unwrap_or_else(|| event.metadata().target().to_string()); - - // Build ordered JSON: _msg first, then standard fields, then event fields - let mut ordered = serde_json::Map::with_capacity(10 + collector.fields.len()); - ordered.insert("_msg".to_string(), serde_json::Value::String(msg)); - ordered.insert( - "timestamp".to_string(), - serde_json::Value::String(chrono::Utc::now().to_rfc3339()), - ); - ordered.insert( - "level".to_string(), - serde_json::Value::String(format!("{}", event.metadata().level())), - ); - ordered.insert( - "target".to_string(), - serde_json::Value::String(event.metadata().target().to_string()), - ); - - if let Some(module_path) = event.metadata().module_path() { - ordered.insert( - "module".to_string(), - serde_json::Value::String(module_path.to_string()), - ); - } - if let Some(file) = event.metadata().file() { - ordered.insert( - "file".to_string(), - serde_json::Value::String(file.to_string()), - ); - } - if let Some(line) = event.metadata().line() { - ordered.insert("line".to_string(), serde_json::Value::Number(line.into())); - } - - // Event fields (excluding `message` which is already in `_msg`) - for (key, value) in &collector.fields { - ordered.insert(key.clone(), value.clone()); - } - - // Use pretty JSON for human readability in terminals, compact for pipelines - let json = if std::env::var("APP_LOG_PRETTY").as_deref() == Ok("true") { - serde_json::to_string_pretty(&ordered) - } else { - serde_json::to_string(&ordered) - }; - write!(writer, "{}", json.map_err(|_| fmt::Error)?) - } -} - -/// Log entry wrapper for VictoriaLogs-compatible output. -/// -/// All fields serialize in a fixed order with `_msg` first. -#[derive(Serialize)] -pub struct VLogEntry { - _msg: String, - timestamp: String, - level: String, - target: String, - #[serde(skip_serializing_if = "Option::is_none")] - module: Option, - #[serde(skip_serializing_if = "Option::is_none")] - file: Option, - #[serde(skip_serializing_if = "Option::is_none")] - line: Option, - #[serde(flatten)] - extra: serde_json::Map, -} - -impl VLogEntry { - pub fn new(msg: String, level: &str, target: &str) -> Self { - Self { - _msg: msg, - timestamp: chrono::Utc::now().to_rfc3339(), - level: level.to_string(), - target: target.to_string(), - module: None, - file: None, - line: None, - extra: serde_json::Map::new(), - } - } - - pub fn module(mut self, m: String) -> Self { - self.module = Some(m); - self - } - - pub fn file(mut self, f: String) -> Self { - self.file = Some(f); - self - } - - pub fn line(mut self, l: u32) -> Self { - self.line = Some(l); - self - } - - pub fn field(mut self, key: String, value: serde_json::Value) -> Self { - self.extra.insert(key, value); - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn json_order_has_msg_first() { - let entry = VLogEntry::new("GET /api/users".to_string(), "INFO", "my::module") - .module("my::module".to_string()) - .field("status".to_string(), serde_json::json!(200)) - .field("duration_ms".to_string(), serde_json::json!(42)); - - let json = serde_json::to_string(&entry).unwrap(); - assert!(json.starts_with(r#"{"_msg":"GET /api/users""#)); - } -} diff --git a/libs/observability/src/otlp.rs b/libs/observability/src/otlp.rs deleted file mode 100644 index a259a65..0000000 --- a/libs/observability/src/otlp.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! OTLP tracer initialisation (Phase 6). -//! -//! Uses HTTP/proto transport to the OTLP endpoint. -//! The endpoint URL is passed as-is to the HTTP exporter. -//! Default Kubernetes otel-collector-agent accepts HTTP on :4318. -//! -//! Call `init_otlp()` **after** `init_tracing_subscriber()` so the fmt layer is -//! already registered. This function rebuilds the global subscriber with the -//! OTLP tracing layer on top. - -use crate::msg_json_fmt::MsgJsonFormat; - -use opentelemetry::trace::TracerProvider; -use opentelemetry_otlp::{SpanExporter, WithExportConfig}; -use opentelemetry_sdk::trace as sdktrace; -use tracing_subscriber::{ - EnvFilter, fmt, fmt::Layer as FmtLayer, layer::SubscriberExt, util::SubscriberInitExt, -}; - -/// Guard that shuts down the OTLP pipeline on drop. -#[must_use] -pub struct OtelGuard { - provider: sdktrace::SdkTracerProvider, -} - -impl OtelGuard { - /// Force-flush any pending spans and shut down the OTLP exporter. - pub async fn shutdown(self) { - if let Err(e) = self.provider.shutdown() { - tracing::warn!(error = %e, "OTLP tracer shutdown error"); - } - } -} - -/// Initialise OTLP tracing and attach it to the global tracing subscriber. -/// -/// Uses HTTP/proto transport to the given endpoint. -/// Returns `Ok(Some(guard))` on success; the caller should store the guard and -/// call `guard.shutdown()` during app shutdown for a clean flush. -/// -/// The `fmt_registry` parameter should be the value returned by -/// `init_tracing_subscriber(level, true)` — i.e. a registry that was built but -/// not yet installed. This function extends that registry with the OTLP tracing -/// layer and calls `try_init()` once, avoiding the "global default already set" error. -pub fn init_otlp( - endpoint: &str, - service_name: &str, - _service_version: &str, - log_level: &str, -) -> Result, InitOtlError> { - if endpoint.is_empty() { - return Err(InitOtlError::EmptyEndpoint); - } - - let endpoint = endpoint.trim_end_matches('/'); - - let exporter = SpanExporter::builder() - .with_http() - .with_endpoint(endpoint) - .build() - .map_err(|e| InitOtlError::ExporterInit(e.to_string()))?; - - let env_filter = - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level)); - - let fmt_layer: FmtLayer<_, _, _, _> = fmt::layer().event_format(MsgJsonFormat); - - let tracer_provider = sdktrace::SdkTracerProvider::builder() - .with_batch_exporter(exporter) - .build(); - - let tracer = tracer_provider.tracer(service_name.to_string()); - let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer); - - let layered = tracing_subscriber::registry() - .with(env_filter) - .with(fmt_layer) - .with(otel_layer); - - tracing::Dispatch::new(layered) - .try_init() - .map_err(|e| InitOtlError::SubscriberInit(e.to_string()))?; - - tracing::debug!(endpoint = %endpoint, "OTLP tracer installed"); - - Ok(Some(OtelGuard { - provider: tracer_provider, - })) -} - -#[derive(Debug, thiserror::Error)] -pub enum InitOtlError { - #[error("endpoint is empty")] - EmptyEndpoint, - - #[error("failed to build OTLP exporter: {0}")] - ExporterInit(String), - - #[error("failed to set tracing subscriber: {0}")] - SubscriberInit(String), -} diff --git a/libs/observability/src/prometheus_exporter.rs b/libs/observability/src/prometheus_exporter.rs deleted file mode 100644 index 3d771bc..0000000 --- a/libs/observability/src/prometheus_exporter.rs +++ /dev/null @@ -1,176 +0,0 @@ -//! Prometheus metrics exporter. -//! -//! Exposes `HttpMetrics` (AtomicU64) and `RoomMetrics` (`metrics` crate) via a -//! Prometheus-compatible `/metrics` endpoint. -//! -//! Usage: -//! let recorder_handle = install_recorder(); // returns PrometheusHandle -//! let guard = Arc::new(RwLock::new(HttpMetricsSnapshot::default())); -//! spawn_http_metrics_poller(metrics.clone(), guard.clone(), Duration::from_secs(15)); -//! // Register /metrics route with prometheus_handler -//! -//! `RoomMetrics` must be constructed **after** `install_recorder()` is called, -//! because its `register_*` macro calls require a global recorder to be set. - -use actix_web::{HttpRequest, HttpResponse, web}; -use metrics::{Unit, describe_counter, describe_histogram}; -use metrics_exporter_prometheus::PrometheusBuilder; -use std::collections::HashMap; -use std::sync::atomic::Ordering; -use std::sync::{Arc, RwLock}; - -/// Installs the global `metrics` crate recorder as a Prometheus exporter. -/// -/// Returns a `PrometheusHandle` for rendering the `/metrics` endpoint. -/// **Must be called before any `metrics::register_*` macro is invoked.** -pub fn install_recorder() -> metrics_exporter_prometheus::PrometheusHandle { - describe_counter!( - "ai_calls_total", - Unit::Count, - "Total AI chat completion calls" - ); - describe_counter!("ai_calls_success", Unit::Count, "Successful AI calls"); - describe_counter!("ai_calls_failure", Unit::Count, "Failed AI calls"); - describe_counter!( - "ai_input_tokens_total", - Unit::Count, - "Total input tokens consumed" - ); - describe_counter!( - "ai_output_tokens_total", - Unit::Count, - "Total output tokens generated" - ); - describe_counter!( - "ai_function_calls_total", - Unit::Count, - "Total AI function/tool calls" - ); - describe_histogram!( - "ai_call_latency_ms", - Unit::Milliseconds, - "AI call latency in milliseconds" - ); - - // Register all business-level metrics descriptions - crate::business_metrics::describe_business_metrics(); - - let recorder = PrometheusBuilder::new().build_recorder(); - - let handle = recorder.handle(); - - metrics::set_global_recorder(recorder).expect("failed to set global metrics recorder"); - - handle -} - -/// Parses Prometheus text exposition format into a flat map of metric name → value. -/// Labels are discarded (only the last value for each name is kept). -pub fn render_to_hashmap(body: &str) -> HashMap { - let mut out = HashMap::new(); - for line in body.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - // Prometheus format: METRIC_NAME{labels} VALUE - // or: METRIC_NAME VALUE - if let Some(brace_pos) = line.find('{') { - let name = &line[..brace_pos]; - if let Some(space_pos) = line[brace_pos..].find(' ') { - let value_str = &line[brace_pos + space_pos + 1..]; - if let Ok(v) = value_str.parse::() { - out.insert(name.to_string(), serde_json::json!(v)); - } - } - } else if let Some(last_space) = line.rfind(' ') { - let name = &line[..last_space]; - let value_str = &line[last_space + 1..]; - if let Ok(v) = value_str.parse::() { - out.insert(name.to_string(), serde_json::json!(v)); - } - } - } - out -} - -/// Re-export `HttpMetrics` so callers don't need to import from `metrics_middleware`. -pub use crate::metrics_middleware::HttpMetrics; - -/// Live HTTP metric values updated by the background poller. -#[derive(Debug, Clone, Default)] -pub struct HttpMetricsSnapshot { - pub request_count: u64, - pub total_duration_ms: u64, - pub status_2xx: u64, - pub status_4xx: u64, - pub status_5xx: u64, -} - -/// `Arc>` shared between the background poller -/// and the HTTP handler. -pub type HttpSnapshotGuard = Arc>; - -/// Starts a background task that snapshots `HttpMetrics` atomics and stores them -/// in `guard` at `interval`. -pub fn spawn_http_metrics_poller( - metrics: Arc, - guard: HttpSnapshotGuard, - interval: std::time::Duration, -) { - tokio::spawn(async move { - let mut ticker = tokio::time::interval(interval); - loop { - ticker.tick().await; - let snapshot = HttpMetricsSnapshot { - request_count: metrics.request_count.load(Ordering::Relaxed), - total_duration_ms: metrics.total_duration_ms.load(Ordering::Relaxed), - status_2xx: metrics.status_2xx.load(Ordering::Relaxed), - status_4xx: metrics.status_4xx.load(Ordering::Relaxed), - status_5xx: metrics.status_5xx.load(Ordering::Relaxed), - }; - if let Ok(mut g) = guard.write() { - *g = snapshot; - } - } - }); -} - -/// Prometheus exposition handler. -/// -/// Requires `web::Data` injected via `.app_data()`. -pub async fn prometheus_handler( - _: HttpRequest, - snapshot: web::Data, - handle: actix_web::web::Data, -) -> HttpResponse { - // Render all `metrics`-crate metrics (RoomMetrics etc.) registered with the - // global recorder installed by `install_recorder()`. - let body = handle.render(); - - // Append live HttpMetrics from the shared snapshot. - let snap = snapshot.read().expect("metrics snapshot lock poisoned"); - let http_extra = format!( - concat!( - "# TYPE http_requests_total counter\n", - "http_requests_total{{service=\"app\",protocol=\"HTTP\"}} {}\n", - "# TYPE http_request_duration_ms_total counter\n", - "http_request_duration_ms_total{{service=\"app\"}} {}\n", - "# TYPE http_requests_by_status_class gauge\n", - "http_requests_by_status_class{{service=\"app\",status_class=\"2xx\"}} {}\n", - "http_requests_by_status_class{{service=\"app\",status_class=\"4xx\"}} {}\n", - "http_requests_by_status_class{{service=\"app\",status_class=\"5xx\"}} {}\n", - ), - snap.request_count, - snap.total_duration_ms, - snap.status_2xx, - snap.status_4xx, - snap.status_5xx, - ); - - let combined = format!("{}{}", body, http_extra); - - HttpResponse::Ok() - .content_type("text/plain; version=0.0.4; charset=utf-8") - .body(combined) -} diff --git a/libs/observability/src/push.rs b/libs/observability/src/push.rs deleted file mode 100644 index 611a95d..0000000 --- a/libs/observability/src/push.rs +++ /dev/null @@ -1,343 +0,0 @@ -//! Metrics push client for active reporting to apps/metrics. -//! -//! Each app instance periodically pushes ALL collected data to apps/metrics: -//! - Business counters (projects created, issues opened, etc.) -//! - System metrics (CPU, memory, uptime) -//! - HTTP metrics (request counts, status classes, per-endpoint) -//! - Latency histogram snapshots (p50/p90/p99/max) -//! - AI token usage (input/output/cost by model) -//! - Task queue stats (queued/running/completed/failed) -//! -//! Usage: -//! let pusher = MetricsPusher::new("http://metrics-aggregator:9090", "app"); -//! pusher.spawn(metrics, prometheus_handle, Duration::from_secs(15)); - -use std::collections::HashMap; -use std::sync::Arc; -use std::time::Duration; - -use serde::{Deserialize, Serialize}; - -use crate::instance_id; -use crate::metrics_middleware::HttpMetrics; - -// ── Payload types ────────────────────────────────────────────────────────────── - -/// Full payload pushed to apps/metrics via POST /api/v1/push. -#[derive(Debug, Serialize, Deserialize)] -pub struct MetricsPayload { - /// Source app name (e.g. "app", "gitserver", "git-hook"). - pub app: String, - /// Unique instance identifier (hostname or INSTANCE_ID env var). - pub instance: String, - /// Unix timestamp (seconds). - pub timestamp: i64, - /// HTTP metrics snapshot. - #[serde(skip_serializing_if = "Option::is_none")] - pub http: Option, - /// System metrics (CPU, memory, uptime). - #[serde(skip_serializing_if = "Option::is_none")] - pub system: Option, - /// Business counter values extracted from Prometheus recorder. - #[serde(skip_serializing_if = "HashMap::is_empty")] - pub business: HashMap, - /// AI token usage stats. - #[serde(skip_serializing_if = "Option::is_none")] - pub token_usage: Option, - /// Task queue stats. - #[serde(skip_serializing_if = "Option::is_none")] - pub tasks: Option, - /// Latency histogram snapshots (per-endpoint p50/p90/p99/max). - #[serde(skip_serializing_if = "HashMap::is_empty")] - pub latency: HashMap, - /// Raw log lines collected since last push. - #[serde(skip_serializing_if = "Vec::is_empty")] - pub logs: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct HttpPayload { - pub requests_total: u64, - pub request_duration_ms_total: u64, - pub requests_2xx: u64, - pub requests_4xx: u64, - pub requests_5xx: u64, - /// Per-endpoint counters: endpoint → count. - pub endpoints: HashMap, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SystemPayload { - /// CPU usage percentage (0–100). - pub cpu_usage_percent: f32, - /// Used memory in MB. - pub memory_used_mb: u64, - /// Total memory in MB. - pub memory_total_mb: u64, - /// Process uptime in seconds. - pub uptime_secs: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TokenUsagePayload { - pub ai_input_tokens_total: i64, - pub ai_output_tokens_total: i64, - pub ai_calls_total: i64, - pub ai_calls_success: i64, - pub ai_calls_failure: i64, - /// Cost breakdown by model name. - #[serde(skip_serializing_if = "HashMap::is_empty")] - pub by_model: HashMap, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ModelTokenUsage { - pub input_tokens: i64, - pub output_tokens: i64, - pub calls: i64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TaskStatsPayload { - pub queued: i64, - pub running: i64, - pub completed: i64, - pub failed: i64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LatencySnapshot { - pub p50_ms: f64, - pub p90_ms: f64, - pub p99_ms: f64, - pub max_ms: f64, - pub count: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LogEntry { - pub timestamp: i64, - pub level: String, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub target: Option, -} - -// ── System metrics collector ─────────────────────────────────────────────────── - -/// Collect system metrics (CPU, memory, uptime) using sysinfo. -pub fn collect_system_metrics(start_time: chrono::DateTime) -> SystemPayload { - let mut sys = sysinfo::System::new(); - sys.refresh_cpu_usage(); - sys.refresh_memory(); - - // CPU usage: average across all cores - let cpu_usage = sys.global_cpu_usage(); - - let memory_total_mb = sys.total_memory() / 1024 / 1024; - let memory_used_mb = sys.used_memory() / 1024 / 1024; - let uptime_secs = (chrono::Utc::now() - start_time).num_seconds().max(0) as u64; - - SystemPayload { - cpu_usage_percent: cpu_usage, - memory_used_mb, - memory_total_mb, - uptime_secs, - } -} - -// ── Metrics pusher ───────────────────────────────────────────────────────────── - -/// Client that periodically pushes ALL metrics to apps/metrics. -pub struct MetricsPusher { - aggregator_url: String, - app_name: String, - client: reqwest::Client, - start_time: chrono::DateTime, -} - -impl MetricsPusher { - /// Create a new pusher. - /// - /// `aggregator_url`: base URL of apps/metrics (e.g. `http://metrics-aggregator:9090`). - /// `app_name`: logical name of this app instance. - pub fn new(aggregator_url: impl Into, app_name: impl Into) -> Self { - Self { - aggregator_url: aggregator_url.into().trim_end_matches('/').to_string(), - app_name: app_name.into(), - client: reqwest::Client::builder() - .timeout(Duration::from_secs(5)) - .build() - .expect("valid reqwest client"), - start_time: chrono::Utc::now(), - } - } - - /// Build the full push payload from all available data sources. - pub fn build_payload( - &self, - http_metrics: &HttpMetrics, - prometheus_handle: &metrics_exporter_prometheus::PrometheusHandle, - ) -> MetricsPayload { - // HTTP metrics - let snapshot = http_metrics.snapshot(); - let http = Some(HttpPayload { - requests_total: snapshot - .get("http_requests_total") - .and_then(|v| v.as_u64()) - .unwrap_or(0), - request_duration_ms_total: snapshot - .get("http_request_duration_ms_total") - .and_then(|v| v.as_u64()) - .unwrap_or(0), - requests_2xx: snapshot - .get("http_requests_2xx") - .and_then(|v| v.as_u64()) - .unwrap_or(0), - requests_4xx: snapshot - .get("http_requests_4xx") - .and_then(|v| v.as_u64()) - .unwrap_or(0), - requests_5xx: snapshot - .get("http_requests_5xx") - .and_then(|v| v.as_u64()) - .unwrap_or(0), - endpoints: snapshot - .iter() - .filter(|(k, _)| k.starts_with("http_endpoint_")) - .filter_map(|(k, v)| v.as_u64().map(|n| (k.clone(), n))) - .collect(), - }); - - // System metrics - let system = Some(collect_system_metrics(self.start_time)); - - // Extract business counters from Prometheus text output - let prom_text = prometheus_handle.render(); - let business = crate::prometheus_exporter::render_to_hashmap(&prom_text); - // Filter to only business-level counters (skip http_ and ai_ prefixes - // that are already captured in their dedicated sections) - let business_filtered: HashMap = business - .into_iter() - .filter(|(k, _)| { - !k.starts_with("http_") - && !k.starts_with("push_") - && !k.starts_with("ai_call_latency") - }) - .map(|(k, v)| (k, v.as_f64().unwrap_or(0.0))) - .collect(); - - // Token usage from business counters - let token_usage = Some(TokenUsagePayload { - ai_input_tokens_total: business_filtered - .get("ai_input_tokens_total") - .copied() - .map(|v| v as i64) - .unwrap_or(0), - ai_output_tokens_total: business_filtered - .get("ai_output_tokens_total") - .copied() - .map(|v| v as i64) - .unwrap_or(0), - ai_calls_total: business_filtered - .get("ai_calls_total") - .copied() - .map(|v| v as i64) - .unwrap_or(0), - ai_calls_success: business_filtered - .get("ai_calls_success") - .copied() - .map(|v| v as i64) - .unwrap_or(0), - ai_calls_failure: business_filtered - .get("ai_calls_failure") - .copied() - .map(|v| v as i64) - .unwrap_or(0), - by_model: HashMap::new(), // Populated by apps that track per-model usage - }); - - MetricsPayload { - app: self.app_name.clone(), - instance: instance_id(), - timestamp: chrono::Utc::now().timestamp(), - http, - system, - business: business_filtered, - token_usage, - tasks: None, // Populated by apps that have task queues - latency: HashMap::new(), // Populated from histogram data - logs: Vec::new(), - } - } - - /// Push metrics once. Silently ignores errors. - pub async fn push_once( - &self, - http_metrics: &HttpMetrics, - prometheus_handle: &metrics_exporter_prometheus::PrometheusHandle, - ) { - let payload = self.build_payload(http_metrics, prometheus_handle); - let url = format!("{}/api/v1/push", self.aggregator_url); - - if let Err(e) = self.client.post(&url).json(&payload).send().await { - tracing::debug!(error = %e, "metrics push failed, skipping"); - } - } - - /// Spawn a background task that pushes metrics every `interval`. - pub fn spawn( - self, - http_metrics: Arc, - prometheus_handle: Arc, - interval: Duration, - ) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - let mut ticker = tokio::time::interval(interval); - loop { - ticker.tick().await; - self.push_once(&http_metrics, &prometheus_handle).await; - } - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn payload_serialises_to_json() { - let _pusher = MetricsPusher::new("http://localhost:9090", "test-app"); - let metrics = HttpMetrics::new(); - metrics - .request_count - .fetch_add(42, std::sync::atomic::Ordering::Relaxed); - let _handle = metrics_exporter_prometheus::PrometheusBuilder::new() - .build_recorder() - .handle(); - // Don't set global recorder in tests — it conflicts with other tests. - // Just check the HTTP payload portion. - let snapshot = metrics.snapshot(); - let http = HttpPayload { - requests_total: snapshot - .get("http_requests_total") - .and_then(|v| v.as_u64()) - .unwrap_or(0), - request_duration_ms_total: 0, - requests_2xx: 0, - requests_4xx: 0, - requests_5xx: 0, - endpoints: HashMap::new(), - }; - let json = serde_json::to_string(&http).unwrap(); - assert!(json.contains("\"requests_total\":42")); - } - - #[test] - fn system_payload_has_fields() { - let payload = collect_system_metrics(chrono::Utc::now() - chrono::Duration::seconds(100)); - assert!(payload.uptime_secs >= 100); - assert!(payload.memory_total_mb > 0); - } -} diff --git a/libs/observability/src/slog_json.rs b/libs/observability/src/slog_json.rs deleted file mode 100644 index 1a1653e..0000000 --- a/libs/observability/src/slog_json.rs +++ /dev/null @@ -1,3 +0,0 @@ -// This file is intentionally left empty. -// Slog has been migrated to tracing. -// Replaced by tracing_subscriber in observability crate v2. diff --git a/libs/observability/src/tracing_fmt.rs b/libs/observability/src/tracing_fmt.rs deleted file mode 100644 index d3475f1..0000000 --- a/libs/observability/src/tracing_fmt.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Tracing subscriber initialisation. -//! -//! Terminal (TTY): human-readable format with colors -//! Pipeline (non-TTY): JSON output for VictoriaLogs -//! Override via `APP_LOG_FORMAT=json|pretty` env var. - -use crate::msg_json_fmt::MsgJsonFormat; - -use once_cell::sync::Lazy; -use std::io::IsTerminal; -use std::str::FromStr; -use tracing_subscriber::Layer; -use tracing_subscriber::{ - EnvFilter, - fmt::{self, format::FmtSpan}, - layer::SubscriberExt, - util::SubscriberInitExt, -}; - -/// Global instance identifier, resolved once at startup. -/// Priority: `INSTANCE_ID` env var → system hostname → `"unknown"`. -pub static INSTANCE_ID: Lazy = Lazy::new(|| { - std::env::var("INSTANCE_ID") - .ok() - .filter(|s| !s.is_empty()) - .or_else(|| { - hostname::get() - .ok() - .and_then(|h| h.into_string().ok()) - .filter(|s| !s.is_empty()) - }) - .unwrap_or_else(|| "unknown".to_string()) -}); - -/// Returns the platform-wide instance identifier for this process. -pub fn instance_id() -> String { - INSTANCE_ID.clone() -} - -/// Determines the log format based on environment and TTY detection. -fn use_json() -> bool { - match std::env::var("APP_LOG_FORMAT").as_deref() { - Ok("json") => true, - Ok("pretty") | Ok("compact") => false, - _ => !std::io::stdout().is_terminal(), // TTY → pretty, non-TTY → json - } -} - -/// Initialises the global tracing subscriber. -/// -/// TTY terminals get human-readable output with colors. -/// Non-TTY (pipes, container logs) get JSON for log aggregation. -/// `APP_LOG_FORMAT=json|pretty` overrides auto-detection. -/// `RUST_LOG` env var controls the log level filter. -/// -/// Pass `defer = true` when OTLP will be initialized afterwards via `init_otlp()`. -pub fn init_tracing_subscriber(level: &str, defer: bool) { - let env_filter = EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::from_str(level)) - .unwrap_or_else(|_| EnvFilter::from_str("info").expect("default log level")); - - let fmt_layer: Box + Send + Sync> = if use_json() { - let mut layer = fmt::layer().event_format(MsgJsonFormat); - layer.set_span_events(FmtSpan::CLOSE); - <_ as Layer<_>>::boxed(layer) - } else { - let mut layer = fmt::layer() - .with_target(false) - .with_level(true) - .with_ansi(std::io::stdout().is_terminal()) - .pretty(); - layer.set_span_events(FmtSpan::CLOSE); - <_ as Layer<_>>::boxed(layer) - }; - - let registry = tracing_subscriber::registry() - .with(env_filter) - .with(fmt_layer); - - if defer { - return; - } - - let _ = registry.try_init(); -} diff --git a/libs/observability/src/tracing_init.rs b/libs/observability/src/tracing_init.rs deleted file mode 100644 index 04caa3a..0000000 --- a/libs/observability/src/tracing_init.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Tracing initialization. -//! -//! Call `init_tracing()` during application startup to set up the -//! tracing-subscriber fmt layer (writes human-readable spans to stderr). - -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::{EnvFilter, fmt}; - -/// Initialize tracing with a fmt layer. -/// -/// The `EnvFilter` reads the `RUST_LOG` environment variable to -/// set the log level (e.g. `RUST_LOG=info`). -pub fn init_tracing() { - let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); - - let fmt_layer = fmt::layer() - .with_target(true) - .with_thread_ids(false) - .with_file(true) - .with_line_number(true) - .compact(); - - let registry = tracing_subscriber::registry() - .with(env_filter) - .with(fmt_layer); - - tracing::subscriber::set_global_default(registry) - .expect("failed to set global tracing subscriber"); -} diff --git a/libs/observability/src/tracing_middleware.rs b/libs/observability/src/tracing_middleware.rs deleted file mode 100644 index 6489cf2..0000000 --- a/libs/observability/src/tracing_middleware.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Actix-web middleware that creates a `tracing::Span` for each HTTP request. -//! -//! The span is set as the current context so all downstream async code is -//! automatically instrumented. When the `tracing_opentelemetry` layer is -//! active, spans are exported via OTLP. - -use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; -use std::sync::Arc; -use std::task::{Context, Poll}; - -/// Actix-web middleware that creates a tracing span per request. -pub struct TracingSpanMiddleware; - -impl TracingSpanMiddleware { - pub fn new() -> Self { - Self - } -} - -impl Default for TracingSpanMiddleware { - fn default() -> Self { - Self::new() - } -} - -impl Transform for TracingSpanMiddleware -where - S: Service, Error = actix_web::Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Transform = TracingSpanMiddlewareService; - type InitError = (); - type Future = std::future::Ready>; - - fn new_transform(&self, service: S) -> Self::Future { - std::future::ready(Ok(TracingSpanMiddlewareService { - service: Arc::new(service), - })) - } -} - -pub struct TracingSpanMiddlewareService { - service: Arc, -} - -impl Clone for TracingSpanMiddlewareService { - fn clone(&self) -> Self { - Self { - service: self.service.clone(), - } - } -} - -impl Service for TracingSpanMiddlewareService -where - S: Service, Error = actix_web::Error> + 'static, - S::Future: 'static, - B: 'static, -{ - type Response = ServiceResponse; - type Error = actix_web::Error; - type Future = std::pin::Pin< - Box> + 'static>, - >; - - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - let method = req.method().to_string(); - let path = req.path().to_string(); - let service = self.service.clone(); - - Box::pin(async move { - // Set _msg for VictoriaLogs before entering the span. - // The JSON formatter reads this thread-local value. - crate::msg_json_fmt::set_span_msg(format!("{} {}", method, path)); - let span = tracing::info_span!("HTTP {method} {path}", method = %method, path = %path); - let _guard = span.enter(); - let res = service.call(req).await?; - Ok(res) - }) - } -} diff --git a/libs/queue/Cargo.toml b/libs/queue/Cargo.toml deleted file mode 100644 index d094df9..0000000 --- a/libs/queue/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "queue" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[lib] -path = "lib.rs" -name = "queue" - -[dependencies] -redis = { workspace = true } -deadpool-redis = { workspace = true, features = ["rt_tokio_1", "cluster-async", "cluster"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tokio = { workspace = true, features = ["rt", "rt-multi-thread", "sync", "time", "macros"] } -tokio-stream = { workspace = true } -futures = { workspace = true } -anyhow = { workspace = true } -thiserror = { workspace = true } -uuid = { workspace = true, features = ["v7", "v4", "serde"] } -chrono = { workspace = true, features = ["serde"] } -tracing = { workspace = true } -metrics = { workspace = true } -async-nats = { workspace = true } -config = { path = "../config" } -futures-util = { workspace = true } - -[lints] -workspace = true diff --git a/libs/queue/lib.rs b/libs/queue/lib.rs deleted file mode 100644 index 3e40491..0000000 --- a/libs/queue/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Room message queue: NATS JetStream (persistence) + Core NATS (broadcast). - -pub mod nats_client; -pub mod producer; -pub mod types; -pub mod worker; - -pub use nats_client::NatsClient; -pub use producer::{MessageProducer, NatsPublishResult}; -pub use types::{ - AgentTaskEvent, ChatMessageEvent, ChatStreamChunkEvent, EmailEnvelope, ProjectRoomEvent, - ReactionGroup, RoomMessageEnvelope, RoomMessageEvent, RoomMessageStreamChunkEvent, TypingEvent, -}; -pub use worker::{ - EmailSendFn, EmailSendFut, NatsConsumeFn, PersistFn, room_worker_task, start as start_worker, - start_email_worker, -}; diff --git a/libs/queue/nats_client.rs b/libs/queue/nats_client.rs deleted file mode 100644 index e4c812c..0000000 --- a/libs/queue/nats_client.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! NATS client — single connection shared across MessageProducer, workers, and pubsub. -//! -//! Uses Core NATS for real-time broadcast (fan-out) and JetStream for message -//! persistence (durable consumers). Replaces Redis Pub/Sub + Redis Streams. - -use std::sync::Arc; -use std::time::Duration; - -use async_nats::jetstream; -use config::AppConfig; - -pub struct NatsClient { - pub client: async_nats::Client, - pub jetstream: jetstream::Context, - stream_name: String, - connected: Arc, -} - -impl NatsClient { - pub async fn connect(config: &AppConfig) -> Option { - let url = config.nats_url()?; - let token = config.nats_token()?; - - let opts = async_nats::ConnectOptions::with_token(token) - .retry_on_initial_connect() - .connection_timeout(Duration::from_secs(10)) - .reconnect_delay_callback(|attempts| { - let ms = 100 * 2u64.saturating_pow(attempts as u32); - Duration::from_millis(ms.min(30_000)) - }) - .event_callback(|event| async move { - match event { - async_nats::Event::Connected => { - tracing::info!("NATS connected"); - } - async_nats::Event::Disconnected => { - tracing::warn!("NATS disconnected, reconnecting..."); - } - async_nats::Event::ServerError(e) => { - tracing::warn!(error = %e, "NATS server error"); - } - _ => {} - } - }); - - match opts.connect(&url).await { - Ok(client) => { - let connected = Arc::new(std::sync::atomic::AtomicBool::new(true)); - let connected_clone = connected.clone(); - - // Track connection state for health checks - let client_clone = client.clone(); - tokio::spawn(async move { - let mut attempts: u64 = 0; - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - let was = connected_clone.load(std::sync::atomic::Ordering::SeqCst); - let is = client_clone.connection_state() - == async_nats::connection::State::Connected; - if was != is { - connected_clone.store(is, std::sync::atomic::Ordering::SeqCst); - if is { - attempts = 0; - } - } - if !is { - attempts += 1; - if attempts > 12 { - tracing::error!("NATS disconnected for {}s", attempts * 5); - } - } - } - }); - - let jetstream = jetstream::new(client.clone()); - let stream_name = config.nats_stream_name(); - - let nats = Self { - client, - jetstream, - stream_name, - connected, - }; - - if let Err(e) = nats.ensure_stream(config).await { - tracing::warn!(error = %e, "JetStream stream init failed"); - } else { - tracing::info!(stream = %nats.stream_name, "JetStream stream ready"); - } - - tracing::info!(url = %url, "NATS connected"); - Some(nats) - } - Err(e) => { - tracing::warn!(error = %e, url = %url, "NATS connect failed — running without NATS"); - None - } - } - } - - /// Create or verify the JetStream stream for room message persistence. - pub async fn ensure_stream(&self, config: &AppConfig) -> anyhow::Result<()> { - let stream_config = jetstream::stream::Config { - name: self.stream_name.clone(), - subjects: vec![ - "room.message.>".to_string(), - "room.chunk.>".to_string(), - "chat.message.>".to_string(), - "chat.chunk.>".to_string(), - "chat.subagent.chunk.>".to_string(), - ], - retention: jetstream::stream::RetentionPolicy::Interest, - max_age: Duration::from_secs(config.nats_max_age_secs()), - storage: jetstream::stream::StorageType::File, - max_messages_per_subject: 100_000, - ..Default::default() - }; - - self.jetstream - .get_or_create_stream(stream_config) - .await - .map_err(|e| anyhow::anyhow!("JetStream stream create failed: {}", e))?; - - Ok(()) - } - - pub fn is_connected(&self) -> bool { - self.connected.load(std::sync::atomic::Ordering::SeqCst) - } - - pub fn stream_name(&self) -> &str { - &self.stream_name - } - - /// Publish to core NATS for real-time broadcast. Fire-and-forget. - pub async fn core_publish(&self, subject: String, payload: Vec) { - if let Err(e) = self.client.publish(subject.clone(), payload.into()).await { - tracing::warn!(subject = %subject, error = %e, "NATS core publish failed"); - } - } - - /// Publish to JetStream and await the publish acknowledgement. - /// Returns the stream sequence number, or 0 on failure. - pub async fn jetstream_publish( - &self, - subject: String, - payload: Vec, - ) -> anyhow::Result { - let ack_future = self - .jetstream - .publish(subject.clone(), payload.into()) - .await - .map_err(|e| anyhow::anyhow!("JetStream publish failed: {}", e))?; - - let ack = ack_future - .await - .map_err(|e| anyhow::anyhow!("JetStream publish ack failed: {}", e))?; - - Ok(ack.sequence) - } - - /// Create a core NATS subscription. Returns a subscriber receiver. - pub async fn subscribe(&self, subject: &str) -> anyhow::Result { - self.client - .subscribe(subject.to_string()) - .await - .map_err(|e| anyhow::anyhow!("NATS subscribe failed: {}", e)) - } - - /// Create or get an ephemeral JetStream push consumer for broadcast delivery. - /// Returns a message stream that will receive messages published to the subject. - pub async fn consumer_messages( - &self, - durable: &str, - filter_subject: &str, - config: &AppConfig, - ) -> anyhow::Result { - let stream = self - .jetstream - .get_stream(&self.stream_name) - .await - .map_err(|e| anyhow::anyhow!("JetStream get stream failed: {}", e))?; - - let pull_config = async_nats::jetstream::consumer::pull::Config { - durable_name: Some(durable.to_string()), - filter_subject: filter_subject.to_string(), - max_deliver: config.nats_max_deliver(), - ack_wait: Duration::from_secs(config.nats_ack_wait_secs()), - max_ack_pending: config.nats_buffer_size() as i64, - ..Default::default() - }; - - let consumer = stream - .get_or_create_consumer(durable, pull_config) - .await - .map_err(|e| anyhow::anyhow!("JetStream consumer create failed: {}", e))?; - - consumer - .messages() - .await - .map_err(|e| anyhow::anyhow!("JetStream consumer messages failed: {}", e)) - } -} diff --git a/libs/queue/producer.rs b/libs/queue/producer.rs deleted file mode 100644 index 90082a6..0000000 --- a/libs/queue/producer.rs +++ /dev/null @@ -1,321 +0,0 @@ -//! Publishes room messages via NATS JetStream (persistence) + Core NATS (broadcast). -//! -//! Architecture: -//! - JetStream publish: durable, acknowledged, for message persistence + consumer-based delivery -//! - Core NATS publish: fire-and-forget, for real-time broadcast fan-out across nodes - -use crate::nats_client::NatsClient; -use crate::types::{ - AgentTaskEvent, ChatMessageEvent, ChatStreamChunkEvent, EmailEnvelope, ProjectRoomEvent, - ReactionGroup, RoomMessageEnvelope, RoomMessageEvent, RoomMessageStreamChunkEvent, -}; -use std::sync::Arc; - -pub type NatsPublishResult = u64; - -/// Publishes room messages via NATS. -#[derive(Clone)] -pub struct MessageProducer { - /// JetStream publish function for durable/persisted messages. - pub jetstream_publish: Arc< - dyn Fn( - String, - Vec, - ) - -> std::pin::Pin> + Send>> - + Send - + Sync, - >, - /// Core NATS publish function for real-time broadcast (fire-and-forget). - pub core_publish: Arc< - dyn Fn(String, Vec) -> std::pin::Pin + Send>> - + Send - + Sync, - >, - /// Redis connection getter — kept for cache/seq access (notification count, etc.) - pub get_redis: Arc< - dyn Fn() -> tokio::task::JoinHandle> - + Send - + Sync, - >, - /// Direct NATS client reference for subscriptions (watch endpoints, etc.) - pub nats: Option>, -} - -impl MessageProducer { - pub fn new( - nats: Option>, - get_redis: Arc< - dyn Fn() -> tokio::task::JoinHandle> - + Send - + Sync, - >, - ) -> Self { - let js_fn: Arc< - dyn Fn( - String, - Vec, - ) -> std::pin::Pin< - Box> + Send>, - > + Send - + Sync, - > = if let Some(ref n) = nats { - let n = n.clone(); - Arc::new(move |subject: String, payload: Vec| { - let n = n.clone(); - Box::pin(async move { n.jetstream_publish(subject, payload).await }) as _ - }) - } else { - Arc::new(|_subject: String, _payload: Vec| { - Box::pin(async move { Ok::(0) }) as _ - }) - }; - - let core_fn: Arc< - dyn Fn( - String, - Vec, - ) - -> std::pin::Pin + Send>> - + Send - + Sync, - > = if let Some(ref n) = nats { - let n = n.clone(); - Arc::new(move |subject: String, payload: Vec| { - let n = n.clone(); - Box::pin(async move { n.core_publish(subject, payload).await }) as _ - }) - } else { - Arc::new(|_subject: String, _payload: Vec| Box::pin(async move {}) as _) - }; - - Self { - jetstream_publish: js_fn, - core_publish: core_fn, - get_redis, - nats, - } - } - - /// Publish a room message — broadcast via JetStream for reliable cross-node delivery. - /// Persistence (DB INSERT) must happen in the caller BEFORE calling this method. - pub async fn publish( - &self, - room_id: uuid::Uuid, - envelope: RoomMessageEnvelope, - ) -> anyhow::Result<()> { - let subject = format!("room.message.{}", room_id); - let event = RoomMessageEvent::from(envelope); - let payload = serde_json::to_vec(&event)?; - - let seq = (self.jetstream_publish)(subject, payload).await?; - tracing::info!(room_id = %room_id, seq = seq, "message broadcast via JetStream"); - Ok(()) - } - - /// Publish a stream chunk event via JetStream for reliable cross-node real-time delivery. - /// Chunks are transient — NOT persisted to DB. - pub async fn publish_stream_chunk(&self, event: &RoomMessageStreamChunkEvent) { - let subject = format!("room.chunk.{}", event.room_id); - let payload = match serde_json::to_vec(event) { - Ok(p) => p, - Err(e) => { - tracing::error!(error = %e, "serialise stream chunk failed"); - return; - } - }; - if let Err(e) = (self.jetstream_publish)(subject, payload).await { - tracing::warn!(error = %e, room_id = %event.room_id, "JetStream chunk publish failed"); - } - } - - /// Publish a project-level room event via Core NATS (no JetStream persistence). - pub async fn publish_project_room_event( - &self, - project_id: uuid::Uuid, - event: ProjectRoomEvent, - ) { - let subject = format!("project.event.{}", project_id); - let payload = match serde_json::to_vec(&event) { - Ok(p) => p, - Err(e) => { - tracing::error!(error = %e, "serialise ProjectRoomEvent failed"); - return; - } - }; - (self.core_publish)(subject, payload).await; - } - - /// Publish an agent task event via Core NATS (no JetStream persistence). - pub async fn publish_agent_task_event(&self, project_id: uuid::Uuid, event: AgentTaskEvent) { - let subject = format!("task.event.{}", project_id); - let payload = match serde_json::to_vec(&event) { - Ok(p) => p, - Err(e) => { - tracing::error!(error = %e, "serialise AgentTaskEvent failed"); - return; - } - }; - (self.core_publish)(subject, payload).await; - } - - /// Broadcast a reaction-update event via Core NATS. - pub async fn publish_reaction_event( - &self, - room_id: uuid::Uuid, - message_id: uuid::Uuid, - reactions: Vec, - ) { - let event = RoomMessageEvent { - id: uuid::Uuid::now_v7(), - room_id, - sender_type: String::new(), - sender_id: None, - thread_id: None, - in_reply_to: None, - content: String::new(), - content_type: String::new(), - thinking_content: None, - send_at: chrono::Utc::now(), - seq: 0, - display_name: None, - reactions: Some(reactions), - message_id: Some(message_id), - }; - let payload = match serde_json::to_vec(&event) { - Ok(p) => p, - Err(e) => { - tracing::error!(error = %e, "serialise reaction event failed"); - return; - } - }; - (self.core_publish)(format!("room.broadcast.{}", room_id), payload).await; - } - - /// Publish an email message via JetStream for async processing. - pub async fn publish_email(&self, envelope: EmailEnvelope) -> anyhow::Result { - let subject = "email.queue".to_string(); - let payload = serde_json::to_string(&envelope)?.into_bytes(); - let seq = (self.jetstream_publish)(subject, payload).await?; - let msg_id = format!("nats:{}", seq); - tracing::info!(to = %envelope.to, msg_id = %msg_id, "email queued to NATS"); - metrics::counter!("email_queued_total").increment(1); - Ok(msg_id) - } - - // ── Chat message publishing ────────────────────────────────────────────── - - /// Publish a chat message via JetStream for persistence + multi-viewer delivery. - pub async fn publish_chat_message(&self, event: &ChatMessageEvent) -> anyhow::Result { - let subject = format!("chat.message.{}", event.conversation_id); - let payload = serde_json::to_vec(event)?; - let seq = (self.jetstream_publish)(subject.clone(), payload.clone()).await?; - (self.core_publish)(subject, payload).await; - tracing::info!(conversation_id = %event.conversation_id, seq = seq, "chat message broadcast via JetStream"); - Ok(seq) - } - - /// Publish a chat stream chunk via JetStream for real-time multi-viewer streaming. - pub async fn publish_chat_chunk(&self, event: &ChatStreamChunkEvent) { - let subject = format!("chat.chunk.{}", event.conversation_id); - let payload = match serde_json::to_vec(event) { - Ok(p) => p, - Err(e) => { - tracing::error!(error = %e, "serialise chat chunk failed"); - return; - } - }; - (self.core_publish)(subject.clone(), payload.clone()).await; - if let Err(e) = (self.jetstream_publish)(subject, payload).await { - tracing::warn!(error = %e, conversation_id = %event.conversation_id, "JetStream chat chunk publish failed"); - } - } - - /// Publish a sub-agent stream chunk via JetStream for real-time multi-viewer delivery. - /// Subject: `chat.subagent.chunk.{conversation_id}.{children_id}` - pub async fn publish_sub_agent_chunk(&self, event: &crate::types::SubAgentStreamChunkEvent) { - let subject = format!( - "chat.subagent.chunk.{}.{}", - event.conversation_id, event.children_id - ); - let payload = match serde_json::to_vec(event) { - Ok(p) => p, - Err(e) => { - tracing::error!(error = %e, "serialise sub-agent chunk failed"); - return; - } - }; - if let Err(e) = self - .publish_sub_agent_chunk_redis(&subject, payload.clone()) - .await - { - tracing::warn!(error = %e, conversation_id = %event.conversation_id, children_id = %event.children_id, "Redis sub-agent chunk publish failed"); - } - let core_publish = self.core_publish.clone(); - let core_subject = subject.clone(); - let core_payload = payload.clone(); - tokio::spawn(async move { - core_publish(core_subject, core_payload).await; - }); - let jetstream_publish = self.jetstream_publish.clone(); - let conversation_id = event.conversation_id; - let children_id = event.children_id.clone(); - tokio::spawn(async move { - if let Err(e) = (jetstream_publish)(subject, payload).await { - tracing::warn!(error = %e, conversation_id = %conversation_id, children_id = %children_id, "JetStream sub-agent chunk publish failed"); - } - }); - } - - /// Publish a sub-agent chunk on Core NATS only. - /// Token-level sub-agent output is transient and latency-sensitive; final - /// output is persisted separately as an AI sub-agent session. - pub async fn publish_sub_agent_chunk_realtime( - &self, - event: &crate::types::SubAgentStreamChunkEvent, - ) { - let subject = format!( - "chat.subagent.chunk.{}.{}", - event.conversation_id, event.children_id - ); - let payload = match serde_json::to_vec(event) { - Ok(p) => p, - Err(e) => { - tracing::error!(error = %e, "serialise realtime sub-agent chunk failed"); - return; - } - }; - if let Err(e) = self - .publish_sub_agent_chunk_redis(&subject, payload.clone()) - .await - { - tracing::warn!(error = %e, conversation_id = %event.conversation_id, children_id = %event.children_id, "Redis realtime sub-agent chunk publish failed"); - } - let core_publish = self.core_publish.clone(); - tokio::spawn(async move { - core_publish(subject, payload).await; - }); - } - - async fn publish_sub_agent_chunk_redis( - &self, - subject: &str, - payload: Vec, - ) -> anyhow::Result<()> { - tokio::time::timeout(std::time::Duration::from_millis(500), async { - let handle = (self.get_redis)(); - let mut conn = handle - .await - .map_err(|e| anyhow::anyhow!("redis pool task panicked: {}", e))??; - let _: i32 = redis::cmd("PUBLISH") - .arg(subject) - .arg(payload) - .query_async(&mut conn) - .await - .map_err(|e| anyhow::anyhow!("redis publish failed: {}", e))?; - Ok(()) - }) - .await - .map_err(|_| anyhow::anyhow!("redis publish timed out"))? - } -} diff --git a/libs/queue/types.rs b/libs/queue/types.rs deleted file mode 100644 index b52ca25..0000000 --- a/libs/queue/types.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Message types shared between producer and worker. - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RoomMessageEnvelope { - pub id: Uuid, - pub dedup_key: Option, - pub room_id: Uuid, - pub sender_type: String, - pub sender_id: Option, - /// AI model ID — set when sender_type = "ai", used for display name lookups. - pub model_id: Option, - pub thread_id: Option, - pub in_reply_to: Option, - pub content: String, - pub content_type: String, - /// Accumulated AI reasoning/thinking text. - #[serde(skip_serializing_if = "Option::is_none")] - pub thinking_content: Option, - pub send_at: DateTime, - pub seq: i64, - /// Pre-resolved display name for the sender (e.g. AI model name). - #[serde(skip_serializing_if = "Option::is_none")] - pub display_name: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RoomMessageEvent { - pub id: Uuid, - pub room_id: Uuid, - pub sender_type: String, - pub sender_id: Option, - pub thread_id: Option, - pub in_reply_to: Option, - pub content: String, - pub content_type: String, - /// Accumulated AI reasoning/thinking text. - #[serde(skip_serializing_if = "Option::is_none")] - pub thinking_content: Option, - pub send_at: DateTime, - pub seq: i64, - pub display_name: Option, - /// Present when this event carries reaction updates for the message. - #[serde(skip_serializing_if = "Option::is_none")] - pub reactions: Option>, - /// Target message ID for reaction update events. - #[serde(skip_serializing_if = "Option::is_none")] - pub message_id: Option, -} - -/// Typing indicator event — broadcast to all room members. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TypingEvent { - pub room_id: Uuid, - pub user_id: Uuid, - pub username: String, - pub avatar_url: Option, - /// "start" or "stop" - pub action: String, - /// Sender type: "user" or "ai". Defaults to "user" if absent. - #[serde(skip_serializing_if = "Option::is_none")] - pub sender_type: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReactionGroup { - pub emoji: String, - pub count: i32, - pub reacted_by_me: bool, - /// Stored as strings (UUIDs) to match the frontend's `users: string[]` type. - pub users: Vec, -} - -impl From for RoomMessageEvent { - fn from(e: RoomMessageEnvelope) -> Self { - Self { - id: e.id, - room_id: e.room_id, - sender_type: e.sender_type, - sender_id: e.sender_id, - thread_id: e.thread_id, - in_reply_to: e.in_reply_to, - content: e.content, - content_type: e.content_type, - thinking_content: e.thinking_content, - send_at: e.send_at, - seq: e.seq, - display_name: e.display_name, - reactions: None, - message_id: None, - } - } -} - -impl From for RoomMessageEnvelope { - fn from(e: RoomMessageEvent) -> Self { - Self { - id: e.id, - dedup_key: None, - room_id: e.room_id, - sender_type: e.sender_type, - sender_id: e.sender_id, - model_id: None, - thread_id: e.thread_id, - in_reply_to: e.in_reply_to, - content: e.content, - content_type: e.content_type, - thinking_content: e.thinking_content, - send_at: e.send_at, - seq: e.seq, - display_name: e.display_name, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProjectRoomEvent { - pub event_type: String, - pub project_id: Uuid, - pub room_id: Option, - pub category_id: Option, - pub message_id: Option, - pub seq: Option, - pub timestamp: DateTime, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RoomMessageStreamChunkEvent { - pub message_id: Uuid, - pub room_id: Uuid, - /// Monotonically increasing sequence number for ordering within this stream. - #[serde(default)] - pub seq: u64, - pub content: String, - pub done: bool, - pub error: Option, - /// Human-readable AI model name (e.g. "Claude 3.5 Sonnet") for display. - pub display_name: Option, - /// What kind of content this chunk contains: "thinking", "answer", "tool_call", "tool_result". - pub chunk_type: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EmailEnvelope { - pub id: Uuid, - pub to: String, - pub subject: String, - pub body: String, - pub created_at: DateTime, -} - -/// Agent task event published via NATS core to notify WebSocket clients. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentTaskEvent { - /// Task ID - pub task_id: i64, - /// Project this task belongs to. - pub project_id: Uuid, - /// Parent task ID (null for root tasks). - pub parent_id: Option, - /// Event type: started | progress | done | failed | child_done - pub event: String, - /// Human-readable progress/status text. - pub message: Option, - /// Task output (only on done event). - pub output: Option, - /// Error message (only on failed event). - pub error: Option, - /// Current status. - pub status: String, - /// Timestamp. - pub timestamp: DateTime, -} - -/// Chat message event — broadcast via NATS JetStream for persistence + multi-viewer support. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChatMessageEvent { - pub message_id: Uuid, - pub conversation_id: Uuid, - pub project_id: Option, - pub sender_id: Uuid, - pub role: String, - pub content: String, - pub model: Option, - pub input_tokens: Option, - pub output_tokens: Option, - pub timestamp: DateTime, -} - -/// Chat stream chunk event — broadcast via NATS JetStream for real-time multi-viewer streaming. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChatStreamChunkEvent { - pub conversation_id: Uuid, - pub message_id: Uuid, - pub seq: u64, - pub content: String, - pub done: bool, - pub error: Option, - pub chunk_type: Option, - pub model_name: Option, -} - -/// Sub-agent stream chunk event — published to dedicated NATS subject `chat.subagent.chunk.{conversation_id}.{children_id}`. -/// Frontend subscribes to this subject via the sub-agent SSE endpoint using children_id. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SubAgentStreamChunkEvent { - pub conversation_id: Uuid, - pub children_id: String, - pub seq: u64, - pub content: String, - pub done: bool, - pub error: Option, - pub chunk_type: Option, - pub role: String, - pub task: String, -} - -/// Sub-agent session record — persisted after sub-agent completes. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SubAgentSessionRecord { - pub conversation_id: Uuid, - pub children_id: String, - pub role: String, - pub task: String, - pub output: String, - pub input_tokens: i64, - pub output_tokens: i64, - pub created_at: DateTime, -} diff --git a/libs/queue/worker.rs b/libs/queue/worker.rs deleted file mode 100644 index 0eb4163..0000000 --- a/libs/queue/worker.rs +++ /dev/null @@ -1,300 +0,0 @@ -//! Room message worker: NATS JetStream durable pull consumer. - -use crate::types::{EmailEnvelope, RoomMessageEnvelope, RoomMessageEvent}; -use futures::StreamExt; -use metrics::counter; -use std::sync::Arc; - -const BATCH_SIZE: usize = 50; -const FLUSH_INTERVAL_SECS: u64 = 1; - -/// NATS consumer function type: returns (message_data, ack_fn) pairs. -pub type NatsConsumeFn = Arc< - dyn Fn( - String, - usize, - ) -> std::pin::Pin< - Box< - dyn std::future::Future< - Output = anyhow::Result< - Vec<( - Vec, - Box< - dyn Fn() -> std::pin::Pin< - Box< - dyn std::future::Future> - + Send, - >, - > + Send, - >, - )>, - >, - > + Send, - >, - > + Send - + Sync, ->; - -/// Function that persists a batch of room message envelopes to the database. -pub type PersistFn = Arc) -> PersistFut + Send + Sync>; -pub type PersistFut = - std::pin::Pin> + Send>>; - -/// Start the room message worker that consumes from NATS JetStream per room. -pub async fn start( - nats: Option>, - _persist_fn: PersistFn, - mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, -) { - let Some(_nats) = nats else { - tracing::warn!("NATS not available, room workers disabled"); - let _ = shutdown_rx.recv().await; - return; - }; - - tracing::info!("room-message worker starting"); - - loop { - tokio::select! { - _ = shutdown_rx.recv() => { - tracing::info!("room-message worker shutting down"); - break; - } - _ = tokio::time::sleep(std::time::Duration::from_secs(FLUSH_INTERVAL_SECS)) => {} - } - - // Workers are spawned lazily per room when rooms are accessed. - // This start() just keeps the worker alive; actual room workers - // are spawned by spawn_room_workers() in workers_spawn.rs. - } -} - -/// Room worker that consumes from NATS JetStream for a specific room. -pub async fn room_worker_task( - room_id: uuid::Uuid, - nats: Arc, - persist_fn: PersistFn, - mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, -) { - let stream_name = nats.stream_name().to_string(); - let subject = format!("room.message.{}", room_id); - let durable = format!("worker-{}", room_id); - - tracing::info!(room_id = %room_id, durable = %durable, subject = %subject, "room worker task starting"); - - loop { - tokio::select! { - _ = shutdown_rx.recv() => { - tracing::info!(room_id = %room_id, "room worker task shutting down"); - break; - } - result = consume_once(&nats, &stream_name, &durable, &subject, &persist_fn) => { - match result { - Ok(0) => {} - Ok(n) => tracing::debug!(room_id = %room_id, n = n, "batch persisted"), - Err(e) => { - tracing::error!(room_id = %room_id, error = %e, "JetStream consume error"); - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - } - } - } - } - } -} - -async fn consume_once( - nats: &crate::NatsClient, - stream_name: &str, - durable: &str, - subject: &str, - persist_fn: &PersistFn, -) -> anyhow::Result { - let stream = nats - .jetstream - .get_stream(stream_name) - .await - .map_err(|e| anyhow::anyhow!("get stream failed: {}", e))?; - - let pull_config = async_nats::jetstream::consumer::pull::Config { - durable_name: Some(durable.to_string()), - filter_subject: subject.to_string(), - max_deliver: 3, - ack_wait: std::time::Duration::from_secs(10), - max_ack_pending: 256, - ..Default::default() - }; - - let consumer = stream - .get_or_create_consumer(durable, pull_config) - .await - .map_err(|e| anyhow::anyhow!("create consumer failed: {}", e))?; - - let mut messages = consumer - .messages() - .await - .map_err(|e| anyhow::anyhow!("consumer messages failed: {}", e))?; - - let mut batch: Vec = Vec::with_capacity(BATCH_SIZE); - let mut acks: Vec = Vec::new(); - - // Fetch up to BATCH_SIZE messages - for _ in 0..BATCH_SIZE { - match tokio::time::timeout(std::time::Duration::from_millis(500), messages.next()).await { - Ok(Some(Ok(msg))) => match serde_json::from_slice::(&msg.payload) { - Ok(event) => { - let env = RoomMessageEnvelope::from(event); - batch.push(env); - acks.push(msg); - } - Err(e) => { - tracing::warn!(error = %e, "malformed envelope"); - let _ = msg.ack().await; - } - }, - Ok(Some(Err(e))) => { - tracing::warn!(error = %e, "message error"); - } - Ok(None) => break, - Err(_) => break, - } - } - - if batch.is_empty() { - return Ok(0); - } - - let batch_size = batch.len(); - - if let Err(e) = persist_fn(batch).await { - tracing::error!(error = %e, "persist_fn failed — entries NOT acked (will retry)"); - return Err(e); - } - - for msg in &acks { - if let Err(e) = msg.ack().await { - tracing::warn!(error = %e, "ack failed"); - } - } - - tracing::info!(n = batch_size, "batch persisted and acked"); - Ok(batch_size) -} - -/// Email send function type. -pub type EmailSendFn = Arc) -> EmailSendFut + Send + Sync>; -pub type EmailSendFut = - std::pin::Pin> + Send>>; - -/// Start the email worker that consumes from NATS JetStream. -pub async fn start_email_worker( - nats: Option>, - send_fn: EmailSendFn, - mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, -) { - let Some(nats) = nats else { - tracing::warn!("NATS not available, email worker disabled"); - let _ = shutdown_rx.recv().await; - return; - }; - - let stream_name = nats.stream_name().to_string(); - let consumer = format!("email-worker-{}", uuid::Uuid::new_v4()); - tracing::info!(consumer = %consumer, "email worker starting"); - - loop { - tokio::select! { - _ = shutdown_rx.recv() => { - tracing::info!("email worker shutting down"); - break; - } - _ = tokio::time::sleep(std::time::Duration::from_secs(FLUSH_INTERVAL_SECS)) => {} - result = email_consume_once(&nats, &stream_name, &consumer, &send_fn) => { - match result { - Ok(0) => {} - Ok(n) => tracing::debug!(n = n, "email batch processed"), - Err(e) => { - tracing::error!(error = %e, "email JetStream consume error"); - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - } - } - } - } - } -} - -async fn email_consume_once( - nats: &crate::NatsClient, - stream_name: &str, - consumer_name: &str, - send_fn: &EmailSendFn, -) -> anyhow::Result { - let stream = nats - .jetstream - .get_stream(stream_name) - .await - .map_err(|e| anyhow::anyhow!("get stream failed: {}", e))?; - - let pull_config = async_nats::jetstream::consumer::pull::Config { - durable_name: Some(consumer_name.to_string()), - filter_subject: "email.queue".to_string(), - max_deliver: 3, - ack_wait: std::time::Duration::from_secs(10), - max_ack_pending: 256, - ..Default::default() - }; - - let consumer = stream - .get_or_create_consumer(consumer_name, pull_config) - .await - .map_err(|e| anyhow::anyhow!("create consumer failed: {}", e))?; - - let mut messages = consumer - .messages() - .await - .map_err(|e| anyhow::anyhow!("consumer messages failed: {}", e))?; - - let mut batch: Vec = Vec::with_capacity(BATCH_SIZE); - let mut acks: Vec = Vec::new(); - - for _ in 0..BATCH_SIZE { - match tokio::time::timeout(std::time::Duration::from_millis(500), messages.next()).await { - Ok(Some(Ok(msg))) => match serde_json::from_slice::(&msg.payload) { - Ok(env) => { - batch.push(env); - acks.push(msg); - } - Err(e) => { - tracing::warn!(error = %e, "malformed email envelope"); - let _ = msg.ack().await; - } - }, - Ok(Some(Err(e))) => { - tracing::warn!(error = %e, "email message error"); - } - Ok(None) => break, - Err(_) => break, - } - } - - if batch.is_empty() { - return Ok(0); - } - - let batch_size = batch.len(); - counter!("email_consumed_total").increment(batch_size as u64); - counter!("email_batch_size").increment(batch_size as u64); - - if let Err(e) = send_fn(batch).await { - tracing::error!(error = %e, "email send_fn failed — messages NOT acked (will retry)"); - return Err(e); - } - - for msg in &acks { - if let Err(e) = msg.ack().await { - tracing::warn!(error = %e, "NATS ack failed"); - } - } - - tracing::info!(n = batch_size, "email batch sent and acked"); - Ok(batch_size) -} diff --git a/libs/room/Cargo.toml b/libs/room/Cargo.toml deleted file mode 100644 index 381c7c6..0000000 --- a/libs/room/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "room" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -repository.workspace = true -readme.workspace = true -homepage.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true -documentation.workspace = true - -[lib] -path = "src/lib.rs" -name = "room" - -[dependencies] -models = { workspace = true } -db = { workspace = true } -session = { workspace = true } -queue = { workspace = true } -agent = { path = "../agent" } -observability = { path = "../observability" } -fctool = { path = "../fctool" } -config = { path = "../config" } - -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tracing = { workspace = true } -chrono = { workspace = true, features = ["serde"] } -uuid = { workspace = true, features = ["serde", "v7", "v4"] } -sea-orm = { workspace = true } -anyhow = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } -tokio-util = { workspace = true } -tokio-stream = { workspace = true } -futures = { workspace = true } -deadpool-redis = { workspace = true, features = ["rt_tokio_1", "cluster-async", "cluster"] } -utoipa = { workspace = true, features = ["uuid", "chrono"] } -metrics = "0.22" -regex-lite = "0.1.6" -redis = { workspace = true, features = ["tokio-comp", "connection-manager"] } -hostname = "0.4" -dashmap = "7.0.0-rc2" -lru = "0.12.0" -ammonia = "4.0" -async-nats.workspace = true - -[lints] -workspace = true diff --git a/libs/room/src/ai.rs b/libs/room/src/ai.rs deleted file mode 100644 index aef3169..0000000 --- a/libs/room/src/ai.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::error::RoomError; -use crate::service::RoomService; -use crate::ws_context::WsUserContext; -use chrono::Utc; -use models::agents::model as ai_model; -use models::rooms::room_ai; -use sea_orm::*; -use uuid::Uuid; - -impl RoomService { - pub async fn room_ai_list( - &self, - room_id: Uuid, - ctx: &WsUserContext, - ) -> Result, RoomError> { - let user_id = ctx.user_id; - self.require_room_access(room_id, user_id).await?; - - let configs = room_ai::Entity::find() - .filter(room_ai::Column::Room.eq(room_id)) - .all(&self.db) - .await?; - - if configs.is_empty() { - return Ok(Vec::new()); - } - - // Batch-fetch all referenced models to avoid N+1 queries - let model_ids: Vec = configs.iter().map(|c| c.model).collect(); - let models = ai_model::Entity::find() - .filter(ai_model::Column::Id.is_in(model_ids)) - .all(&self.db) - .await?; - - let model_names: std::collections::HashMap = - models.into_iter().map(|m| (m.id, m.name)).collect(); - - let mut responses = Vec::with_capacity(configs.len()); - for config in configs { - // Skip entries where model_name cannot be resolved — never expose UID - let Some(model_name) = model_names.get(&config.model).cloned() else { - tracing::warn!( - "room_ai_list: skipping config with unknown model_id={}", - config.model - ); - continue; - }; - let mut resp = super::RoomAiResponse::from(config); - resp.model_name = Some(model_name); - responses.push(resp); - } - - Ok(responses) - } - - pub async fn room_ai_upsert( - &self, - room_id: Uuid, - request: super::RoomAiUpsertRequest, - ctx: &WsUserContext, - ) -> Result { - let user_id = ctx.user_id; - self.require_room_admin(room_id, user_id).await?; - - let now = Utc::now(); - let existing = room_ai::Entity::find_by_id((room_id, request.model)) - .one(&self.db) - .await?; - - let saved = if let Some(existing) = existing { - let mut active: room_ai::ActiveModel = existing.into(); - if request.version.is_some() { - active.version = Set(request.version); - } - if request.history_limit.is_some() { - active.history_limit = Set(request.history_limit); - } - if request.system_prompt.is_some() { - active.system_prompt = Set(request.system_prompt); - } - if request.temperature.is_some() { - active.temperature = Set(request.temperature); - } - if request.max_tokens.is_some() { - active.max_tokens = Set(request.max_tokens); - } - if request.use_exact.is_some() { - active.use_exact = Set(request.use_exact.unwrap_or(true)); - } - if request.think.is_some() { - active.think = Set(request.think.unwrap_or(false)); - } - if request.stream.is_some() { - active.stream = Set(request.stream.unwrap_or(false)); - } - if request.min_score.is_some() { - active.min_score = Set(request.min_score); - } - if request.agent_type.is_some() { - active.agent_type = Set(request.agent_type); - } - active.updated_at = Set(now); - active.update(&self.db).await? - } else { - room_ai::ActiveModel { - room: Set(room_id), - model: Set(request.model), - version: Set(request.version), - call_count: Set(0), - last_call_at: Set(None), - history_limit: Set(request.history_limit), - system_prompt: Set(request.system_prompt), - temperature: Set(request.temperature), - max_tokens: Set(request.max_tokens), - use_exact: Set(request.use_exact.unwrap_or(true)), - think: Set(request.think.unwrap_or(false)), - stream: Set(request.stream.unwrap_or(false)), - min_score: Set(request.min_score), - agent_type: Set(request.agent_type), - created_at: Set(now), - updated_at: Set(now), - } - .insert(&self.db) - .await? - }; - - let model_name = ai_model::Entity::find_by_id(saved.model) - .one(&self.db) - .await - .ok() - .flatten() - .map(|m| m.name); - let mut resp = super::RoomAiResponse::from(saved); - resp.model_name = model_name; - - if let Ok(room) = self.find_room_or_404(room_id).await { - self.publish_room_event( - room.project, - super::RoomEventType::RoomAiUpdated, - Some(room_id), - None, - None, - None, - ) - .await; - } - - Ok(resp) - } - - pub async fn room_ai_delete( - &self, - room_id: Uuid, - model_id: Uuid, - ctx: &WsUserContext, - ) -> Result<(), RoomError> { - let user_id = ctx.user_id; - self.require_room_admin(room_id, user_id).await?; - - room_ai::Entity::delete_by_id((room_id, model_id)) - .exec(&self.db) - .await?; - - if let Ok(room) = self.find_room_or_404(room_id).await { - self.publish_room_event( - room.project, - super::RoomEventType::RoomAiUpdated, - Some(room_id), - None, - None, - None, - ) - .await; - } - - Ok(()) - } - - pub async fn room_ai_stop(&self, room_id: Uuid, ctx: &WsUserContext) -> Result<(), RoomError> { - let user_id = ctx.user_id; - self.require_room_access(room_id, user_id).await?; - tracing::info!(%room_id, %user_id, "AI stream stop requested"); - Ok(()) - } -} diff --git a/libs/room/src/category.rs b/libs/room/src/category.rs deleted file mode 100644 index 4e388a6..0000000 --- a/libs/room/src/category.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::error::RoomError; -use crate::service::RoomService; -use crate::ws_context::WsUserContext; -use chrono::Utc; -use models::rooms::{room, room_category}; -use queue::ProjectRoomEvent; -use sea_orm::prelude::Expr; -use sea_orm::*; -use uuid::Uuid; - -impl RoomService { - pub async fn room_category_list( - &self, - project_name: String, - ctx: &WsUserContext, - ) -> Result, RoomError> { - let user_id = ctx.user_id; - let project = self.utils_find_project_by_name(project_name).await?; - self.check_project_access(project.id, user_id).await?; - - let models = room_category::Entity::find() - .filter(room_category::Column::Project.eq(project.id)) - .order_by_asc(room_category::Column::Position) - .all(&self.db) - .await?; - - Ok(models - .into_iter() - .map(super::RoomCategoryResponse::from) - .collect()) - } - - pub async fn room_category_create( - &self, - project_name: String, - request: super::RoomCategoryCreateRequest, - ctx: &WsUserContext, - ) -> Result { - let user_id = ctx.user_id; - let project = self.utils_find_project_by_name(project_name).await?; - self.require_project_admin(project.id, user_id).await?; - - Self::validate_name(&request.name, super::MAX_CATEGORY_NAME_LEN)?; - - let position = if let Some(position) = request.position { - position - } else { - let max_position: Option> = room_category::Entity::find() - .filter(room_category::Column::Project.eq(project.id)) - .select_only() - .column_as(room_category::Column::Position.max(), "max_position") - .into_tuple::>() - .one(&self.db) - .await?; - max_position.flatten().unwrap_or(0) + 1 - }; - - let model = room_category::ActiveModel { - id: Set(Uuid::now_v7()), - project: Set(project.id), - name: Set(request.name), - position: Set(position), - created_by: Set(user_id), - created_at: Set(Utc::now()), - } - .insert(&self.db) - .await?; - - let event = ProjectRoomEvent { - event_type: super::RoomEventType::CategoryCreated.as_str().into(), - project_id: project.id, - room_id: None, - category_id: Some(model.id), - message_id: None, - seq: None, - timestamp: Utc::now(), - }; - let _ = self - .queue - .publish_project_room_event(project.id, event) - .await; - - Ok(super::RoomCategoryResponse::from(model)) - } - - pub async fn room_category_update( - &self, - category_id: Uuid, - request: super::RoomCategoryUpdateRequest, - ctx: &WsUserContext, - ) -> Result { - let user_id = ctx.user_id; - let model = room_category::Entity::find_by_id(category_id) - .one(&self.db) - .await? - .ok_or_else(|| RoomError::NotFound("Room category not found".to_string()))?; - self.require_project_admin(model.project, user_id).await?; - - let mut active: room_category::ActiveModel = model.into(); - if let Some(name) = request.name { - active.name = Set(name); - } - if let Some(position) = request.position { - active.position = Set(position); - } - let updated = active.update(&self.db).await?; - - let event = ProjectRoomEvent { - event_type: super::RoomEventType::CategoryUpdated.as_str().into(), - project_id: updated.project, - room_id: None, - category_id: Some(updated.id), - message_id: None, - seq: None, - timestamp: Utc::now(), - }; - let _ = self - .queue - .publish_project_room_event(updated.project, event) - .await; - - Ok(super::RoomCategoryResponse::from(updated)) - } - - pub async fn room_category_delete( - &self, - category_id: Uuid, - ctx: &WsUserContext, - ) -> Result<(), RoomError> { - let user_id = ctx.user_id; - let model = room_category::Entity::find_by_id(category_id) - .one(&self.db) - .await? - .ok_or_else(|| RoomError::NotFound("Room category not found".to_string()))?; - self.require_project_admin(model.project, user_id).await?; - let project_id = model.project; - - let txn = self.db.begin().await?; - - room::Entity::update_many() - .col_expr(room::Column::Category, Expr::value(None::)) - .filter(room::Column::Category.eq(category_id)) - .exec(&txn) - .await?; - - room_category::Entity::delete_by_id(category_id) - .exec(&txn) - .await?; - - txn.commit().await?; - - let event = ProjectRoomEvent { - event_type: super::RoomEventType::CategoryDeleted.as_str().into(), - project_id, - room_id: None, - category_id: Some(category_id), - message_id: None, - seq: None, - timestamp: Utc::now(), - }; - let _ = self - .queue - .publish_project_room_event(project_id, event) - .await; - - Ok(()) - } -} diff --git a/libs/room/src/connection/lifecycle.rs b/libs/room/src/connection/lifecycle.rs deleted file mode 100644 index d2be89d..0000000 --- a/libs/room/src/connection/lifecycle.rs +++ /dev/null @@ -1,138 +0,0 @@ -use tokio::sync::broadcast; -use uuid::Uuid; - -use super::{RoomConnectionManager, SHUTDOWN_CHANNEL_CAPACITY}; - -impl RoomConnectionManager { - pub fn subscribe_shutdown(&self) -> broadcast::Receiver<()> { - self.shutdown_tx.subscribe() - } - - pub fn trigger_shutdown(&self) { - let _ = self.shutdown_tx.send(()); - } - - pub async fn register_room(&self, room_id: Uuid) -> broadcast::Receiver<()> { - let mut txs = self.room_shutdown_txs.write().await; - if let Some(tx) = txs.get(&room_id) { - return tx.subscribe(); - } - let (tx, rx) = broadcast::channel(SHUTDOWN_CHANNEL_CAPACITY); - txs.insert(room_id, tx); - rx - } - - pub async fn shutdown_room(&self, room_id: Uuid) { - { - let txs = self.room_shutdown_txs.read().await; - if let Some(tx) = txs.get(&room_id) { - let _ = tx.send(()); - } - } - crate::service::unmark_room_spawned(room_id); - { - let mut counts = self.room_subscriber_count.write().await; - let count = counts.remove(&room_id).unwrap_or(0) as f64; - if count > 0.0 { - self.metrics.users_online.decrement(count); - } - } - { - let mut map = self.room_inner.write().await; - map.remove(&room_id); - } - { - let mut stream_map = self.room_stream_inner.write().await; - stream_map.remove(&room_id); - } - // Remove all streams associated with this room from active_streams and room_to_streams. - { - let mut r2s = self.room_to_streams.write().await; - if let Some(stream_ids) = r2s.remove(&room_id) { - let mut active = self.active_streams.write().await; - for id in stream_ids { - active.remove(&id); - } - } - } - { - let mut txs = self.room_shutdown_txs.write().await; - txs.remove(&room_id); - } - } - - pub async fn prune_stale_rooms(&self, active_room_ids: &[Uuid]) { - let mut txs = self.room_shutdown_txs.write().await; - let stale: Vec = txs - .keys() - .filter(|id| !active_room_ids.contains(id)) - .copied() - .collect(); - for id in &stale { - txs.remove(id); - crate::service::unmark_room_spawned(*id); - } - drop(txs); - let mut counts = self.room_subscriber_count.write().await; - counts.retain(|room_id, _| active_room_ids.contains(room_id)); - } - - pub async fn register_project(&self, project_id: Uuid) -> broadcast::Receiver<()> { - let mut txs = self.project_shutdown_txs.write().await; - if let Some(tx) = txs.get(&project_id) { - return tx.subscribe(); - } - let (tx, rx) = broadcast::channel(SHUTDOWN_CHANNEL_CAPACITY); - txs.insert(project_id, tx); - rx - } - - pub async fn shutdown_project(&self, project_id: Uuid) { - { - let txs = self.project_shutdown_txs.read().await; - if let Some(tx) = txs.get(&project_id) { - let _ = tx.send(()); - } - } - { - let mut map = self.project_inner.write().await; - map.remove(&project_id); - } - { - let mut txs = self.project_shutdown_txs.write().await; - txs.remove(&project_id); - } - } - - pub async fn prune_stale_projects(&self, active_project_ids: &[Uuid]) { - let mut txs = self.project_shutdown_txs.write().await; - txs.retain(|project_id, _| active_project_ids.contains(project_id)); - } - - pub async fn register_user(&self, user_id: Uuid) -> broadcast::Receiver<()> { - let mut txs = self.user_shutdown_txs.write().await; - if let Some(tx) = txs.get(&user_id) { - return tx.subscribe(); - } - let (tx, rx) = broadcast::channel(SHUTDOWN_CHANNEL_CAPACITY); - txs.insert(user_id, tx); - rx - } - - pub async fn shutdown_user(&self, user_id: Uuid) { - { - let txs = self.user_shutdown_txs.read().await; - if let Some(tx) = txs.get(&user_id) { - let _ = tx.send(()); - } - } - { - let mut map = self.user_inner.write().await; - map.remove(&user_id); - } - { - let mut txs = self.user_shutdown_txs.write().await; - txs.remove(&user_id); - } - } -} diff --git a/libs/room/src/connection/mod.rs b/libs/room/src/connection/mod.rs deleted file mode 100644 index cadb1da..0000000 --- a/libs/room/src/connection/mod.rs +++ /dev/null @@ -1,136 +0,0 @@ -mod lifecycle; -mod persist; -mod project_ops; -mod pubsub; -mod rate_limit; -mod room_ops; -mod stream; -mod typing; -mod user_ops; - -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::{RwLock, broadcast}; -use uuid::Uuid; - -use crate::metrics::RoomMetrics; -use crate::types::NotificationEvent; -use db::cache::AppCache; -use queue::types::TypingEvent; -use queue::{AgentTaskEvent, ProjectRoomEvent, RoomMessageEvent, RoomMessageStreamChunkEvent}; - -use std::time::Duration; - -pub const BROADCAST_CAPACITY: usize = 1000; -pub const SHUTDOWN_CHANNEL_CAPACITY: usize = 16; -pub const CONNECTION_COOLDOWN: Duration = Duration::from_secs(30); -pub const MAX_CONNECTIONS_PER_ROOM: usize = 50000; -pub const MAX_CONNECTIONS_PER_PROJECT: usize = 50000; -pub const MAX_CONNECTIONS_PER_USER: usize = 50000; -/// Maximum rooms a single WS session can subscribe to. -pub const MAX_ROOMS_PER_SESSION: usize = 100; -pub const BATCH_SIZE: usize = 100; -pub const ROOM_IDLE_TIMEOUT: Duration = Duration::from_secs(30 * 60); -pub const REPLAY_BUFFER_SIZE: usize = 100; - -/// Metadata for an active AI stream — used to replay chunks to late-joining subscribers. -#[derive(Clone)] -pub struct ActiveStreamMeta { - pub message_id: Uuid, - pub room_id: Uuid, - pub display_name: Option, - /// Ring buffer of recent chunks. New chunks are appended; old ones are evicted - /// when the buffer exceeds REPLAY_BUFFER_SIZE. - pub chunks: Arc>>, -} - -pub struct RoomConnectionManager { - room_inner: RwLock>>>, - project_inner: RwLock>>>, - user_inner: RwLock>>>, - user_notification_inner: RwLock>>>, - task_inner: RwLock>>>, - pub metrics: Arc, - cache: AppCache, - connection_rate: RwLock>, - shutdown_tx: broadcast::Sender<()>, - room_shutdown_txs: RwLock>>, - project_shutdown_txs: RwLock>>, - user_shutdown_txs: RwLock>>, - stream_inner: RwLock>>>, - room_stream_inner: - RwLock>, usize)>>, - /// Active AI streams keyed by message_id. Used to replay buffered chunks to - /// late-joining subscribers who missed the stream start. - active_streams: RwLock>, - /// Reverse index: room_id -> set of active message_ids. Used by - /// `subscribe_room_stream` to replay buffered chunks to late joiners. - room_to_streams: RwLock>>, - typing_inner: RwLock>>>, - room_last_activity: RwLock>, - room_subscriber_count: RwLock>, - project_subscriber_count: RwLock>, - user_subscriber_count: RwLock>, - stream_cancel_tokens: RwLock>>, -} - -impl RoomConnectionManager { - pub fn new(metrics: Arc, cache: AppCache) -> Self { - let (shutdown_tx, _) = broadcast::channel(SHUTDOWN_CHANNEL_CAPACITY); - Self { - room_inner: RwLock::new(HashMap::new()), - project_inner: RwLock::new(HashMap::new()), - user_inner: RwLock::new(HashMap::new()), - user_notification_inner: RwLock::new(HashMap::new()), - task_inner: RwLock::new(HashMap::new()), - metrics, - cache, - connection_rate: RwLock::new(HashMap::new()), - shutdown_tx, - room_shutdown_txs: RwLock::new(HashMap::new()), - project_shutdown_txs: RwLock::new(HashMap::new()), - user_shutdown_txs: RwLock::new(HashMap::new()), - stream_inner: RwLock::new(HashMap::new()), - room_stream_inner: RwLock::new(HashMap::new()), - active_streams: RwLock::new(HashMap::new()), - room_to_streams: RwLock::new(HashMap::new()), - typing_inner: RwLock::new(HashMap::new()), - room_last_activity: RwLock::new(HashMap::new()), - room_subscriber_count: RwLock::new(HashMap::new()), - project_subscriber_count: RwLock::new(HashMap::new()), - user_subscriber_count: RwLock::new(HashMap::new()), - stream_cancel_tokens: RwLock::new(HashMap::new()), - } - } -} - -pub use persist::{DedupCache, PersistFn, cleanup_dedup_cache, make_persist_fn}; -pub use pubsub::{ - subscribe_project_room_events, subscribe_room_events, subscribe_room_stream_chunk_events, - subscribe_task_events_fn, -}; - -/// Extract a Redis connection getter from MessageProducer. -/// Used for cache operations (notification unread count, etc.), NOT for message broadcasting. -pub type RedisFuture = std::pin::Pin< - Box< - dyn std::future::Future> - + Send, - >, ->; - -pub fn extract_get_redis( - queue: queue::MessageProducer, -) -> Arc RedisFuture + Send + Sync> { - let get_redis = queue.get_redis.clone(); - Arc::new(move || { - let get_redis = get_redis.clone(); - Box::pin(async move { - let handle = get_redis(); - match handle.await { - Ok(conn) => conn, - Err(_) => anyhow::bail!("redis pool task panicked"), - } - }) as RedisFuture - }) -} diff --git a/libs/room/src/connection/persist.rs b/libs/room/src/connection/persist.rs deleted file mode 100644 index dbd9c4d..0000000 --- a/libs/room/src/connection/persist.rs +++ /dev/null @@ -1,253 +0,0 @@ -use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::time::Instant; -use uuid::Uuid; - -use dashmap::DashMap; -use db::database::AppDatabase; -use models::rooms::{MessageContentType, MessageSenderType, room_message}; -use queue::RoomMessageEnvelope; -use sea_orm::{ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, Set}; - -use crate::metrics::RoomMetrics; - -pub const BATCH_SIZE: usize = 100; -pub const DEDUP_CACHE_TTL: Duration = Duration::from_secs(300); - -use std::time::Duration; - -pub type PersistFn = Arc< - dyn Fn(Vec) -> Pin> + Send>> - + Send - + Sync, ->; - -pub type DedupCache = Arc>; - -pub fn cleanup_dedup_cache(cache: &DedupCache) { - let cutoff = Instant::now() - DEDUP_CACHE_TTL; - cache.retain(|_, inserted_at| *inserted_at > cutoff); -} - -fn parse_sender_type(s: &str) -> MessageSenderType { - match s { - "user" => MessageSenderType::User, - "ai" => MessageSenderType::Ai, - "system" => MessageSenderType::System, - "tool" => MessageSenderType::Tool, - "webhook" => MessageSenderType::Webhook, - _ => MessageSenderType::User, - } -} - -fn parse_content_type(s: &str) -> MessageContentType { - match s { - "text" => MessageContentType::Text, - "image" => MessageContentType::Image, - "audio" => MessageContentType::Audio, - "video" => MessageContentType::Video, - "file" => MessageContentType::File, - _ => MessageContentType::Text, - } -} - -pub fn make_persist_fn( - db: AppDatabase, - metrics: Arc, - dedup_cache: DedupCache, - embed_service: Option>, -) -> PersistFn { - Arc::new(move |envelopes: Vec| { - let db = db.clone(); - let metrics = metrics.clone(); - let cache = dedup_cache.clone(); - let embed = embed_service.clone(); - Box::pin(async move { - let mut persisted: Vec = Vec::new(); - - for chunk in envelopes.chunks(BATCH_SIZE) { - let mut models_to_insert = Vec::new(); - let mut ids_to_dedup: Vec = Vec::new(); - - for env in chunk { - if cache.contains_key(&env.id) { - metrics.incr_duplicates_skipped(); - continue; - } - ids_to_dedup.push(env.id); - } - - let existing_ids: std::collections::HashSet = - if !ids_to_dedup.is_empty() { - room_message::Entity::find() - .filter(room_message::Column::Id.is_in(ids_to_dedup.clone())) - .into_model::() - .all(&db) - .await? - .into_iter() - .map(|m| m.id) - .collect() - } else { - std::collections::HashSet::new() - }; - - for env in chunk { - if cache.contains_key(&env.id) { - continue; - } - cache.insert(env.id, Instant::now()); - - if existing_ids.contains(&env.id) { - metrics.incr_duplicates_skipped(); - continue; - } - - let sender_type = parse_sender_type(&env.sender_type); - let content_type = parse_content_type(&env.content_type); - - models_to_insert.push(room_message::ActiveModel { - id: Set(env.id), - seq: Set(env.seq), - room: Set(env.room_id), - sender_type: Set(sender_type), - sender_id: Set(env.sender_id), - model_id: Set(env.model_id), - thread: Set(env.thread_id), - content: Set(env.content.clone()), - content_type: Set(content_type), - thinking_content: Set(env.thinking_content.clone()), - edited_at: Set(None), - send_at: Set(env.send_at.clone()), - revoked: Set(None), - revoked_by: Set(None), - in_reply_to: Set(env.in_reply_to), - }); - } - - if !models_to_insert.is_empty() { - let count = models_to_insert.len() as u64; - if let Err(e) = room_message::Entity::insert_many(models_to_insert) - .exec(&db) - .await - { - metrics.messages_persist_failed.increment(count); - return Err(e.into()); - } - - metrics.messages_persisted.increment(count); - - if !ids_to_dedup.is_empty() { - let stmt = sea_orm::Statement::from_sql_and_values( - sea_orm::DbBackend::Postgres, - "UPDATE room_message AS t \ - SET content_tsv = to_tsvector('simple', content) \ - WHERE t.id = ANY($1)", - vec![ids_to_dedup.into()], - ); - if let Err(e) = db.execute_raw(stmt).await { - metrics.messages_persist_failed.increment(count); - tracing::warn!(error = %e, "full text index update failed"); - } - } - - for env in chunk { - if existing_ids.contains(&env.id) { - continue; - } - persisted.push(env.clone()); - } - } - } - - if let Some(embed) = embed { - if !persisted.is_empty() { - let embed_db = db.clone(); - tokio::spawn(async move { - embed_persisted_messages(embed, embed_db, persisted).await; - }); - } - } - - Ok(()) - }) - }) -} - -async fn embed_persisted_messages( - embed: Arc, - db: AppDatabase, - messages: Vec, -) { - let to_embed: Vec<&RoomMessageEnvelope> = messages - .iter() - .filter(|m| { - m.content_type == "text" - && !m.content.trim().is_empty() - && m.sender_type != "system" - && m.sender_type != "tool" - }) - .collect(); - - if to_embed.is_empty() { - return; - } - - let room_ids: Vec = to_embed.iter().map(|m| m.room_id).collect(); - let rooms = match models::rooms::room::Entity::find() - .filter(models::rooms::room::Column::Id.is_in(room_ids.clone())) - .all(&db) - .await - { - Ok(r) => r, - Err(e) => { - tracing::warn!(error = %e, "embed: failed to lookup rooms"); - return; - } - }; - - let project_ids: Vec = rooms.iter().map(|r| r.project).collect(); - let projects = match models::projects::project::Entity::find() - .filter(models::projects::project::Column::Id.is_in(project_ids)) - .all(&db) - .await - { - Ok(p) => p, - Err(e) => { - tracing::warn!(error = %e, "embed: failed to lookup projects"); - return; - } - }; - - let mut room_project: HashMap = HashMap::new(); - for room in &rooms { - if let Some(proj) = projects.iter().find(|p| p.id == room.project) { - room_project.insert(room.id, proj.display_name.clone()); - } - } - - let inputs: Vec = to_embed - .into_iter() - .filter_map(|m| { - let project_name = room_project.get(&m.room_id)?; - Some(agent::embed::EmbedMemoryInput { - message_id: m.id.to_string(), - seq: m.seq, - content: m.content.clone(), - project_name: project_name.clone(), - room_id: m.room_id.to_string(), - user_id: m.sender_id.map(|id| id.to_string()), - sender_type: m.sender_type.clone(), - }) - }) - .collect(); - - if inputs.is_empty() { - return; - } - - if let Err(e) = embed.embed_memories_batch(inputs).await { - tracing::warn!(error = %e, "batch memory embed failed"); - } -} diff --git a/libs/room/src/connection/project_ops.rs b/libs/room/src/connection/project_ops.rs deleted file mode 100644 index ff8455f..0000000 --- a/libs/room/src/connection/project_ops.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::sync::Arc; -use tokio::sync::broadcast; -use uuid::Uuid; - -use super::{ - AgentTaskEvent, BROADCAST_CAPACITY, MAX_CONNECTIONS_PER_PROJECT, ProjectRoomEvent, - RoomConnectionManager, -}; -use crate::error::RoomError; - -impl RoomConnectionManager { - pub async fn subscribe_project( - &self, - project_id: Uuid, - _user_id: Uuid, - ) -> Result>, RoomError> { - let mut map = self.project_inner.write().await; - if map.get(&project_id).is_some() { - drop(map); - let mut counts = self.project_subscriber_count.write().await; - *counts.entry(project_id).or_insert(0) += 1; - let map = self.project_inner.read().await; - if let Some(sender) = map.get(&project_id) { - return Ok(sender.subscribe()); - } - return Err(RoomError::Internal("project channel disappeared".into())); - } - - if map.len() >= MAX_CONNECTIONS_PER_PROJECT { - return Err(RoomError::RateLimited(format!( - "Project connection limit reached ({})", - MAX_CONNECTIONS_PER_PROJECT - ))); - } - - let (tx, rx) = broadcast::channel(BROADCAST_CAPACITY); - map.insert(project_id, tx); - drop(map); - let mut counts = self.project_subscriber_count.write().await; - counts.insert(project_id, 1); - Ok(rx) - } - - pub async fn unsubscribe_project(&self, project_id: Uuid, _user_id: Uuid) { - let mut counts = self.project_subscriber_count.write().await; - let count = counts.entry(project_id).or_insert(0); - if *count > 0 { - *count -= 1; - } - if *count == 0 { - counts.remove(&project_id); - drop(counts); - let mut map = self.project_inner.write().await; - map.remove(&project_id); - } - } - - pub async fn broadcast_project(&self, project_id: Uuid, event: ProjectRoomEvent) { - let map = self.project_inner.read().await; - if let Some(sender) = map.get(&project_id) { - let event = Arc::new(event); - if sender.send(event).is_err() { - self.metrics.broadcasts_dropped.increment(1); - } - } - } - - pub async fn broadcast_agent_task(&self, project_id: Uuid, event: AgentTaskEvent) { - let map = self.task_inner.read().await; - if let Some(sender) = map.get(&project_id) { - let event = Arc::new(event); - if sender.send(event).is_err() { - self.metrics.broadcasts_dropped.increment(1); - } - } - } - - pub async fn subscribe_task_events( - &self, - project_id: Uuid, - ) -> Result>, RoomError> { - let mut map = self.task_inner.write().await; - - if let Some(sender) = map.get(&project_id).cloned() { - drop(map); - return Ok(sender.subscribe()); - } - - let (tx, rx) = broadcast::channel(BROADCAST_CAPACITY); - map.insert(project_id, tx); - Ok(rx) - } -} diff --git a/libs/room/src/connection/pubsub.rs b/libs/room/src/connection/pubsub.rs deleted file mode 100644 index aed610a..0000000 --- a/libs/room/src/connection/pubsub.rs +++ /dev/null @@ -1,310 +0,0 @@ -//! NATS subscriptions — JetStream durable pull consumers for reliable broadcast. -//! -//! All subscriptions run as async tokio tasks on the shared NATS connection. - -use std::sync::Arc; - -use queue::{AgentTaskEvent, ProjectRoomEvent, RoomMessageEvent, RoomMessageStreamChunkEvent}; -use tokio::sync::broadcast; -use uuid::Uuid; - -use super::RoomConnectionManager; - -/// Subscribe to room message events via JetStream and broadcast to local WS clients. -pub async fn subscribe_room_events( - nats: Arc, - manager: Arc, - room_id: Uuid, - mut shutdown_rx: broadcast::Receiver<()>, -) { - let stream_name = nats.stream_name().to_string(); - let filter_subject = format!("room.message.{}", room_id); - // Generate a unique instance-specific suffix to prevent competition in multi-node setups. - let instance_id = uuid::Uuid::new_v4().to_string(); - let instance_id_short = &instance_id[..8]; - let durable = format!("broadcast-room-{}-{}", room_id, instance_id_short); - - tracing::info!(room_id = %room_id, durable = %durable, "JetStream room events subscriber starting"); - - loop { - tokio::select! { - _ = shutdown_rx.recv() => { - tracing::info!(room_id = %room_id, "room events subscriber shutting down"); - break; - } - result = consume_room_broadcast(&nats, &stream_name, &durable, &filter_subject, &manager, room_id) => { - match result { - Ok(0) => {} - Ok(n) => tracing::debug!(room_id = %room_id, n = n, "room messages broadcast"), - Err(e) => { - tracing::error!(room_id = %room_id, error = %e, "JetStream consume error, retrying"); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } - } - } - } - } - tracing::info!(room_id = %room_id, "room events subscriber stopped"); -} - -async fn consume_room_broadcast( - nats: &queue::NatsClient, - stream_name: &str, - durable: &str, - filter_subject: &str, - manager: &Arc, - room_id: Uuid, -) -> anyhow::Result { - use futures::StreamExt; - - let stream = nats - .jetstream - .get_stream(stream_name) - .await - .map_err(|e| anyhow::anyhow!("get stream failed: {}", e))?; - - let pull_config = async_nats::jetstream::consumer::pull::Config { - durable_name: Some(durable.to_string()), - filter_subject: filter_subject.to_string(), - max_deliver: 3, - ack_wait: std::time::Duration::from_secs(10), - max_ack_pending: 256, - // Ensure temporary consumers are cleaned up by the server after inactivity. - inactive_threshold: std::time::Duration::from_secs(3600), - ..Default::default() - }; - - let consumer = stream - .get_or_create_consumer(durable, pull_config) - .await - .map_err(|e| anyhow::anyhow!("create consumer failed: {}", e))?; - - let mut messages = consumer - .messages() - .await - .map_err(|e| anyhow::anyhow!("consumer messages failed: {}", e))?; - - let mut count = 0usize; - - while count < 100 { - match tokio::time::timeout(std::time::Duration::from_millis(200), messages.next()).await { - Ok(Some(Ok(msg))) => { - match serde_json::from_slice::(&msg.payload) { - Ok(event) => { - manager.broadcast(room_id, event).await; - count += 1; - } - Err(e) => tracing::warn!(error = %e, "malformed RoomMessageEvent"), - } - let _ = msg.ack().await; - } - Ok(Some(Err(e))) => { - tracing::warn!(error = %e, "message error"); - } - Ok(None) => break, - Err(_) => break, - } - } - - Ok(count) -} - -/// Subscribe to room stream chunk events via JetStream and broadcast to local WS clients. -pub async fn subscribe_room_stream_chunk_events( - nats: Arc, - manager: Arc, - room_id: Uuid, - mut shutdown_rx: broadcast::Receiver<()>, -) { - let stream_name = nats.stream_name().to_string(); - let filter_subject = format!("room.chunk.{}", room_id); - // Generate a unique instance-specific suffix to prevent competition in multi-node setups. - let instance_id = uuid::Uuid::new_v4().to_string(); - let instance_id_short = &instance_id[..8]; - let durable = format!("chunk-room-{}-{}", room_id, instance_id_short); - - tracing::info!(room_id = %room_id, durable = %durable, "JetStream chunk events subscriber starting"); - - loop { - tokio::select! { - _ = shutdown_rx.recv() => { - tracing::info!(room_id = %room_id, "chunk events subscriber shutting down"); - break; - } - result = consume_chunk_broadcast(&nats, &stream_name, &durable, &filter_subject, &manager, room_id) => { - match result { - Ok(0) => {} - Ok(n) => tracing::debug!(room_id = %room_id, n = n, "chunks broadcast"), - Err(e) => { - tracing::error!(room_id = %room_id, error = %e, "JetStream chunk consume error, retrying"); - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } - } - } - } - } - tracing::info!(room_id = %room_id, "chunk events subscriber stopped"); -} - -#[allow(unused_variables)] -async fn consume_chunk_broadcast( - nats: &queue::NatsClient, - stream_name: &str, - durable: &str, - filter_subject: &str, - manager: &Arc, - room_id: Uuid, -) -> anyhow::Result { - use futures::StreamExt; - - let stream = nats - .jetstream - .get_stream(stream_name) - .await - .map_err(|e| anyhow::anyhow!("get stream failed: {}", e))?; - - let pull_config = async_nats::jetstream::consumer::pull::Config { - durable_name: Some(durable.to_string()), - filter_subject: filter_subject.to_string(), - max_deliver: 3, - ack_wait: std::time::Duration::from_secs(10), - max_ack_pending: 256, - // Ensure temporary consumers are cleaned up by the server after inactivity. - inactive_threshold: std::time::Duration::from_secs(3600), - ..Default::default() - }; - - let consumer = stream - .get_or_create_consumer(durable, pull_config) - .await - .map_err(|e| anyhow::anyhow!("create consumer failed: {}", e))?; - - let mut messages = consumer - .messages() - .await - .map_err(|e| anyhow::anyhow!("consumer messages failed: {}", e))?; - - let mut count = 0usize; - - while count < 100 { - match tokio::time::timeout(std::time::Duration::from_millis(200), messages.next()).await { - Ok(Some(Ok(msg))) => { - match serde_json::from_slice::(&msg.payload) { - Ok(event) => { - manager.broadcast_stream_chunk(event).await; - count += 1; - } - Err(e) => tracing::warn!(error = %e, "malformed RoomMessageStreamChunkEvent"), - } - let _ = msg.ack().await; - } - Ok(Some(Err(e))) => { - tracing::warn!(error = %e, "chunk message error"); - } - Ok(None) => break, - Err(_) => break, - } - } - - Ok(count) -} - -/// Subscribe to project-level room events and broadcast to local WS clients. -pub async fn subscribe_project_room_events( - nats: Arc, - manager: Arc, - project_id: Uuid, - mut shutdown_rx: broadcast::Receiver<()>, -) { - let subject = format!("project.event.{}", project_id); - - let subscriber = match nats.subscribe(&subject).await { - Ok(s) => s, - Err(e) => { - tracing::error!(project_id = %project_id, error = %e, "NATS subscribe failed"); - return; - } - }; - - tracing::info!(project_id = %project_id, subject = %subject, "NATS project events subscriber started"); - - use futures::StreamExt; - let mut stream = subscriber; - - loop { - tokio::select! { - _ = shutdown_rx.recv() => { - tracing::info!(project_id = %project_id, "project events subscriber shutting down"); - break; - } - msg = stream.next() => { - let Some(payload) = msg else { - tracing::warn!(project_id = %project_id, "NATS subscription ended, reconnecting"); - match nats.subscribe(&subject).await { - Ok(new_sub) => { stream = new_sub; } - Err(e) => { - tracing::error!(project_id = %project_id, error = %e, "reconnect failed"); - break; - } - } - continue; - }; - match serde_json::from_slice::(&payload.payload) { - Ok(event) => manager.broadcast_project(project_id, event).await, - Err(e) => tracing::warn!(error = %e, "malformed ProjectRoomEvent"), - } - } - } - } - tracing::info!(project_id = %project_id, "project events subscriber stopped"); -} - -/// Subscribe to agent task events and broadcast to local WS clients. -pub async fn subscribe_task_events_fn( - nats: Arc, - manager: Arc, - project_id: Uuid, - mut shutdown_rx: broadcast::Receiver<()>, -) { - let subject = format!("task.event.{}", project_id); - - let subscriber = match nats.subscribe(&subject).await { - Ok(s) => s, - Err(e) => { - tracing::error!(project_id = %project_id, error = %e, "NATS subscribe failed"); - return; - } - }; - - tracing::info!(project_id = %project_id, subject = %subject, "NATS task events subscriber started"); - - use futures::StreamExt; - let mut stream = subscriber; - - loop { - tokio::select! { - _ = shutdown_rx.recv() => { - tracing::info!(project_id = %project_id, "task events subscriber shutting down"); - break; - } - msg = stream.next() => { - let Some(payload) = msg else { - tracing::warn!(project_id = %project_id, "NATS subscription ended, reconnecting"); - match nats.subscribe(&subject).await { - Ok(new_sub) => { stream = new_sub; } - Err(e) => { - tracing::error!(project_id = %project_id, error = %e, "reconnect failed"); - break; - } - } - continue; - }; - match serde_json::from_slice::(&payload.payload) { - Ok(event) => manager.broadcast_agent_task(project_id, event).await, - Err(e) => tracing::warn!(error = %e, "malformed AgentTaskEvent"), - } - } - } - } - tracing::info!(project_id = %project_id, "task events subscriber stopped"); -} diff --git a/libs/room/src/connection/rate_limit.rs b/libs/room/src/connection/rate_limit.rs deleted file mode 100644 index 9b22fa5..0000000 --- a/libs/room/src/connection/rate_limit.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::time::Instant; -use uuid::Uuid; - -use super::{ - CONNECTION_COOLDOWN, MAX_CONNECTIONS_PER_ROOM, ROOM_IDLE_TIMEOUT, RoomConnectionManager, -}; -use crate::error::RoomError; - -impl RoomConnectionManager { - pub async fn check_room_connection_rate( - &self, - room_id: Uuid, - user_id: Uuid, - ) -> Result<(), RoomError> { - let mut map = self.connection_rate.write().await; - let key = (room_id, user_id); - if let Some(last) = map.get(&key) { - if last.elapsed() < CONNECTION_COOLDOWN { - self.metrics.ws_rate_limit_hits.increment(1); - return Err(RoomError::RateLimited(format!( - "Connection cooldown active, retry in {}s", - CONNECTION_COOLDOWN.saturating_sub(last.elapsed()).as_secs() - ))); - } - } - map.insert(key, Instant::now()); - Ok(()) - } - - pub async fn check_project_connection_rate( - &self, - project_id: Uuid, - user_id: Uuid, - ) -> Result<(), RoomError> { - let mut map = self.connection_rate.write().await; - let key = (project_id, user_id); - if let Some(last) = map.get(&key) { - if last.elapsed() < CONNECTION_COOLDOWN { - self.metrics.ws_rate_limit_hits.increment(1); - return Err(RoomError::RateLimited(format!( - "Connection cooldown active, retry in {}s", - CONNECTION_COOLDOWN.saturating_sub(last.elapsed()).as_secs() - ))); - } - } - map.insert(key, Instant::now()); - Ok(()) - } - - pub async fn check_user_connection_rate(&self, user_id: Uuid) -> Result<(), RoomError> { - let mut map = self.connection_rate.write().await; - let key = (Uuid::nil(), user_id); - if let Some(last) = map.get(&key) { - if last.elapsed() < CONNECTION_COOLDOWN { - self.metrics.ws_rate_limit_hits.increment(1); - return Err(RoomError::RateLimited(format!( - "Connection cooldown active, retry in {}s", - CONNECTION_COOLDOWN.saturating_sub(last.elapsed()).as_secs() - ))); - } - } - map.insert(key, Instant::now()); - Ok(()) - } - - pub async fn cleanup_rate_limit(&self) { - let mut map = self.connection_rate.write().await; - map.retain(|_, instant| instant.elapsed() < CONNECTION_COOLDOWN * 2); - const MAX_RATE_ENTRIES: usize = MAX_CONNECTIONS_PER_ROOM * 10; - if map.len() > MAX_RATE_ENTRIES { - let mut entries: Vec<_> = map.iter().collect(); - entries.sort_by(|a, b| a.1.cmp(b.1)); - let keep_count = entries.len() / 2; - let to_remove: Vec<_> = entries - .into_iter() - .take(keep_count) - .map(|(k, _)| *k) - .collect(); - for key in to_remove { - map.remove(&key); - } - } - drop(map); - self.cleanup_idle_rooms().await; - } - - pub async fn cleanup_idle_rooms(&self) { - let now = Instant::now(); - let activity = self.room_last_activity.read().await; - let idle_room_ids: Vec = activity - .iter() - .filter(|(_, last_time)| now.duration_since(**last_time) > ROOM_IDLE_TIMEOUT) - .map(|(room_id, _)| *room_id) - .collect(); - drop(activity); - - if idle_room_ids.is_empty() { - return; - } - - { - let mut counts = self.room_subscriber_count.write().await; - let mut rooms = self.room_inner.write().await; - for room_id in &idle_room_ids { - if let Some(sender) = rooms.remove(room_id) { - let count = counts.remove(&room_id).unwrap_or(1); - self.metrics.users_online.decrement(count as f64); - drop(sender); - } - } - } - - { - let mut stream_map = self.room_stream_inner.write().await; - for room_id in &idle_room_ids { - stream_map.remove(room_id); - } - } - - { - let mut txs = self.room_shutdown_txs.write().await; - for room_id in &idle_room_ids { - txs.remove(room_id); - } - } - - { - let mut activity = self.room_last_activity.write().await; - for room_id in &idle_room_ids { - activity.remove(room_id); - } - } - } -} diff --git a/libs/room/src/connection/room_ops.rs b/libs/room/src/connection/room_ops.rs deleted file mode 100644 index 8887221..0000000 --- a/libs/room/src/connection/room_ops.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::sync::Arc; -use tokio::sync::broadcast; -use uuid::Uuid; - -use super::{ - BROADCAST_CAPACITY, MAX_CONNECTIONS_PER_ROOM, RoomConnectionManager, RoomMessageEvent, -}; -use crate::error::RoomError; - -impl RoomConnectionManager { - pub async fn subscribe( - &self, - room_id: Uuid, - _user_id: Uuid, - ) -> Result>, RoomError> { - let mut map = self.room_inner.write().await; - if let Some(_sender) = map.get(&room_id) { - drop(map); - let mut counts = self.room_subscriber_count.write().await; - *counts.entry(room_id).or_insert(0) += 1; - let map = self.room_inner.read().await; - if let Some(sender) = map.get(&room_id) { - return Ok(sender.subscribe()); - } - return Err(RoomError::Internal( - "room disappeared during subscribe".into(), - )); - } - - if map.len() >= MAX_CONNECTIONS_PER_ROOM { - return Err(RoomError::RateLimited(format!( - "Room connection limit reached ({})", - MAX_CONNECTIONS_PER_ROOM - ))); - } - - let (tx, rx) = broadcast::channel(BROADCAST_CAPACITY); - map.insert(room_id, tx); - drop(map); - let mut counts = self.room_subscriber_count.write().await; - counts.insert(room_id, 1); - self.metrics.users_online.increment(1.0); - Ok(rx) - } - - pub async fn unsubscribe(&self, room_id: Uuid, _user_id: Uuid) { - let mut counts = self.room_subscriber_count.write().await; - let count = counts.entry(room_id).or_insert(0); - if *count > 0 { - *count -= 1; - self.metrics.users_online.decrement(1.0); - } - if *count == 0 { - counts.remove(&room_id); - drop(counts); - let mut map = self.room_inner.write().await; - map.remove(&room_id); - } - } - - pub async fn broadcast(&self, room_id: Uuid, event: RoomMessageEvent) { - { - let mut activity = self.room_last_activity.write().await; - activity.insert(room_id, std::time::Instant::now()); - } - - let map = self.room_inner.read().await; - if let Some(sender) = map.get(&room_id) { - let event = Arc::new(event); - if sender.send(event).is_err() { - self.metrics.broadcasts_dropped.increment(1); - } else { - self.metrics.broadcasts_sent.increment(1); - } - } - } -} diff --git a/libs/room/src/connection/stream.rs b/libs/room/src/connection/stream.rs deleted file mode 100644 index 90139fb..0000000 --- a/libs/room/src/connection/stream.rs +++ /dev/null @@ -1,228 +0,0 @@ -use std::sync::Arc; -use tokio::sync::{RwLock, broadcast}; -use uuid::Uuid; - -use super::{ - BROADCAST_CAPACITY, REPLAY_BUFFER_SIZE, RoomConnectionManager, RoomMessageStreamChunkEvent, -}; - -impl RoomConnectionManager { - pub async fn register_stream_channel( - &self, - message_id: Uuid, - room_id: Uuid, - display_name: Option, - ) -> broadcast::Receiver> { - let mut map = self.stream_inner.write().await; - if let Some(tx) = map.get(&message_id) { - return tx.subscribe(); - } - let (tx, rx) = broadcast::channel(BROADCAST_CAPACITY); - map.insert(message_id, tx.clone()); - - // Also register in active_streams for late-joiner catchup - let meta = super::ActiveStreamMeta { - message_id, - room_id, - display_name: display_name.clone(), - chunks: Arc::new(RwLock::new(Vec::new())), - }; - drop(map); - let mut active = self.active_streams.write().await; - active.insert(message_id, meta); - - rx - } - - pub async fn subscribe_stream( - &self, - message_id: Uuid, - ) -> Option>> { - let map = self.stream_inner.read().await; - map.get(&message_id).map(|tx| tx.subscribe()) - } - - pub async fn subscribe_room_stream( - &self, - room_id: Uuid, - ) -> broadcast::Receiver> { - // New subscriber: replay active streams in this room so they catch up, - // then subscribe to the room's channel. - - let (_existing_tx, new_rx) = { - let mut map = self.room_stream_inner.write().await; - match map.get_mut(&room_id) { - Some((existing_tx, count)) => { - *count += 1; - let tx_clone = existing_tx.clone(); - let rx_clone = existing_tx.subscribe(); - drop(map); - // Replay buffered chunks to existing channel so all subscribers receive them. - let active = self.active_streams.read().await; - for (&msg_id, meta) in active.iter() { - if meta.room_id != room_id { - continue; - } - let start_event = Arc::new(RoomMessageStreamChunkEvent { - message_id: msg_id, - room_id, - seq: 0, - content: String::new(), - done: false, - error: None, - display_name: meta.display_name.clone(), - chunk_type: None, - }); - let _ = tx_clone.send(Arc::clone(&start_event)); - let chunks = meta.chunks.read().await; - for chunk in chunks.iter() { - let _ = tx_clone.send(Arc::new(chunk.clone())); - } - } - (tx_clone, rx_clone) - } - None => { - let (tx, rx) = broadcast::channel(BROADCAST_CAPACITY); - map.insert(room_id, (tx.clone(), 1)); - drop(map); - // Replay buffered chunks to new channel. - let active = self.active_streams.read().await; - for (&msg_id, meta) in active.iter() { - if meta.room_id != room_id { - continue; - } - let start_event = Arc::new(RoomMessageStreamChunkEvent { - message_id: msg_id, - room_id, - seq: 0, - content: String::new(), - done: false, - error: None, - display_name: meta.display_name.clone(), - chunk_type: None, - }); - let _ = tx.send(Arc::clone(&start_event)); - let chunks = meta.chunks.read().await; - for chunk in chunks.iter() { - let _ = tx.send(Arc::new(chunk.clone())); - } - } - (tx, rx) - } - } - }; - new_rx - } - - pub async fn broadcast_stream_chunk(&self, event: RoomMessageStreamChunkEvent) { - { - let mut activity = self.room_last_activity.write().await; - activity.insert(event.room_id, std::time::Instant::now()); - } - - let is_start = event.seq == 0 && !event.done; - let is_final_chunk = event.done; - - // Buffer chunk in active_streams for late-joiner replay. - if !is_final_chunk || is_start { - let mut active = self.active_streams.write().await; - if let Some(meta) = active.get_mut(&event.message_id) { - let mut chunks = meta.chunks.write().await; - chunks.push(event.clone()); - // Evict oldest if buffer exceeds REPLAY_BUFFER_SIZE. - if chunks.len() > REPLAY_BUFFER_SIZE { - chunks.remove(0); - } - } - drop(active); - // Also update room_to_streams reverse index. - if is_start { - let mut r2s = self.room_to_streams.write().await; - r2s.entry(event.room_id) - .or_default() - .insert(event.message_id); - } - } - - let event = Arc::new(event); - - let map = self.stream_inner.read().await; - if let Some(tx) = map.get(&event.message_id) { - let _ = tx.send(Arc::clone(&event)); - } - - drop(map); - let map = self.room_stream_inner.read().await; - if let Some((tx, _)) = map.get(&event.room_id) { - let _ = tx.send(Arc::clone(&event)); - } - - if is_final_chunk { - drop(map); - // Cleanup active_streams entry. - let mut active = self.active_streams.write().await; - if active.remove(&event.message_id).is_some() { - let mut r2s = self.room_to_streams.write().await; - if let Some(ids) = r2s.get_mut(&event.room_id) { - ids.remove(&event.message_id); - if ids.is_empty() { - r2s.remove(&event.room_id); - } - } - } - drop(active); - // Cleanup room_stream_inner subscriber count. - let mut map = self.room_stream_inner.write().await; - if let Some((_, count)) = map.get_mut(&event.room_id) { - if *count > 0 { - *count -= 1; - } - if *count == 0 { - map.remove(&event.room_id); - } - } - } - } - - pub async fn close_stream_channel(&self, message_id: Uuid) { - let mut map = self.stream_inner.write().await; - map.remove(&message_id); - drop(map); - // Remove from active_streams (cleanup on stream end). - let mut active = self.active_streams.write().await; - if let Some(meta) = active.remove(&message_id) { - let mut r2s = self.room_to_streams.write().await; - if let Some(ids) = r2s.get_mut(&meta.room_id) { - ids.remove(&message_id); - if ids.is_empty() { - r2s.remove(&meta.room_id); - } - } - } - } - - pub async fn register_stream_cancel( - &self, - room_id: Uuid, - ) -> Arc { - let cancel = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let mut map = self.stream_cancel_tokens.write().await; - map.insert(room_id, cancel.clone()); - cancel - } - - pub async fn cancel_ai_stream(&self, room_id: Uuid) -> bool { - let map = self.stream_cancel_tokens.read().await; - if let Some(cancel) = map.get(&room_id) { - cancel.store(true, std::sync::atomic::Ordering::Release); - true - } else { - false - } - } - - pub async fn unregister_stream_cancel(&self, room_id: Uuid) { - let mut map = self.stream_cancel_tokens.write().await; - map.remove(&room_id); - } -} diff --git a/libs/room/src/connection/typing.rs b/libs/room/src/connection/typing.rs deleted file mode 100644 index 6d2c22e..0000000 --- a/libs/room/src/connection/typing.rs +++ /dev/null @@ -1,130 +0,0 @@ -use queue::types::TypingEvent; -use std::sync::Arc; -use tokio::sync::{RwLockReadGuard, RwLockWriteGuard, broadcast}; -use uuid::Uuid; - -use super::{BROADCAST_CAPACITY, RoomConnectionManager}; - -impl RoomConnectionManager { - pub async fn subscribe_typing(&self, room_id: Uuid) -> broadcast::Receiver> { - let mut map: RwLockWriteGuard< - '_, - std::collections::HashMap>>, - > = self.typing_inner.write().await; - let tx = map.entry(room_id).or_insert_with(|| { - let (tx, _) = broadcast::channel(BROADCAST_CAPACITY); - tx - }); - let active_events = self.get_active_typing_events(room_id).await; - for event in active_events { - let _ = tx.send(Arc::new(event)); - } - tx.subscribe() - } - - pub async fn broadcast_typing(&self, room_id: Uuid, event: TypingEvent) { - let user_key = format!("typing:{}:{}", room_id, event.user_id); - let action = event.action.clone(); - let username = event.username.clone(); - let avatar_url = event.avatar_url.clone(); - let sender_type = event - .sender_type - .clone() - .unwrap_or_else(|| "user".to_string()); - - if let Ok(mut conn) = self.cache.conn().await { - let key = user_key; - if action == "start" { - let value = serde_json::json!({ - "username": username, - "avatar_url": avatar_url, - "sender_type": sender_type, - }) - .to_string(); - let _: Result<(), _> = redis::cmd("SETEX") - .arg(&key) - .arg(60i64) - .arg(&value) - .query_async(&mut conn) - .await; - } else { - let _: Result<(), _> = redis::cmd("DEL").arg(&key).query_async(&mut conn).await; - } - } - - let map: RwLockReadGuard< - '_, - std::collections::HashMap>>, - > = self.typing_inner.read().await; - if let Some(tx) = map.get(&room_id) { - let event = Arc::new(event); - let _ = tx.send(event); - } - } - - pub async fn get_active_typing_events(&self, room_id: Uuid) -> Vec { - let pattern = format!("typing:{}:*", room_id); - if let Ok(mut conn) = self.cache.conn().await { - let mut cursor: u64 = 0; - let mut all_keys: Vec = Vec::new(); - loop { - let (next_cursor, keys): (u64, Vec) = match redis::cmd("SCAN") - .arg(cursor) - .arg("MATCH") - .arg(&pattern) - .arg("COUNT") - .arg(100) - .query_async(&mut conn) - .await - { - Ok(r) => r, - Err(_) => return vec![], - }; - all_keys.extend(keys); - if next_cursor == 0 { - break; - } - cursor = next_cursor; - } - if all_keys.is_empty() { - return vec![]; - } - let mut results = Vec::new(); - for key in all_keys { - let parts: Vec<&str> = key.split(':').collect(); - let user_id = parts.get(2).and_then(|s| Uuid::parse_str(s).ok()); - if let (Some(value), Some(user_uuid)) = ( - redis::cmd("GET") - .arg(&key) - .query_async::(&mut conn) - .await - .ok(), - user_id, - ) { - if let Ok(parsed) = serde_json::from_str::(&value) { - results.push(TypingEvent { - room_id, - user_id: user_uuid, - username: parsed - .get("username") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(), - avatar_url: parsed - .get("avatar_url") - .and_then(|v| v.as_str()) - .map(String::from), - action: "start".to_string(), - sender_type: parsed - .get("sender_type") - .and_then(|v| v.as_str()) - .map(String::from), - }); - } - } - } - return results; - } - vec![] - } -} diff --git a/libs/room/src/connection/user_ops.rs b/libs/room/src/connection/user_ops.rs deleted file mode 100644 index cc61a06..0000000 --- a/libs/room/src/connection/user_ops.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::sync::Arc; -use tokio::sync::broadcast; -use uuid::Uuid; - -use super::{ - BROADCAST_CAPACITY, MAX_CONNECTIONS_PER_USER, ProjectRoomEvent, RoomConnectionManager, -}; -use crate::error::RoomError; -use crate::types::NotificationEvent; - -impl RoomConnectionManager { - pub async fn subscribe_user( - &self, - user_id: Uuid, - ) -> Result>, RoomError> { - let mut map = self.user_inner.write().await; - - if let Some(_sender) = map.get(&user_id) { - drop(map); - let mut counts = self.user_subscriber_count.write().await; - *counts.entry(user_id).or_insert(0) += 1; - let map = self.user_inner.read().await; - if let Some(sender) = map.get(&user_id) { - return Ok(sender.subscribe()); - } - return Err(RoomError::Internal("user channel disappeared".into())); - } - - if map.len() >= MAX_CONNECTIONS_PER_USER { - return Err(RoomError::RateLimited(format!( - "User connection limit reached ({})", - MAX_CONNECTIONS_PER_USER - ))); - } - - let (tx, rx) = broadcast::channel(BROADCAST_CAPACITY); - map.insert(user_id, tx); - drop(map); - let mut counts = self.user_subscriber_count.write().await; - counts.insert(user_id, 1); - self.metrics.users_online.increment(1.0); - Ok(rx) - } - - pub async fn unsubscribe_user(&self, user_id: Uuid) { - let mut counts = self.user_subscriber_count.write().await; - let count = counts.entry(user_id).or_insert(0); - if *count > 0 { - *count -= 1; - } - if *count == 0 { - self.metrics.users_online.decrement(1.0); - counts.remove(&user_id); - drop(counts); - let mut map = self.user_inner.write().await; - map.remove(&user_id); - } - } - - pub async fn broadcast_to_user(&self, user_id: Uuid, event: ProjectRoomEvent) { - let map = self.user_inner.read().await; - if let Some(sender) = map.get(&user_id) { - let event = Arc::new(event); - if sender.send(event).is_err() { - self.metrics.broadcasts_dropped.increment(1); - } - } - } - - pub async fn subscribe_user_notification( - &self, - user_id: Uuid, - ) -> broadcast::Receiver> { - let mut map = self.user_notification_inner.write().await; - if let Some(sender) = map.get(&user_id) { - return sender.subscribe(); - } - let (tx, rx) = broadcast::channel(BROADCAST_CAPACITY); - map.insert(user_id, tx); - rx - } - - pub async fn unsubscribe_user_notification(&self, user_id: Uuid) { - let mut map = self.user_notification_inner.write().await; - map.remove(&user_id); - } - - pub async fn push_user_notification(&self, user_id: Uuid, event: Arc) { - let map = self.user_notification_inner.read().await; - if let Some(sender) = map.get(&user_id) { - let _ = sender.send(event); - } - } -} diff --git a/libs/room/src/draft_and_history.rs b/libs/room/src/draft_and_history.rs deleted file mode 100644 index 17dfe3d..0000000 --- a/libs/room/src/draft_and_history.rs +++ /dev/null @@ -1,276 +0,0 @@ -use crate::error::RoomError; -use crate::service::RoomService; -use crate::ws_context::WsUserContext; -use chrono::Utc; -use models::rooms::NotificationType; -use models::rooms::room_message_edit_history; -use models::users::user as user_model; -use sea_orm::*; -use uuid::Uuid; - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct MessageEditHistoryEntry { - pub old_content: String, - pub new_content: String, - pub edited_at: chrono::DateTime, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct MessageEditHistoryResponse { - pub message_id: Uuid, - pub history: Vec, - pub total_edits: i64, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct MentionNotificationResponse { - pub message_id: Uuid, - pub mentioned_by: Uuid, - pub mentioned_by_name: String, - pub content_preview: String, - pub room_id: Uuid, - pub room_name: String, - pub created_at: chrono::DateTime, -} - -#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] -pub struct DraftResponse { - pub room_id: Uuid, - pub content: String, - pub saved_at: chrono::DateTime, -} - -#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] -pub struct DraftSaveRequest { - pub content: String, -} - -impl RoomService { - pub async fn save_message_edit_history( - &self, - message_id: Uuid, - user_id: Uuid, - old_content: String, - new_content: String, - ) -> Result<(), RoomError> { - let history = room_message_edit_history::ActiveModel { - id: Set(Uuid::now_v7()), - message: Set(message_id), - user: Set(user_id), - old_content: Set(old_content), - new_content: Set(new_content), - edited_at: Set(Utc::now()), - }; - - history.insert(&self.db).await?; - - Ok(()) - } - - pub async fn get_message_edit_history( - &self, - message_id: Uuid, - ctx: &WsUserContext, - ) -> Result { - let user_id = ctx.user_id; - - let message = models::rooms::room_message::Entity::find_by_id(message_id) - .one(&self.db) - .await? - .ok_or_else(|| RoomError::NotFound("Message not found".to_string()))?; - - self.require_room_access(message.room, user_id).await?; - - let history = room_message_edit_history::Entity::find() - .filter(room_message_edit_history::Column::Message.eq(message_id)) - .order_by_asc(room_message_edit_history::Column::EditedAt) - .all(&self.db) - .await?; - - let total_edits = history.len() as i64; - let history_entries = history - .into_iter() - .map(|h| MessageEditHistoryEntry { - old_content: h.old_content, - new_content: h.new_content, - edited_at: h.edited_at, - }) - .collect(); - - Ok(MessageEditHistoryResponse { - message_id, - history: history_entries, - total_edits, - }) - } - - pub async fn get_mention_notifications( - &self, - limit: Option, - ctx: &WsUserContext, - ) -> Result, RoomError> { - let user_id = ctx.user_id; - - let limit = limit.unwrap_or(50); - - let notifications = models::rooms::room_notifications::Entity::find() - .filter(models::rooms::room_notifications::Column::UserId.eq(user_id)) - .filter( - models::rooms::room_notifications::Column::NotificationType - .eq(NotificationType::Mention), - ) - .order_by_desc(models::rooms::room_notifications::Column::CreatedAt) - .limit(limit) - .all(&self.db) - .await?; - - // Batch fetch related users to avoid N+1 queries - let related_user_ids: Vec = notifications - .iter() - .filter_map(|n| n.related_user_id) - .collect(); - let users: std::collections::HashMap = if !related_user_ids.is_empty() { - user_model::Entity::find() - .filter(user_model::Column::Uid.is_in(related_user_ids)) - .all(&self.db) - .await? - .into_iter() - .map(|u| (u.uid, u.display_name.unwrap_or(u.username))) - .collect() - } else { - std::collections::HashMap::new() - }; - - // Batch fetch room names to avoid N+1 queries - let room_ids: Vec = notifications.iter().filter_map(|n| n.room).collect(); - let rooms: std::collections::HashMap = if !room_ids.is_empty() { - models::rooms::room::Entity::find() - .filter(models::rooms::room::Column::Id.is_in(room_ids)) - .all(&self.db) - .await? - .into_iter() - .map(|r| (r.id, r.room_name)) - .collect() - } else { - std::collections::HashMap::new() - }; - - let result = notifications - .into_iter() - .map(|notification| { - let mentioned_by_name = notification - .related_user_id - .and_then(|uid| users.get(&uid)) - .cloned() - .unwrap_or_else(|| "Unknown User".to_string()); - - let room_name = notification - .room - .and_then(|rid| rooms.get(&rid)) - .cloned() - .unwrap_or_else(|| "Unknown Room".to_string()); - - let content_preview = notification - .content - .unwrap_or_default() - .chars() - .take(100) - .collect(); - - MentionNotificationResponse { - message_id: notification.related_message_id.unwrap_or_default(), - mentioned_by: notification.related_user_id.unwrap_or_default(), - mentioned_by_name, - content_preview, - room_id: notification.room.unwrap_or_default(), - room_name, - created_at: notification.created_at, - } - }) - .collect(); - - Ok(result) - } - - pub async fn mark_mention_notifications_read( - &self, - ctx: &WsUserContext, - ) -> Result<(), RoomError> { - let user_id = ctx.user_id; - - use sea_orm::sea_query::Expr; - - let now = Utc::now(); - models::rooms::room_notifications::Entity::update_many() - .col_expr( - models::rooms::room_notifications::Column::IsRead, - Expr::value(true), - ) - .col_expr( - models::rooms::room_notifications::Column::ReadAt, - Expr::value(Some(now)), - ) - .filter(models::rooms::room_notifications::Column::UserId.eq(user_id)) - .filter( - models::rooms::room_notifications::Column::NotificationType - .eq(NotificationType::Mention), - ) - .filter(models::rooms::room_notifications::Column::IsRead.eq(false)) - .exec(&self.db) - .await?; - - Ok(()) - } - - pub async fn draft_save( - &self, - room_id: Uuid, - content: String, - ctx: &WsUserContext, - ) -> Result { - let user_id = ctx.user_id; - self.require_room_access(room_id, user_id).await?; - - let key = format!("room:{}:draft:{}", room_id, user_id); - let mut conn = self - .cache - .conn() - .await - .map_err(|e| RoomError::Internal(e.to_string()))?; - - let now = Utc::now(); - deadpool_redis::redis::cmd("SETEX") - .arg(&key) - .arg(7 * 24 * 60 * 60) - .arg(&content) - .query_async::<()>(&mut conn) - .await - .map_err(|e| RoomError::Internal(e.to_string()))?; - - Ok(DraftResponse { - room_id, - content, - saved_at: now, - }) - } - - pub async fn draft_clear(&self, room_id: Uuid, ctx: &WsUserContext) -> Result<(), RoomError> { - let user_id = ctx.user_id; - self.require_room_access(room_id, user_id).await?; - - let key = format!("room:{}:draft:{}", room_id, user_id); - let mut conn = self - .cache - .conn() - .await - .map_err(|e| RoomError::Internal(e.to_string()))?; - - deadpool_redis::redis::cmd("DEL") - .arg(&key) - .query_async::<()>(&mut conn) - .await - .map_err(|e| RoomError::Internal(e.to_string()))?; - - Ok(()) - } -} diff --git a/libs/room/src/error.rs b/libs/room/src/error.rs deleted file mode 100644 index 3670cdf..0000000 --- a/libs/room/src/error.rs +++ /dev/null @@ -1,40 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum RoomError { - #[error("Database error: {0}")] - Database(#[from] sea_orm::DbErr), - - #[error("Not found: {0}")] - NotFound(String), - - #[error("Unauthorized")] - Unauthorized, - - #[error("No power / permission denied")] - NoPower, - - #[error("Rate limited: {0}")] - RateLimited(String), - - #[error("Bad request: {0}")] - BadRequest(String), - - #[error("Role parse error")] - RoleParseError, - - #[error("Internal error: {0}")] - Internal(String), -} - -impl From for RoomError { - fn from(e: anyhow::Error) -> Self { - RoomError::Internal(e.to_string()) - } -} - -impl From for RoomError { - fn from(e: agent::error::AgentError) -> Self { - RoomError::Internal(e.to_string()) - } -} diff --git a/libs/room/src/helpers.rs b/libs/room/src/helpers.rs deleted file mode 100644 index f4a2907..0000000 --- a/libs/room/src/helpers.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::service::RoomService; -use models::agents::model as ai_model; -use models::rooms::room_message; -use models::users::user as user_model; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; -use uuid::Uuid; - -impl RoomService { - pub async fn resolve_display_name( - &self, - msg: room_message::Model, - _room_id: Uuid, - ) -> super::RoomMessageResponse { - let sender_type = msg.sender_type.to_string(); - let display_name = match sender_type.as_str() { - "ai" => { - if let Some(mid) = msg.model_id { - ai_model::Entity::find_by_id(mid) - .one(&self.db) - .await - .inspect_err(|e| tracing::warn!(error = %e, model_id = %mid, "resolve_display_name: AI model lookup failed")) - .ok() - .flatten() - .map(|m| m.name) - .or_else(|| Some(format!("AI({})", &mid.to_string()[..8]))) - } else { - None - } - } - _ => { - if let Some(sender_id) = msg.sender_id { - let user = user_model::Entity::find() - .filter(user_model::Column::Uid.eq(sender_id)) - .one(&self.db) - .await - .inspect_err(|e| tracing::warn!(error = %e, user_id = %sender_id, "resolve_display_name: user lookup failed")) - .ok() - .flatten(); - user.map(|u| u.display_name.unwrap_or_else(|| u.username)) - } else { - None - } - } - }; - - let chunked = super::RoomMessageResponse::detect_chunked(&msg.thinking_content); - super::RoomMessageResponse { - id: msg.id, - seq: msg.seq, - room: msg.room, - sender_type, - sender_id: msg.sender_id, - display_name, - thread: msg.thread, - content: msg.content, - content_type: msg.content_type.to_string(), - thinking_content: msg.thinking_content, - thinking_is_chunked: chunked, - edited_at: msg.edited_at, - send_at: msg.send_at, - revoked: msg.revoked, - revoked_by: msg.revoked_by, - in_reply_to: msg.in_reply_to, - highlighted_content: None, - attachment_ids: Vec::new(), - reactions: Vec::new(), - } - } -} diff --git a/libs/room/src/helpers_tests.rs b/libs/room/src/helpers_tests.rs deleted file mode 100644 index b962c00..0000000 --- a/libs/room/src/helpers_tests.rs +++ /dev/null @@ -1,204 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::service::RoomService; - use models::rooms::MessageContentType; - - #[test] - fn test_parse_message_content_type_valid() { - assert!(matches!( - RoomService::parse_message_content_type(Some("text".into())).unwrap(), - MessageContentType::Text - )); - assert!(matches!( - RoomService::parse_message_content_type(Some("image".into())).unwrap(), - MessageContentType::Image - )); - assert!(matches!( - RoomService::parse_message_content_type(Some("audio".into())).unwrap(), - MessageContentType::Audio - )); - assert!(matches!( - RoomService::parse_message_content_type(Some("video".into())).unwrap(), - MessageContentType::Video - )); - assert!(matches!( - RoomService::parse_message_content_type(Some("file".into())).unwrap(), - MessageContentType::File - )); - } - - #[test] - fn test_parse_message_content_type_case_insensitive() { - assert!(matches!( - RoomService::parse_message_content_type(Some("TEXT".into())).unwrap(), - MessageContentType::Text - )); - assert!(matches!( - RoomService::parse_message_content_type(Some("Image".into())).unwrap(), - MessageContentType::Image - )); - } - - #[test] - fn test_parse_message_content_type_none_defaults_to_text() { - assert!(matches!( - RoomService::parse_message_content_type(None).unwrap(), - MessageContentType::Text - )); - } - - #[test] - fn test_parse_message_content_type_invalid() { - assert!(RoomService::parse_message_content_type(Some("pdf".into())).is_err()); - } - - #[test] - fn test_validate_name_valid() { - assert!(RoomService::validate_name("test-room", 100).is_ok()); - assert!(RoomService::validate_name("a", 100).is_ok()); - } - - #[test] - fn test_validate_name_empty() { - assert!(RoomService::validate_name("", 100).is_err()); - assert!(RoomService::validate_name(" ", 100).is_err()); - } - - #[test] - fn test_validate_name_too_long() { - let long = "x".repeat(101); - assert!(RoomService::validate_name(&long, 100).is_err()); - } - - #[test] - fn test_validate_content_valid() { - assert!(RoomService::validate_content("hello", 10000).is_ok()); - } - - #[test] - fn test_validate_content_empty() { - assert!(RoomService::validate_content("", 10000).is_err()); - assert!(RoomService::validate_content(" ", 10000).is_err()); - } - - #[test] - fn test_validate_content_too_long() { - let long = "x".repeat(10001); - assert!(RoomService::validate_content(&long, 10000).is_err()); - } - - #[test] - fn test_sanitize_content_removes_script_tag() { - let input = ""; - let result = RoomService::sanitize_content(input); - assert!(!result.contains("