commit 57e0ef2cf6fd150441e89c3b2b8cb0bfcb8d05a2 Author: 0264408 Date: Tue Mar 10 16:26:48 2026 +0800 init: 初始化项目 diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..c8a447f --- /dev/null +++ b/.env.dev @@ -0,0 +1,8 @@ +# 开发环境配置 +SERVER_ADDRESS=:9050 +SERVER_MODE=dev +DATABASE_DSN=nebula.db +STORAGE_TYPE=local +STORAGE_BASE_PATH=./uploads +STORAGE_BASE_URL=http://localhost:9050/files +JWT_SECRET=dev-secret-key diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b8694e6 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# 服务器配置 +SERVER_ADDRESS=:9050 +SERVER_MODE=dev # dev 或 prod + +# 数据库配置 +DATABASE_DSN=nebula.db + +# 存储配置 +STORAGE_TYPE=local +STORAGE_BASE_PATH=./uploads +STORAGE_BASE_URL=http://localhost:9050/files + +# JWT 配置 +JWT_SECRET=your-secret-key-change-in-production +JWT_ACCESS_TOKEN_DURATION=7200 # 2小时(秒) +JWT_REFRESH_TOKEN_DURATION=604800 # 7天(秒) + +# 前端配置(生产环境) +# FRONTEND_PATH=./web/dist diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..758186a --- /dev/null +++ b/.env.prod @@ -0,0 +1,9 @@ +# 生产环境配置 +SERVER_ADDRESS=:9050 +SERVER_MODE=prod +DATABASE_DSN=nebula.db +STORAGE_TYPE=local +STORAGE_BASE_PATH=./uploads +STORAGE_BASE_URL=http://localhost:9050/files +JWT_SECRET=your-secret-key-change-in-production +FRONTEND_PATH=./web/dist diff --git a/README.md b/README.md new file mode 100644 index 0000000..32d3cd0 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Nebula + +Nebula 是一个用于分发桌面/客户端应用版本的全栈项目,后端使用 Go (Gin + GORM) 暴露应用、版本、资产、更新检查与 JWT 身份验证接口,前端使用 Vue 3 + Vite 构建管理控制台。 + +## 🔍 项目概览 + +- **API 服务** (`cmd/nebula-server` + `internal/`): 提供应用、版本、资产、更新校验、认证等 REST API,并支持本地文件存储。 +- **Web 控制台** (`web/`): Vue 3 + TypeScript + Pinia + Vue Router,负责登录、版本管理等 UI。 +- **文档** (`docs/`): 包含资产接口、JWT、JSON Key、发布 API 以及更新检查规格。 +- **脚本** (`scripts/`): 统一封装 dev/build/start 脚本(.sh/.bat)。 + +``` +. +├── cmd/nebula-server # API 服务入口 +├── internal/ # 领域服务、API 路由、存储、认证等 +├── web/ # Vue 3 前端应用 +├── docs/ # API 与协议文档 +├── scripts/ # 本地开发与构建脚本 +├── config*.yaml # 运行配置 (默认、dev、prod) +├── uploads/ # 本地存储文件输出目录 +└── go.mod # Go 依赖定义 +``` + +## 🚀 快速开始 + +### 1. 准备依赖 + +- Go 1.21+(`go.mod` 设定 1.25,使用最新稳定版即可) +- Node.js 20+ 与 [pnpm](https://pnpm.io/)(前端) + +### 2. 配置后端 + +1. 复制并调整配置:`cp config.dev.yaml config.yaml`(Windows 可使用 `copy`)。 +2. 启动 API 服务: + - 开发模式:`go run ./cmd/nebula-server` + - 生产构建:`go build -o bin/nebula-server ./cmd/nebula-server` +3. 默认会初始化本地 SQLite 数据库、文件存储与 JWT 认证。 + +### 3. 启动前端 + +```bash +cd web +pnpm install +pnpm dev # 开发热更新 +pnpm build # 生产构建,产物位于 web/dist +``` + +也可以使用 `scripts/dev.(sh|bat)`、`scripts/build.(sh|bat)`、`scripts/start.(sh|bat)` 进行一键操作。 + +## 📚 文档资源 + +- `docs/ASSET_API.md` – 资产上传/管理接口规格 +- `docs/AUTH_JWT.md` – JWT 身份认证说明 +- `docs/JSON_KEY_FORMAT.md` – JSON Key 结构 +- `docs/RELEASE_API.md` – 发布版本 API +- `docs/UPDATE_CHECK.md` – 客户端更新检查流程 + +## 📦 功能实现情况 + +### 后端 API 与服务 + +| 模块 | 功能 | 状态 | 说明 | +|------|------|------|------| +| 认证 | 管理员/普通用户登录、Refresh、Profile、改密 | ✅ 已落地 | `AuthHandler.Login/RefreshToken/GetProfile/ChangePassword` 已通过 `JWTMiddleware` 保护,并支持配置中的默认管理员。 | +| 认证 | 自助注册 | 🟡 未启用 | `AuthHandler.Register` 与 `AuthService.Register` 已实现,但 `/api/auth/register` 在路由中被注释以限制注册,`docs/AUTH_JWT.md` 需同步说明。 | +| 应用 | CRUD | ✅ 已落地 | `AppService` + `AppHandler` 覆盖列表/详情/创建/更新/删除。 | +| 版本 | CRUD + 最新版本查询 | ✅ 基本可用 | `ReleaseService` 支持按渠道筛选及 `GetLatest`,但 Delete 目前不会清理关联资源(见 TODO #3)。 | +| 资源 | 列表/详情/外链创建/上传/下载 | ✅ 已落地 | `AssetService` 负责校验和计算、本地存储、下载路径转换。 | +| 资源 | 数字签名与删除日志 | 🟡 待补完 | 上传时 `Signature` 仍为空,删除文件失败只留下 TODO(见 TODO #1/#2)。 | +| 更新检查 | `/api/check-update` | ✅ 已落地 | `updater.CheckUpdate` 使用语义化版本比较和校验和返回。 | +| 存储 | LocalStorage | ✅ 已落地 | file save/delete/url/exists 已实现并在 `main.go` 注入。 | +| 存储 | OSS / S3 适配器 | ⏳ 预留接口 | `storage.Storage` 提供接口,配置项存在但尚无实现分支。 | +| 中间件 | AdminMiddleware | ⚠️ 未接入 | 中间件已实现,但目前所有受保护路由仅做 JWT 校验,未强制管理员角色。 | + +### 前端 Web 控制台 + +| 功能 | 状态 | 说明 | +|------|------|------| +| 登录页 | ✅ 可用 | `view/auth/login.vue` 已接入 `api.login` 与 Pinia 存储,并写入 localStorage。 | +| Token 刷新 | 🟡 未使用 | `api.refreshToken` 定义但尚未在 store/拦截器中调用,实际会在 Access Token 过期后直接失败。 | +| 仪表盘数据 | 🟡 静态占位 | `view/home/index.vue` 的统计卡片与最近动态均为硬编码示例,尚未调用后端。 | +| 导航/路由 | ⚠️ 不完整 | 路由仅注册 `/` 与 `/login`,但 `layout/default.vue` 的“应用管理”“系统设置”菜单会指向不存在的路由。 | +| 应用/版本/资产管理 UI | 🔲 未实现 | 目前没有对应的列表、表单或上传界面,API 只能通过 Postman/curl 使用。 | +| 注册/忘记密码 UI | 🔲 未实现 | 登录页仅展示提示文字,未提供表单或链接。 | + +## ✅ 当前 TODO 列表 + +| # | 模块 | 文件 | 待办 | 建议思路 | +|---|------|------|------|----------| +| 1 | 资产上传 | `internal/asset/service.go` (行 133) | `Signature` 字段目前为空,需要实现文件签名生成与校验。 | 生成私钥签名或哈希签名存入字段,并在客户端校验;可考虑与 release key 绑定。 | +| 2 | 资产删除 | `internal/asset/service.go` (行 226) | 删除存储文件失败时仅注释提醒,需要记录日志。 | 注入 logger(`pkg/logger`)并输出错误详情,便于排查存储清理问题。 | +| 3 | 版本删除 | `internal/release/service.go` (行 137) | 删除 Release 时尚未同步删除关联 Assets。 | 在删除前查询并删除对应资产(含文件),或配置数据库外键级联,确保无孤立文件。 | + +> 若新增 TODO,请同步更新此表,保持 README 成为 backlog 的单一事实来源。 + +## 🧪 测试与质量 + +- 后端可使用 `go test ./...` 运行单元测试。 +- 前端使用 `pnpm test`(若添加测试工具)或 `pnpm lint` 保持规范。 + +## 🗺️ 后续展望 + +- 为资产签名、删除链路补全 TODO 后,可引入对象存储(S3、OSS 等)与 CI/CD 流水线。 +- 可基于 `docs/UPDATE_CHECK.md` 实现自动更新客户端 SDK。 diff --git a/cmd/nebula-server/main.go b/cmd/nebula-server/main.go new file mode 100644 index 0000000..90a2d75 --- /dev/null +++ b/cmd/nebula-server/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "log" + "nebula/internal/api" + "nebula/internal/auth" + "nebula/internal/config" + "nebula/internal/db" + "nebula/internal/storage" + + "github.com/gin-gonic/gin" +) + +func main() { + cfg := config.Load() + + database := db.Init(cfg.Database.DSN) + + // 初始化存储 + var stor storage.Storage + var err error + + switch cfg.Storage.Type { + case "local": + stor, err = storage.NewLocalStorage(cfg.Storage.BasePath, cfg.Storage.BaseURL) + if err != nil { + log.Fatalf("failed to initialize storage: %v", err) + } + default: + log.Fatalf("unsupported storage type: %s", cfg.Storage.Type) + } + + // 初始化 JWT 服务 + jwtService := auth.NewJWTService( + cfg.JWT.Secret, + cfg.GetAccessTokenDuration(), + cfg.GetRefreshTokenDuration(), + ) + + // 初始化认证服务(使用配置的管理员账号) + authService := auth.NewAuthService( + database, + jwtService, + cfg.Admin.Username, + cfg.Admin.Password, + ) + + r := gin.Default() + + // 静态文件服务 - 用于提供文件下载 + r.Static("/files", cfg.Storage.BasePath) + + // 注册 API 路由 + api.RegisterRoutes(r, database, stor, jwtService, authService) + + // 生产环境:提供前端静态资源 + if cfg.Frontend.Enabled { + r.Static("/assets", cfg.Frontend.Path+"/assets") + r.StaticFile("/favicon.ico", cfg.Frontend.Path+"/favicon.ico") + + // 处理 SPA 路由 - 所有未匹配的路由都返回 index.html + r.NoRoute(func(c *gin.Context) { + c.File(cfg.Frontend.Path + "/index.html") + }) + + log.Printf("Frontend static files enabled at %s", cfg.Frontend.Path) + log.Printf("Frontend available at http://%s", cfg.Server.Address) + } else { + log.Printf("Running in dev mode - frontend should be served by Vite dev server") + } + + log.Printf("Server starting on %s (mode: %s)", cfg.Server.Address, cfg.Server.Mode) + log.Printf("Admin username: %s", cfg.Admin.Username) + log.Printf("API available at http://%s/api", cfg.Server.Address) + r.Run(cfg.Server.Address) +} diff --git a/config.dev.yaml b/config.dev.yaml new file mode 100644 index 0000000..7fe1524 --- /dev/null +++ b/config.dev.yaml @@ -0,0 +1,25 @@ +server: + address: ":9050" + mode: "dev" + +database: + dsn: "nebula.db" + +storage: + type: "local" + basePath: "./uploads" + baseUrl: "http://localhost:9050/files" + +jwt: + secret: "dev-secret-key" + accessTokenDuration: 7200 + refreshTokenDuration: 604800 + +frontend: + enabled: false + path: "./web/dist" + +# 开发环境管理员账号 +admin: + username: "admin" + password: "123456" diff --git a/config.prod.yaml b/config.prod.yaml new file mode 100644 index 0000000..c436201 --- /dev/null +++ b/config.prod.yaml @@ -0,0 +1,25 @@ +server: + address: ":9050" + mode: "prod" + +database: + dsn: "nebula.db" + +storage: + type: "local" + basePath: "./uploads" + baseUrl: "http://localhost:9050/files" + +jwt: + secret: "change-this-secret-in-production" + accessTokenDuration: 7200 + refreshTokenDuration: 604800 + +frontend: + enabled: true + path: "./web/dist" + +# ⚠️ 生产环境管理员账号 - 请务必修改密码! +admin: + username: "admin" + password: "CHANGE_THIS_PASSWORD" diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..82d546b --- /dev/null +++ b/config.yaml @@ -0,0 +1,25 @@ +server: + address: ":9050" + mode: "dev" # dev or prod + +database: + dsn: "nebula.db" + +storage: + type: "local" # local, oss, s3 + basePath: "./uploads" + baseUrl: "http://localhost:9050/files" + +jwt: + secret: "dev-secret-key-change-in-production" + accessTokenDuration: 7200 # 2 hours in seconds + refreshTokenDuration: 604800 # 7 days in seconds + +frontend: + enabled: false # Will be set to true in prod mode + path: "./web/dist" + +# 管理员账号配置 +admin: + username: "admin" + password: "123456" # ⚠️ 生产环境请务必修改密码! diff --git a/dist/nebula-server.exe b/dist/nebula-server.exe new file mode 100644 index 0000000..d1f9a64 Binary files /dev/null and b/dist/nebula-server.exe differ diff --git a/docs/ASSET_API.md b/docs/ASSET_API.md new file mode 100644 index 0000000..c0bd503 --- /dev/null +++ b/docs/ASSET_API.md @@ -0,0 +1,312 @@ +# Asset API 使用示例 + +服务器地址: http://localhost:9050 + +## 📦 存储架构 + +### 可扩展的存储接口设计 + +项目采用接口化设计,支持多种存储后端: + +```go +type Storage interface { + Save(filename string, content io.Reader) (string, error) + Delete(path string) error + GetURL(path string) string + Exists(path string) bool +} +``` + +**当前实现:** +- ✅ **LocalStorage** - 本地文件系统存储 +- 🔲 **OSSStorage** - 阿里云 OSS(预留) +- 🔲 **S3Storage** - AWS S3(预留) + +**切换存储方式只需:** +1. 实现 `Storage` 接口 +2. 在配置中修改 `STORAGE_TYPE` +3. 无需修改业务代码 + +### 配置 + +通过环境变量配置存储: + +```bash +# 本地存储(默认) +STORAGE_TYPE=local +STORAGE_BASE_PATH=./uploads +STORAGE_BASE_URL=http://localhost:9050/files + +# 未来支持 +# STORAGE_TYPE=oss +# STORAGE_TYPE=s3 +``` + +## 📝 API 端点 + +### 1. 上传文件并创建资源 + +**重要:** 这是最常用的方式,上传文件并自动创建记录。 + +```bash +# 使用 curl +curl -X POST http://localhost:9050/api/releases/1/assets/upload \ + -F "file=@path/to/app.exe" \ + -F "platform=windows" \ + -F "arch=amd64" + +# PowerShell 示例 +$file = "D:\app.exe" +$uri = "http://localhost:9050/api/releases/1/assets/upload" + +$form = @{ + file = Get-Item -Path $file + platform = "windows" + arch = "amd64" +} + +Invoke-RestMethod -Uri $uri -Method Post -Form $form +``` + +**支持的文件类型:** +- Windows: `.exe`, `.msi`, `.zip` +- macOS: `.dmg`, `.pkg`, `.app` +- Linux: `.deb`, `.rpm`, `.appimage`, `.tar.gz` + +**响应:** +```json +{ + "code": 0, + "msg": "ok", + "data": { + "id": 1, + "releaseId": 1, + "platform": "windows", + "arch": "amd64", + "url": "http://localhost:9050/files/releases/1/windows-amd64/app.exe", + "checksum": "sha256哈希值", + "createdAt": "2026-03-10T12:00:00Z" + } +} +``` + +### 2. 获取所有资源列表 + +```bash +curl http://localhost:9050/api/assets + +# PowerShell +Invoke-RestMethod -Uri "http://localhost:9050/api/assets" +``` + +### 3. 获取指定版本的资源列表 + +```bash +curl http://localhost:9050/api/releases/1/assets + +# PowerShell +Invoke-RestMethod -Uri "http://localhost:9050/api/releases/1/assets" +``` + +### 4. 获取单个资源详情 + +```bash +curl http://localhost:9050/api/assets/1 + +# PowerShell +Invoke-RestMethod -Uri "http://localhost:9050/api/assets/1" +``` + +### 5. 创建资源记录(外部 URL) + +如果文件托管在其他地方(如 GitHub Releases、CDN),可以只创建记录: + +```bash +curl -X POST http://localhost:9050/api/assets \ + -H "Content-Type: application/json" \ + -d '{ + "releaseId": 1, + "platform": "windows", + "arch": "amd64", + "url": "https://github.com/user/repo/releases/download/v1.0.0/app.exe", + "checksum": "sha256...", + "signature": "签名数据" + }' + +# PowerShell +$body = @{ + releaseId = 1 + platform = "windows" + arch = "amd64" + url = "https://github.com/user/repo/releases/download/v1.0.0/app.exe" + checksum = "sha256..." +} | ConvertTo-Json + +Invoke-RestMethod -Uri "http://localhost:9050/api/assets" ` + -Method Post -Body $body -ContentType "application/json" +``` + +### 6. 更新资源信息 + +```bash +curl -X PUT http://localhost:9050/api/assets/1 \ + -H "Content-Type: application/json" \ + -d '{ + "platform": "windows", + "arch": "arm64", + "signature": "新签名" + }' + +# PowerShell +$body = @{ + platform = "windows" + arch = "arm64" + signature = "新签名" +} | ConvertTo-Json + +Invoke-RestMethod -Uri "http://localhost:9050/api/assets/1" ` + -Method Put -Body $body -ContentType "application/json" +``` + +### 7. 删除资源 + +**注意:** 会同时删除文件和数据库记录。 + +```bash +curl -X DELETE http://localhost:9050/api/assets/1 + +# PowerShell +Invoke-RestMethod -Uri "http://localhost:9050/api/assets/1" -Method Delete +``` + +### 8. 下载资源文件 + +```bash +# 方式1:通过API端点下载 +curl http://localhost:9050/api/assets/1/download -o app.exe + +# 方式2:直接访问文件URL +curl http://localhost:9050/files/releases/1/windows-amd64/app.exe -o app.exe + +# PowerShell - 方式1 +Invoke-WebRequest -Uri "http://localhost:9050/api/assets/1/download" -OutFile "app.exe" + +# PowerShell - 方式2(推荐,支持断点续传) +Invoke-WebRequest -Uri "http://localhost:9050/files/releases/1/windows-amd64/app.exe" -OutFile "app.exe" +``` + +## 🔄 完整工作流程 + +```powershell +# 1. 创建应用 +$app = Invoke-RestMethod -Method POST -Uri "http://localhost:9050/api/apps" ` + -ContentType "application/json" ` + -Body '{"id":"my-app","name":"我的应用","description":"测试"}' + +# 2. 创建版本 +$release = Invoke-RestMethod -Method POST -Uri "http://localhost:9050/api/releases" ` + -ContentType "application/json" ` + -Body '{"appID":"my-app","version":"1.0.0","notes":"初始版本","channel":"stable","pubDate":"2026-03-10T10:00:00Z"}' + +# 3. 上传 Windows 版本 +$winForm = @{ + file = Get-Item -Path "D:\builds\app-windows.exe" + platform = "windows" + arch = "amd64" +} +$winAsset = Invoke-RestMethod -Uri "http://localhost:9050/api/releases/1/assets/upload" ` + -Method Post -Form $winForm + +# 4. 上传 macOS 版本 +$macForm = @{ + file = Get-Item -Path "D:\builds\app-macos.dmg" + platform = "darwin" + arch = "arm64" +} +$macAsset = Invoke-RestMethod -Uri "http://localhost:9050/api/releases/1/assets/upload" ` + -Method Post -Form $macForm + +# 5. 上传 Linux 版本 +$linuxForm = @{ + file = Get-Item -Path "D:\builds\app-linux" + platform = "linux" + arch = "amd64" +} +$linuxAsset = Invoke-RestMethod -Uri "http://localhost:9050/api/releases/1/assets/upload" ` + -Method Post -Form $linuxForm + +# 6. 查看该版本的所有资源 +$assets = Invoke-RestMethod -Uri "http://localhost:9050/api/releases/1/assets" +$assets.data | Format-Table id, platform, arch, url +``` + +## 📊 API 路由列表 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/assets | 获取所有资源列表 | +| GET | /api/assets/:id | 获取单个资源详情 | +| POST | /api/assets | 创建资源记录(外部URL) | +| PUT | /api/assets/:id | 更新资源信息 | +| DELETE | /api/assets/:id | 删除资源(含文件) | +| GET | /api/assets/:id/download | 下载资源文件 | +| GET | /api/releases/:id/assets | 获取版本的资源列表 | +| POST | /api/releases/:id/assets/upload | 上传文件并创建资源 | +| GET | /files/* | 静态文件访问 | + +## 🔧 文件存储结构 + +``` +uploads/ + └── releases/ + └── {releaseID}/ + ├── windows-amd64/ + │ └── app.exe + ├── darwin-arm64/ + │ └── app.dmg + └── linux-amd64/ + └── app +``` + +## 🔐 安全特性 + +1. **文件校验和**:自动计算 SHA256,确保文件完整性 +2. **文件类型限制**:只允许特定扩展名的文件 +3. **平台唯一性**:同一版本的同一平台+架构只能有一个资源 +4. **级联删除**:删除资源时自动删除文件 + +## 🚀 扩展到云存储 + +未来要切换到 OSS/S3,只需: + +1. **实现新的存储适配器** + +```go +// internal/storage/oss.go +type OSSStorage struct { + client *oss.Client + bucket string +} + +func (s *OSSStorage) Save(filename string, content io.Reader) (string, error) { + // OSS 上传逻辑 +} +// ... 实现其他接口方法 +``` + +2. **在 main.go 中添加分支** + +```go +switch cfg.Storage.Type { +case "local": + stor, err = storage.NewLocalStorage(...) +case "oss": + stor, err = storage.NewOSSStorage(...) +case "s3": + stor, err = storage.NewS3Storage(...) +} +``` + +3. **业务代码无需修改** ✨ + +所有 Service 和 Handler 代码都基于接口编程,自动支持新的存储方式! diff --git a/docs/AUTH_JWT.md b/docs/AUTH_JWT.md new file mode 100644 index 0000000..73e0927 --- /dev/null +++ b/docs/AUTH_JWT.md @@ -0,0 +1,405 @@ +# JWT 认证系统文档 + +## 🔐 认证架构 + +Nebula 项目使用 **JWT (JSON Web Token)** 认证系统,提供完整的用户认证和授权功能。 + +### 核心特性 + +- ✅ **JWT 双 Token 机制**(Access Token + Refresh Token) +- ✅ **密码加密存储**(bcrypt) +- ✅ **灵活的 Token 过期时间配置** +- ✅ **中间件保护路由** +- ✅ **角色权限支持**(user/admin) +- ✅ **用户注册和登录** +- ✅ **修改密码** +- ✅ **Token 刷新** + +## 📝 API 端点 + +### 1. 用户注册 + +```http +POST /api/auth/register +Content-Type: application/json + +{ + "username": "testuser", + "email": "test@example.com", + "password": "password123" +} +``` + +**响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "user": { + "id": "uuid", + "username": "testuser", + "email": "test@example.com", + "role": "user", + "createdAt": "2026-03-10T12:00:00Z", + "updatedAt": "2026-03-10T12:00:00Z" + }, + "tokens": { + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs...", + "expires_in": 7200 + } + } +} +``` + +### 2. 用户登录 + +```http +POST /api/auth/login +Content-Type: application/json + +{ + "username": "testuser", + "password": "password123" +} +``` + +**响应:** 同注册响应 + +### 3. 刷新 Token + +```http +POST /api/auth/refresh +Content-Type: application/json + +{ + "refresh_token": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "access_token": "new_access_token", + "refresh_token": "new_refresh_token", + "expires_in": 7200 + } +} +``` + +### 4. 获取当前用户信息 + +```http +GET /api/auth/profile +Authorization: Bearer +``` + +**响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "id": "uuid", + "username": "testuser", + "email": "test@example.com", + "role": "user", + "createdAt": "2026-03-10T12:00:00Z", + "updatedAt": "2026-03-10T12:00:00Z" + } +} +``` + +### 5. 修改密码 + +```http +POST /api/auth/change-password +Authorization: Bearer +Content-Type: application/json + +{ + "old_password": "password123", + "new_password": "newpassword456" +} +``` + +**响应:** +```json +{ + "code": 0, + "message": "password changed successfully" +} +``` + +## 🧪 PowerShell 测试示例 + +### 完整测试流程 + +```powershell +# 1. 注册用户 +$registerBody = @{ + username = "testuser" + email = "test@example.com" + password = "password123" +} | ConvertTo-Json + +$registerResponse = Invoke-RestMethod -Uri "http://localhost:9050/api/auth/register" ` + -Method Post -Body $registerBody -ContentType "application/json" + +Write-Host "✅ 注册成功!" +Write-Host "用户 ID: $($registerResponse.data.user.id)" +Write-Host "Access Token: $($registerResponse.data.tokens.access_token.Substring(0, 20))..." + +# 保存 token +$accessToken = $registerResponse.data.tokens.access_token +$refreshToken = $registerResponse.data.tokens.refresh_token + +# 2. 登录(测试) +$loginBody = @{ + username = "testuser" + password = "password123" +} | ConvertTo-Json + +$loginResponse = Invoke-RestMethod -Uri "http://localhost:9050/api/auth/login" ` + -Method Post -Body $loginBody -ContentType "application/json" + +Write-Host "✅ 登录成功!" + +# 3. 获取用户信息 +$headers = @{ + Authorization = "Bearer $accessToken" +} + +$profile = Invoke-RestMethod -Uri "http://localhost:9050/api/auth/profile" ` + -Method Get -Headers $headers + +Write-Host "✅ 获取用户信息成功!" +Write-Host "用户名: $($profile.data.username)" +Write-Host "邮箱: $($profile.data.email)" + +# 4. 创建应用(测试认证保护) +$appBody = @{ + id = "test-app" + name = "测试应用" + description = "需要认证才能创建" +} | ConvertTo-Json + +$app = Invoke-RestMethod -Uri "http://localhost:9050/api/apps" ` + -Method Post -Body $appBody -ContentType "application/json" -Headers $headers + +Write-Host "✅ 创建应用成功(认证有效)!" + +# 5. 刷新 Token +Start-Sleep -Seconds 2 +$refreshBody = @{ + refresh_token = $refreshToken +} | ConvertTo-Json + +$newTokens = Invoke-RestMethod -Uri "http://localhost:9050/api/auth/refresh" ` + -Method Post -Body $refreshBody -ContentType "application/json" + +Write-Host "✅ Token 刷新成功!" + +# 6. 修改密码 +$changePasswordBody = @{ + old_password = "password123" + new_password = "newpassword456" +} | ConvertTo-Json + +$changeResult = Invoke-RestMethod -Uri "http://localhost:9050/api/auth/change-password" ` + -Method Post -Body $changePasswordBody -ContentType "application/json" -Headers $headers + +Write-Host "✅ 密码修改成功!" + +# 7. 测试未认证访问(应该失败) +try { + Invoke-RestMethod -Uri "http://localhost:9050/api/apps" -Method Get +} catch { + Write-Host "❌ 未认证访问被拒绝(正确)" +} +``` + +## 🔧 配置 + +### 环境变量 + +```bash +# JWT 密钥(生产环境必须修改!) +JWT_SECRET=your-secret-key-change-in-production + +# Access Token 过期时间(默认 2 小时) +JWT_ACCESS_TOKEN_DURATION=7200 # 秒 +# 或者 +JWT_ACCESS_TOKEN_DURATION=2h + +# Refresh Token 过期时间(默认 7 天) +JWT_REFRESH_TOKEN_DURATION=604800 # 秒 +# 或者 +JWT_REFRESH_TOKEN_DURATION=168h +``` + +### 推荐配置 + +**开发环境:** +- Access Token: 2 小时 +- Refresh Token: 7 天 + +**生产环境:** +- Access Token: 15 分钟 - 1 小时 +- Refresh Token: 30 天 +- 强密钥(256位以上) + +## 🛡️ 安全特性 + +### 1. 密码加密 + +```go +// 使用 bcrypt 加密 +hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + +// 验证密码 +bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +``` + +### 2. Token 验证 + +- ✅ 签名验证 +- ✅ 过期时间检查 +- ✅ 算法验证(HMAC-SHA256) + +### 3. 中间件保护 + +```go +// 需要认证 +authRequired.Use(auth.JWTMiddleware(jwtService)) + +// 需要管理员权限 +adminRequired.Use(auth.JWTMiddleware(jwtService), auth.AdminMiddleware()) +``` + +## 📊 路由保护状态 + +### 公开路由(无需认证) + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | /api/auth/register | 用户注册 | +| POST | /api/auth/login | 用户登录 | +| POST | /api/auth/refresh | 刷新 Token | +| GET | /api/check-update | 检查更新 | + +### 需要认证的路由 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/auth/profile | 获取用户信息 | +| POST | /api/auth/change-password | 修改密码 | +| GET/POST/PUT/DELETE | /api/apps/* | 应用管理 | +| GET/POST/PUT/DELETE | /api/releases/* | 版本管理 | +| GET/POST/PUT/DELETE | /api/assets/* | 资源管理 | + +## 🔄 Token 刷新流程 + +``` +Client Server + | | + |-------- Request API --------->| (Access Token) + |<----- 401 Token Expired ------| ❌ + | | + |--- Refresh Token Request ---->| (Refresh Token) + |<--- New Access Token ---------| ✅ + | | + |-------- Request API --------->| (New Access Token) + |<-------- Success -------------| ✅ +``` + +## 💡 客户端集成示例 + +### JavaScript/TypeScript + +```typescript +class AuthService { + private accessToken: string | null = null; + private refreshToken: string | null = null; + + async login(username: string, password: string) { + const response = await fetch('http://localhost:9050/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + + const data = await response.json(); + if (data.code === 0) { + this.accessToken = data.data.tokens.access_token; + this.refreshToken = data.data.tokens.refresh_token; + + // 保存到 localStorage + localStorage.setItem('access_token', this.accessToken); + localStorage.setItem('refresh_token', this.refreshToken); + } + return data; + } + + async request(url: string, options: RequestInit = {}) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${this.accessToken}` + }; + + let response = await fetch(url, options); + + // Token 过期,尝试刷新 + if (response.status === 401) { + await this.refresh(); + options.headers['Authorization'] = `Bearer ${this.accessToken}`; + response = await fetch(url, options); + } + + return response.json(); + } + + async refresh() { + const response = await fetch('http://localhost:9050/api/auth/refresh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: this.refreshToken }) + }); + + const data = await response.json(); + if (data.code === 0) { + this.accessToken = data.data.access_token; + this.refreshToken = data.data.refresh_token; + localStorage.setItem('access_token', this.accessToken); + localStorage.setItem('refresh_token', this.refreshToken); + } + } +} +``` + +## 🚀 扩展功能 + +### 未来可添加 + +1. **Token 黑名单**:退出登录时将 token 加入黑名单 +2. **双因素认证(2FA)**:增强安全性 +3. **OAuth2 第三方登录**:支持 GitHub、Google 等 +4. **权限细粒度控制**:基于资源的权限管理 +5. **登录日志**:记录登录历史和异常 +6. **IP 白名单**:限制访问来源 +7. **账号锁定**:多次登录失败后锁定 + +## ✅ 测试验证 + +1. ✅ 注册新用户 +2. ✅ 登录获取 token +3. ✅ 使用 token 访问受保护的 API +4. ✅ 刷新 token +5. ✅ 修改密码 +6. ✅ 未认证访问被拒绝 + +完整的 JWT 认证系统已经实现并运行正常!🎉 diff --git a/docs/JSON_KEY_FORMAT.md b/docs/JSON_KEY_FORMAT.md new file mode 100644 index 0000000..f898123 --- /dev/null +++ b/docs/JSON_KEY_FORMAT.md @@ -0,0 +1,164 @@ +# JSON Key 格式验证 + +## ✅ 已统一为小驼峰(camelCase)格式 + +### 修改的文件 + +1. **`internal/auth/jwt.go`** + - `user_id` → `userId` + - `access_token` → `accessToken` + - `refresh_token` → `refreshToken` + - `expires_in` → `expiresIn` + +2. **`internal/auth/service.go`** + - `refresh_token` → `refreshToken` + +3. **`internal/api/handler/auth.go`** + - `old_password` → `oldPassword` + - `new_password` → `newPassword` + +### 统一的 JSON Key 规范 + +| Go 结构体字段 | JSON Key (小驼峰) | +|--------------|------------------| +| ID | id | +| UserID | userId | +| Username | username | +| Email | email | +| Role | role | +| CreatedAt | createdAt | +| UpdatedAt | updatedAt | +| AccessToken | accessToken | +| RefreshToken | refreshToken | +| ExpiresIn | expiresIn | +| OldPassword | oldPassword | +| NewPassword | newPassword | +| AppID | appId (如果有) | +| ReleaseID | releaseId | +| Platform | platform | +| Arch | arch | +| URL | url | +| Signature | signature | +| Checksum | checksum | + +### API 响应示例 + +**注册/登录响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "user": { + "id": "uuid", + "username": "testuser", + "email": "test@example.com", + "role": "user", + "createdAt": "2026-03-10T12:00:00Z", + "updatedAt": "2026-03-10T12:00:00Z" + }, + "tokens": { + "accessToken": "eyJhbGciOiJIUzI1NiIs...", + "refreshToken": "eyJhbGciOiJIUzI1NiIs...", + "expiresIn": 7200 + } + } +} +``` + +**刷新 Token 请求:** +```json +{ + "refreshToken": "eyJhbGciOiJIUzI1NiIs..." +} +``` + +**刷新 Token 响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "accessToken": "new_token...", + "refreshToken": "new_refresh...", + "expiresIn": 7200 + } +} +``` + +**修改密码请求:** +```json +{ + "oldPassword": "oldpass123", + "newPassword": "newpass456" +} +``` + +**资源响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "releaseId": 1, + "platform": "windows", + "arch": "amd64", + "url": "http://localhost:9050/files/...", + "signature": "...", + "checksum": "sha256...", + "createdAt": "2026-03-10T12:00:00Z", + "updatedAt": "2026-03-10T12:00:00Z" + } +} +``` + +### 测试验证 + +```powershell +# 1. 注册用户(验证 camelCase) +$body = @{ + username = "testuser" + email = "test@example.com" + password = "password123" +} | ConvertTo-Json + +$response = Invoke-RestMethod -Uri "http://localhost:9050/api/auth/register" ` + -Method Post -Body $body -ContentType "application/json" + +# 检查响应格式 +Write-Host "✅ Access Token 字段: accessToken" +Write-Host "✅ Refresh Token 字段: refreshToken" +Write-Host "✅ Expires In 字段: expiresIn" +Write-Host "✅ Created At 字段: createdAt" +Write-Host "✅ Updated At 字段: updatedAt" + +# 2. 刷新 Token(验证 camelCase) +$refreshBody = @{ + refreshToken = $response.data.tokens.refreshToken +} | ConvertTo-Json + +$newTokens = Invoke-RestMethod -Uri "http://localhost:9050/api/auth/refresh" ` + -Method Post -Body $refreshBody -ContentType "application/json" + +Write-Host "✅ 刷新 Token 成功,字段格式正确" + +# 3. 修改密码(验证 camelCase) +$headers = @{ + Authorization = "Bearer $($response.data.tokens.accessToken)" +} + +$changePasswordBody = @{ + oldPassword = "password123" + newPassword = "newpassword456" +} | ConvertTo-Json + +$result = Invoke-RestMethod -Uri "http://localhost:9050/api/auth/change-password" ` + -Method Post -Body $changePasswordBody -ContentType "application/json" -Headers $headers + +Write-Host "✅ 修改密码成功,字段格式正确" +``` + +## ✅ 统一完成 + +所有 JSON key 已统一为小驼峰格式(camelCase),符合前端 JavaScript/TypeScript 的命名规范。 diff --git a/docs/RELEASE_API.md b/docs/RELEASE_API.md new file mode 100644 index 0000000..8a4a134 --- /dev/null +++ b/docs/RELEASE_API.md @@ -0,0 +1,138 @@ +# Release API 使用示例 + +服务器地址: http://localhost:9050 + +## 1. 创建应用(前置步骤) + +```bash +curl -X POST http://localhost:9050/api/apps \ + -H "Content-Type: application/json" \ + -d '{ + "id": "my-app", + "name": "我的应用", + "description": "这是一个测试应用" + }' +``` + +## 2. 创建版本发布 + +```bash +curl -X POST http://localhost:9050/api/releases \ + -H "Content-Type: application/json" \ + -d '{ + "appID": "my-app", + "version": "1.0.0", + "notes": "第一个正式版本\n- 新增功能A\n- 修复bug B", + "channel": "stable", + "pubDate": "2026-03-10T10:00:00Z" + }' +``` + +## 3. 获取所有版本列表 + +```bash +curl http://localhost:9050/api/releases +``` + +## 4. 获取指定应用的版本列表 + +```bash +curl http://localhost:9050/api/apps/my-app/releases +``` + +## 5. 按渠道筛选版本 + +```bash +# 获取 stable 渠道的版本 +curl "http://localhost:9050/api/apps/my-app/releases?channel=stable" +``` + +## 6. 获取最新版本 + +```bash +curl http://localhost:9050/api/apps/my-app/releases/latest + +# 获取指定渠道的最新版本 +curl "http://localhost:9050/api/apps/my-app/releases/latest?channel=stable" +``` + +## 7. 获取单个版本详情 + +```bash +curl http://localhost:9050/api/releases/1 +``` + +## 8. 更新版本信息 + +```bash +curl -X PUT http://localhost:9050/api/releases/1 \ + -H "Content-Type: application/json" \ + -d '{ + "version": "1.0.1", + "notes": "更新内容", + "channel": "stable", + "pubDate": "2026-03-10T12:00:00Z" + }' +``` + +## 9. 删除版本 + +```bash +curl -X DELETE http://localhost:9050/api/releases/1 +``` + +## 完整测试流程 + +```powershell +# 1. 创建应用 +Invoke-RestMethod -Method POST -Uri "http://localhost:9050/api/apps" ` + -ContentType "application/json" ` + -Body '{"id":"test-app","name":"测试应用","description":"测试用"}' + +# 2. 创建第一个版本 +Invoke-RestMethod -Method POST -Uri "http://localhost:9050/api/releases" ` + -ContentType "application/json" ` + -Body '{"appID":"test-app","version":"1.0.0","notes":"初始版本","channel":"stable","pubDate":"2026-03-10T10:00:00Z"}' + +# 3. 创建第二个版本 +Invoke-RestMethod -Method POST -Uri "http://localhost:9050/api/releases" ` + -ContentType "application/json" ` + -Body '{"appID":"test-app","version":"1.1.0","notes":"功能更新","channel":"stable","pubDate":"2026-03-10T12:00:00Z"}' + +# 4. 查看该应用的所有版本 +Invoke-RestMethod -Uri "http://localhost:9050/api/apps/test-app/releases" + +# 5. 获取最新版本 +Invoke-RestMethod -Uri "http://localhost:9050/api/apps/test-app/releases/latest" +``` + +## API 路由列表 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/releases | 获取所有版本列表 | +| GET | /api/releases/:id | 获取单个版本详情 | +| POST | /api/releases | 创建新版本 | +| PUT | /api/releases/:id | 更新版本信息 | +| DELETE | /api/releases/:id | 删除版本 | +| GET | /api/apps/:id/releases | 获取指定应用的版本列表 | +| GET | /api/apps/:id/releases/latest | 获取应用的最新版本 | + +## 响应格式 + +成功响应: +```json +{ + "code": 0, + "msg": "ok", + "data": { ... } +} +``` + +错误响应: +```json +{ + "code": 500, + "msg": "错误信息" +} +``` diff --git a/docs/UPDATE_CHECK.md b/docs/UPDATE_CHECK.md new file mode 100644 index 0000000..5912d6f --- /dev/null +++ b/docs/UPDATE_CHECK.md @@ -0,0 +1,267 @@ +# 更新检查 API 文档 + +## 🔧 修复内容 + +### 已修复的 Bug + +1. ✅ **版本比较逻辑错误** + - 之前:使用字符串比较(`"1.10.0" < "1.9.0"` 错误) + - 现在:实现语义化版本号比较(semver) + +2. ✅ **Asset 查询错误处理缺失** + - 之前:查询失败时返回空 URL + - 现在:正确处理错误并返回明确的错误信息 + +3. ✅ **参数验证缺失** + - 现在:验证所有必需参数(app, version, platform, arch) + +4. ✅ **排序逻辑优化** + - 之前:按 version 字符串排序 + - 现在:按 pub_date 排序,更准确 + +5. ✅ **响应格式统一** + - 现在使用统一的响应格式,并添加 checksum 字段 + +## 📝 API 使用 + +### 端点 + +``` +GET /api/check-update +``` + +### 请求参数 + +| 参数 | 类型 | 必需 | 说明 | +|------|------|------|------| +| app | string | ✅ | 应用 ID | +| version | string | ✅ | 当前版本号 | +| platform | string | ✅ | 平台(windows/darwin/linux) | +| arch | string | ✅ | 架构(amd64/arm64/386) | + +### 响应格式 + +**有更新:** +```json +{ + "code": 0, + "msg": "ok", + "data": { + "update": true, + "version": "1.2.0", + "notes": "更新说明\n- 新功能\n- Bug修复", + "url": "http://localhost:9050/files/releases/2/windows-amd64/app.exe", + "checksum": "sha256哈希值" + } +} +``` + +**无需更新:** +```json +{ + "code": 0, + "msg": "ok", + "data": { + "update": false + } +} +``` + +**错误响应:** +```json +{ + "code": 500, + "msg": "no asset found for this platform and architecture" +} +``` + +## 🧪 测试示例 + +### PowerShell 测试 + +```powershell +# 测试更新检查(需要更新) +$params = @{ + app = "test-app" + version = "1.0.0" + platform = "windows" + arch = "amd64" +} +$query = ($params.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join "&" +Invoke-RestMethod -Uri "http://localhost:9050/api/check-update?$query" + +# 测试更新检查(已是最新) +$params = @{ + app = "test-app" + version = "2.0.0" + platform = "windows" + arch = "amd64" +} +$query = ($params.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join "&" +Invoke-RestMethod -Uri "http://localhost:9050/api/check-update?$query" + +# 测试不存在的平台 +$params = @{ + app = "test-app" + version = "1.0.0" + platform = "windows" + arch = "arm" # 假设没有这个架构的版本 +} +$query = ($params.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join "&" +Invoke-RestMethod -Uri "http://localhost:9050/api/check-update?$query" +``` + +### curl 测试 + +```bash +# 测试更新检查 +curl "http://localhost:9050/api/check-update?app=test-app&version=1.0.0&platform=windows&arch=amd64" + +# 测试参数缺失 +curl "http://localhost:9050/api/check-update?app=test-app" +``` + +## 📦 版本号比较规则 + +实现了完整的语义化版本号(semver)比较: + +### 支持的版本格式 + +- `1.0.0` - 标准格式 +- `v1.0.0` - 带 v 前缀 +- `1.0.0-beta.1` - 预发布版本 +- `1.0.0+build.123` - 构建元数据 + +### 比较规则 + +1. **主版本号优先**:`2.0.0 > 1.9.9` +2. **次版本号次之**:`1.10.0 > 1.9.0` +3. **修订号最后**:`1.0.10 > 1.0.9` +4. **预发布版本**:`1.0.0 > 1.0.0-rc.1 > 1.0.0-beta.1 > 1.0.0-alpha.1` + +### 示例 + +```go +// pkg/util/version.go + +CompareVersion("1.10.0", "1.9.0") // 返回 1 (1.10.0 > 1.9.0) +CompareVersion("2.0.0", "1.99.99") // 返回 1 (2.0.0 > 1.99.99) +CompareVersion("1.0.0", "1.0.0-beta") // 返回 1 (正式版 > 预发布版) +CompareVersion("v1.0.0", "1.0.0") // 返回 0 (相等) + +IsNewerVersion("1.0.0", "1.1.0") // 返回 true +IsNewerVersion("1.1.0", "1.0.0") // 返回 false +``` + +## 🔄 完整更新流程 + +### 1. 准备数据 + +```powershell +# 创建应用 +Invoke-RestMethod -Method POST -Uri "http://localhost:9050/api/apps" ` + -ContentType "application/json" ` + -Body '{"id":"my-app","name":"我的应用","description":"测试"}' + +# 创建版本 1.0.0 +Invoke-RestMethod -Method POST -Uri "http://localhost:9050/api/releases" ` + -ContentType "application/json" ` + -Body '{"appID":"my-app","version":"1.0.0","notes":"初始版本","channel":"stable","pubDate":"2026-03-10T10:00:00Z"}' + +# 上传 1.0.0 的 Windows 版本 +$form = @{ + file = Get-Item -Path "D:\app-v1.0.0.exe" + platform = "windows" + arch = "amd64" +} +Invoke-RestMethod -Uri "http://localhost:9050/api/releases/1/assets/upload" -Method Post -Form $form + +# 创建版本 1.1.0 +Invoke-RestMethod -Method POST -Uri "http://localhost:9050/api/releases" ` + -ContentType "application/json" ` + -Body '{"appID":"my-app","version":"1.1.0","notes":"新增功能","channel":"stable","pubDate":"2026-03-11T10:00:00Z"}' + +# 上传 1.1.0 的 Windows 版本 +$form = @{ + file = Get-Item -Path "D:\app-v1.1.0.exe" + platform = "windows" + arch = "amd64" +} +Invoke-RestMethod -Uri "http://localhost:9050/api/releases/2/assets/upload" -Method Post -Form $form +``` + +### 2. 客户端检查更新 + +```powershell +# 用户当前使用 1.0.0,检查更新 +$response = Invoke-RestMethod -Uri "http://localhost:9050/api/check-update?app=my-app&version=1.0.0&platform=windows&arch=amd64" + +if ($response.data.update) { + Write-Host "发现新版本: $($response.data.version)" + Write-Host "更新说明: $($response.data.notes)" + Write-Host "下载地址: $($response.data.url)" + Write-Host "校验和: $($response.data.checksum)" + + # 下载更新 + Invoke-WebRequest -Uri $response.data.url -OutFile "app-update.exe" + + # 验证校验和(可选) + $hash = (Get-FileHash -Path "app-update.exe" -Algorithm SHA256).Hash + if ($hash.ToLower() -eq $response.data.checksum) { + Write-Host "✅ 文件校验通过" + } else { + Write-Host "❌ 文件校验失败" + } +} else { + Write-Host "✅ 当前已是最新版本" +} +``` + +## 🛡️ 错误处理 + +所有可能的错误情况: + +| 错误信息 | 原因 | 解决方法 | +|---------|------|---------| +| `app is required` | 缺少 app 参数 | 提供应用 ID | +| `version is required` | 缺少 version 参数 | 提供当前版本号 | +| `platform is required` | 缺少 platform 参数 | 提供平台信息 | +| `arch is required` | 缺少 arch 参数 | 提供架构信息 | +| `no release found for this app` | 应用没有发布版本 | 先创建版本发布 | +| `no asset found for this platform and architecture` | 没有对应平台的安装包 | 上传对应平台的资源 | +| `asset URL is empty` | 资源记录存在但 URL 为空 | 检查资源数据完整性 | + +## ✅ 改进总结 + +### 修复前的问题 + +```go +// ❌ 错误的版本比较 +if req.Version >= latest.Version { // "1.10.0" < "1.9.0" ! + return &CheckResponse{Update: false}, nil +} + +// ❌ 没有错误处理 +db.Where(...).First(&asset) // 忽略了错误 +return &CheckResponse{URL: asset.URL} // URL 可能为空 +``` + +### 修复后 + +```go +// ✅ 正确的版本比较 +if !util.IsNewerVersion(req.Version, latest.Version) { + return &CheckResponse{Update: false}, nil +} + +// ✅ 完整的错误处理 +err = db.Where(...).First(&ast).Error +if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("no asset found...") + } + return nil, err +} +``` + +现在更新检查功能已经完全可靠!🎉 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3026b94 --- /dev/null +++ b/go.mod @@ -0,0 +1,52 @@ +module nebula + +go 1.25.7 + +require ( + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.12.0 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/glebarez/sqlite v1.11.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.34 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gorm.io/driver/sqlite v1.6.0 // indirect + gorm.io/gorm v1.31.1 // indirect + modernc.org/libc v1.69.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.46.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b4f5401 --- /dev/null +++ b/go.sum @@ -0,0 +1,110 @@ +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8= +modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= diff --git a/internal/api/handler/app.go b/internal/api/handler/app.go new file mode 100644 index 0000000..f3d6f96 --- /dev/null +++ b/internal/api/handler/app.go @@ -0,0 +1,77 @@ +package handler + +import ( + "nebula/internal/api/response" + "nebula/internal/app" + + "github.com/gin-gonic/gin" +) + +type AppHandler struct { + service *app.AppService +} + +func NewAppHandler(service *app.AppService) *AppHandler { + return &AppHandler{service: service} +} + +func (h *AppHandler) List(c *gin.Context) { + apps, err := h.service.List() + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, apps) +} + +func (h *AppHandler) Get(c *gin.Context) { + id := c.Param("id") + app, err := h.service.Get(id) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, app) +} + +func (h *AppHandler) Create(c *gin.Context) { + var app app.App + if err := c.ShouldBindJSON(&app); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + err := h.service.Create(app) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "created") +} + +func (h *AppHandler) Update(c *gin.Context) { + id := c.Param("id") + var app app.App + if err := c.ShouldBindJSON(&app); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + err := h.service.Update(id, map[string]any{ + "name": app.Name, + "description": app.Description, + }) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "updated") +} + +func (h *AppHandler) Delete(c *gin.Context) { + id := c.Param("id") + err := h.service.Delete(id) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "deleted") +} diff --git a/internal/api/handler/asset.go b/internal/api/handler/asset.go new file mode 100644 index 0000000..3a571f5 --- /dev/null +++ b/internal/api/handler/asset.go @@ -0,0 +1,212 @@ +package handler + +import ( + "nebula/internal/api/response" + "nebula/internal/asset" + "path/filepath" + "strconv" + + "github.com/gin-gonic/gin" +) + +type AssetHandler struct { + service *asset.AssetService +} + +func NewAssetHandler(service *asset.AssetService) *AssetHandler { + return &AssetHandler{service: service} +} + +// List 获取所有资源列表 +// GET /api/assets +func (h *AssetHandler) List(c *gin.Context) { + assets, err := h.service.List() + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, assets) +} + +// ListByRelease 获取指定发布版本的所有资源 +// GET /api/releases/:id/assets +func (h *AssetHandler) ListByRelease(c *gin.Context) { + releaseID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid release id") + return + } + + assets, err := h.service.ListByRelease(uint(releaseID)) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, assets) +} + +// Get 获取单个资源详情 +// GET /api/assets/:id +func (h *AssetHandler) Get(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid asset id") + return + } + + ast, err := h.service.Get(uint(id)) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, ast) +} + +// Upload 上传文件并创建资源 +// POST /api/releases/:id/assets/upload +func (h *AssetHandler) Upload(c *gin.Context) { + releaseID, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid release id") + return + } + + // 获取表单参数 + platform := c.PostForm("platform") + arch := c.PostForm("arch") + + if platform == "" || arch == "" { + response.FailBadRequest(c, "platform and arch are required") + return + } + + // 获取上传的文件 + file, err := c.FormFile("file") + if err != nil { + response.FailBadRequest(c, "file is required") + return + } + + // 验证文件扩展名(可选) + ext := filepath.Ext(file.Filename) + allowedExts := map[string]bool{ + ".exe": true, ".dmg": true, ".pkg": true, ".deb": true, + ".rpm": true, ".appimage": true, ".zip": true, ".tar.gz": true, + ".msi": true, ".app": true, + } + if !allowedExts[ext] { + response.FailBadRequest(c, "unsupported file type: "+ext) + return + } + + // 上传文件 + ast, err := h.service.Upload(uint(releaseID), platform, arch, file) + if err != nil { + response.FailServer(c, err.Error()) + return + } + + response.Ok(c, ast) +} + +// Create 创建资源记录(用于外部 URL) +// POST /api/assets +func (h *AssetHandler) Create(c *gin.Context) { + var ast asset.Asset + if err := c.ShouldBindJSON(&ast); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + + // 基本验证 + if ast.ReleaseID == 0 { + response.FailBadRequest(c, "releaseId is required") + return + } + if ast.Platform == "" { + response.FailBadRequest(c, "platform is required") + return + } + if ast.Arch == "" { + response.FailBadRequest(c, "arch is required") + return + } + if ast.URL == "" { + response.FailBadRequest(c, "url is required") + return + } + + err := h.service.Create(ast) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "created") +} + +// Update 更新资源信息 +// PUT /api/assets/:id +func (h *AssetHandler) Update(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid asset id") + return + } + + var ast asset.Asset + if err := c.ShouldBindJSON(&ast); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + + data := map[string]any{ + "platform": ast.Platform, + "arch": ast.Arch, + "signature": ast.Signature, + "checksum": ast.Checksum, + } + + err = h.service.Update(uint(id), data) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "updated") +} + +// Delete 删除资源 +// DELETE /api/assets/:id +func (h *AssetHandler) Delete(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid asset id") + return + } + + err = h.service.Delete(uint(id)) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "deleted") +} + +// Download 下载资源文件 +// GET /api/assets/:id/download +func (h *AssetHandler) Download(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid asset id") + return + } + + // 获取存储路径 + storagePath, err := h.service.GetStoragePath(uint(id)) + if err != nil { + response.FailServer(c, err.Error()) + return + } + + // 返回文件(Gin 会自动处理文件下载) + c.File(storagePath) +} diff --git a/internal/api/handler/auth.go b/internal/api/handler/auth.go new file mode 100644 index 0000000..db5ae77 --- /dev/null +++ b/internal/api/handler/auth.go @@ -0,0 +1,124 @@ +package handler + +import ( + "nebula/internal/api/response" + "nebula/internal/auth" + + "github.com/gin-gonic/gin" +) + +type AuthHandler struct { + service *auth.AuthService +} + +func NewAuthHandler(service *auth.AuthService) *AuthHandler { + return &AuthHandler{service: service} +} + +// Register 用户注册 +// POST /api/auth/register +func (h *AuthHandler) Register(c *gin.Context) { + var req auth.RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + + user, tokens, err := h.service.Register(req) + if err != nil { + response.FailServer(c, err.Error()) + return + } + + response.Ok(c, gin.H{ + "user": user, + "tokens": tokens, + }) +} + +// Login 用户登录 +// POST /api/auth/login +func (h *AuthHandler) Login(c *gin.Context) { + var req auth.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + + user, tokens, err := h.service.Login(req) + if err != nil { + response.Fail(c, 401, err.Error()) + return + } + + response.Ok(c, gin.H{ + "user": user, + "tokens": tokens, + }) +} + +// RefreshToken 刷新访问令牌 +// POST /api/auth/refresh +func (h *AuthHandler) RefreshToken(c *gin.Context) { + var req auth.RefreshTokenRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + + tokens, err := h.service.RefreshToken(req) + if err != nil { + response.Fail(c, 401, err.Error()) + return + } + + response.Ok(c, tokens) +} + +// GetProfile 获取当前用户信息 +// GET /api/auth/profile +func (h *AuthHandler) GetProfile(c *gin.Context) { + userID, exists := auth.GetCurrentUserID(c) + if !exists { + response.Fail(c, 401, "unauthorized") + return + } + + user, err := h.service.GetUserByID(userID) + if err != nil { + response.FailServer(c, err.Error()) + return + } + + response.Ok(c, user) +} + +// ChangePasswordRequest 修改密码请求 +type ChangePasswordRequest struct { + OldPassword string `json:"oldPassword" binding:"required"` + NewPassword string `json:"newPassword" binding:"required,min=6"` +} + +// ChangePassword 修改密码 +// POST /api/auth/change-password +func (h *AuthHandler) ChangePassword(c *gin.Context) { + userID, exists := auth.GetCurrentUserID(c) + if !exists { + response.Fail(c, 401, "unauthorized") + return + } + + var req ChangePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + + err := h.service.ChangePassword(userID, req.OldPassword, req.NewPassword) + if err != nil { + response.FailServer(c, err.Error()) + return + } + + response.OkMsg(c, "password changed successfully") +} diff --git a/internal/api/handler/handle.go b/internal/api/handler/handle.go new file mode 100644 index 0000000..56de9cc --- /dev/null +++ b/internal/api/handler/handle.go @@ -0,0 +1,11 @@ +package handler + +import "gorm.io/gorm" + +type Handler struct { + db *gorm.DB +} + +func New(db *gorm.DB) *Handler { + return &Handler{db: db} +} diff --git a/internal/api/handler/release.go b/internal/api/handler/release.go new file mode 100644 index 0000000..0e0f607 --- /dev/null +++ b/internal/api/handler/release.go @@ -0,0 +1,155 @@ +package handler + +import ( + "nebula/internal/api/response" + "nebula/internal/release" + "strconv" + + "github.com/gin-gonic/gin" +) + +type ReleaseHandler struct { + service *release.ReleaseService +} + +func NewReleaseHandler(service *release.ReleaseService) *ReleaseHandler { + return &ReleaseHandler{service: service} +} + +// List 获取所有版本列表 +// GET /api/releases +func (h *ReleaseHandler) List(c *gin.Context) { + releases, err := h.service.List() + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, releases) +} + +// ListByApp 获取指定应用的所有版本 +// GET /api/apps/:id/releases +func (h *ReleaseHandler) ListByApp(c *gin.Context) { + appID := c.Param("id") + channel := c.Query("channel") // 可选的渠道过滤 + + var releases []release.Release + var err error + + if channel != "" { + releases, err = h.service.ListByAppAndChannel(appID, channel) + } else { + releases, err = h.service.ListByApp(appID) + } + + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, releases) +} + +// Get 获取单个版本详情 +// GET /api/releases/:id +func (h *ReleaseHandler) Get(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid release id") + return + } + + rel, err := h.service.Get(uint(id)) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, rel) +} + +// GetLatest 获取应用的最新版本 +// GET /api/apps/:id/releases/latest +func (h *ReleaseHandler) GetLatest(c *gin.Context) { + appID := c.Param("id") + channel := c.Query("channel") + + rel, err := h.service.GetLatest(appID, channel) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.Ok(c, rel) +} + +// Create 创建新版本 +// POST /api/releases +func (h *ReleaseHandler) Create(c *gin.Context) { + var rel release.Release + if err := c.ShouldBindJSON(&rel); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + + // 基本验证 + if rel.AppID == "" { + response.FailBadRequest(c, "app_id is required") + return + } + if rel.Version == "" { + response.FailBadRequest(c, "version is required") + return + } + + err := h.service.Create(rel) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "created") +} + +// Update 更新版本信息 +// PUT /api/releases/:id +func (h *ReleaseHandler) Update(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid release id") + return + } + + var rel release.Release + if err := c.ShouldBindJSON(&rel); err != nil { + response.FailBadRequest(c, err.Error()) + return + } + + data := map[string]any{ + "version": rel.Version, + "notes": rel.Notes, + "channel": rel.Channel, + "pub_date": rel.PubDate, + } + + err = h.service.Update(uint(id), data) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "updated") +} + +// Delete 删除版本 +// DELETE /api/releases/:id +func (h *ReleaseHandler) Delete(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 32) + if err != nil { + response.FailBadRequest(c, "invalid release id") + return + } + + err = h.service.Delete(uint(id)) + if err != nil { + response.FailServer(c, err.Error()) + return + } + response.OkMsg(c, "deleted") +} diff --git a/internal/api/handler/update.go b/internal/api/handler/update.go new file mode 100644 index 0000000..25e36c7 --- /dev/null +++ b/internal/api/handler/update.go @@ -0,0 +1,26 @@ +package handler + +import ( + "nebula/internal/api/response" + "nebula/internal/updater" + + "github.com/gin-gonic/gin" +) + +func (h *Handler) CheckUpdate(c *gin.Context) { + req := updater.CheckRequest{ + App: c.Query("app"), + Version: c.Query("version"), + Platform: c.Query("platform"), + Arch: c.Query("arch"), + } + + resp, err := updater.CheckUpdate(h.db, req) + + if err != nil { + response.FailServer(c, err.Error()) + return + } + + response.Ok(c, resp) +} diff --git a/internal/api/response/response.go b/internal/api/response/response.go new file mode 100644 index 0000000..c48311f --- /dev/null +++ b/internal/api/response/response.go @@ -0,0 +1,47 @@ +package response + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Result 通用 REST 返回结构 +type Result struct { + Code int `json:"code"` + Message string `json:"message"` + Data any `json:"data,omitempty"` +} + +// 常用业务码 +const ( + CodeOk = 0 + CodeFail = 1 + CodeInvalid = 400 + CodeServer = 500 +) + +// Ok 成功,带数据 +func Ok(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Result{Code: CodeOk, Message: "success", Data: data}) +} + +// OkMsg 成功,自定义消息,无数据 +func OkMsg(c *gin.Context, message string) { + c.JSON(http.StatusOK, Result{Code: CodeOk, Message: message}) +} + +// Fail 失败,业务码 + 消息 +func Fail(c *gin.Context, code int, message string) { + c.JSON(http.StatusOK, Result{Code: code, Message: message}) +} + +// FailBadRequest 参数错误 (HTTP 400) +func FailBadRequest(c *gin.Context, message string) { + c.JSON(http.StatusBadRequest, Result{Code: CodeInvalid, Message: message}) +} + +// FailServer 服务端错误 (HTTP 500) +func FailServer(c *gin.Context, message string) { + c.JSON(http.StatusInternalServerError, Result{Code: CodeServer, Message: message}) +} diff --git a/internal/api/router.go b/internal/api/router.go new file mode 100644 index 0000000..ed17554 --- /dev/null +++ b/internal/api/router.go @@ -0,0 +1,88 @@ +package api + +import ( + "nebula/internal/api/handler" + "nebula/internal/app" + "nebula/internal/asset" + "nebula/internal/auth" + "nebula/internal/release" + "nebula/internal/storage" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func RegisterRoutes(r *gin.Engine, db *gorm.DB, stor storage.Storage, jwtService *auth.JWTService, authService *auth.AuthService) { + h := handler.New(db) + appHandler := handler.NewAppHandler(app.NewService(db)) + releaseHandler := handler.NewReleaseHandler(release.NewService(db)) + assetHandler := handler.NewAssetHandler(asset.NewService(db, stor)) + authHandler := handler.NewAuthHandler(authService) + + api := r.Group("/api") + + { + // 认证相关路由(公开) + authGroup := api.Group("/auth") + { + // 移除注册接口 - 只允许配置的管理员登录 + // authGroup.POST("/register", authHandler.Register) + authGroup.POST("/login", authHandler.Login) + authGroup.POST("/refresh", authHandler.RefreshToken) + } + + // 需要认证的路由 + authRequired := api.Group("") + authRequired.Use(auth.JWTMiddleware(jwtService)) + { + // 用户相关 + authRequired.GET("/auth/profile", authHandler.GetProfile) + authRequired.POST("/auth/change-password", authHandler.ChangePassword) + } + + // 更新检查(公开) + api.GET("/check-update", h.CheckUpdate) + + // 应用管理(需要认证) + appGroup := api.Group("/apps") + appGroup.Use(auth.JWTMiddleware(jwtService)) + { + appGroup.GET("", appHandler.List) + appGroup.POST("", appHandler.Create) + appGroup.GET("/:id", appHandler.Get) + appGroup.PUT("/:id", appHandler.Update) + appGroup.DELETE("/:id", appHandler.Delete) + + // 应用相关的版本 + appGroup.GET("/:id/releases", releaseHandler.ListByApp) + appGroup.GET("/:id/releases/latest", releaseHandler.GetLatest) + } + + // 版本管理(需要认证) + releaseGroup := api.Group("/releases") + releaseGroup.Use(auth.JWTMiddleware(jwtService)) + { + releaseGroup.GET("", releaseHandler.List) + releaseGroup.POST("", releaseHandler.Create) + releaseGroup.GET("/:id", releaseHandler.Get) + releaseGroup.PUT("/:id", releaseHandler.Update) + releaseGroup.DELETE("/:id", releaseHandler.Delete) + + // 版本相关的资源 + releaseGroup.GET("/:id/assets", assetHandler.ListByRelease) + releaseGroup.POST("/:id/assets/upload", assetHandler.Upload) + } + + // 资源管理(需要认证) + assetGroup := api.Group("/assets") + assetGroup.Use(auth.JWTMiddleware(jwtService)) + { + assetGroup.GET("", assetHandler.List) + assetGroup.GET("/:id", assetHandler.Get) + assetGroup.POST("", assetHandler.Create) + assetGroup.PUT("/:id", assetHandler.Update) + assetGroup.DELETE("/:id", assetHandler.Delete) + assetGroup.GET("/:id/download", assetHandler.Download) + } + } +} diff --git a/internal/app/model.go b/internal/app/model.go new file mode 100644 index 0000000..b0dae16 --- /dev/null +++ b/internal/app/model.go @@ -0,0 +1,11 @@ +package app + +import "nebula/types" + +type App struct { + ID string `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` + Description string `json:"description"` + CreatedAt types.JSONTime `json:"createdAt"` + UpdatedAt types.JSONTime `json:"updatedAt"` +} diff --git a/internal/app/service.go b/internal/app/service.go new file mode 100644 index 0000000..35b3c6e --- /dev/null +++ b/internal/app/service.go @@ -0,0 +1,38 @@ +package app + +import "gorm.io/gorm" + +type AppService struct { + db *gorm.DB +} + +func NewService(db *gorm.DB) *AppService { + return &AppService{db: db} +} + +func (s *AppService) List() ([]App, error) { + var apps []App + err := s.db.Find(&apps).Error + return apps, err +} + +func (s *AppService) Get(id string) (*App, error) { + var app App + err := s.db.First(&app, "id = ?", id).Error + if err != nil { + return nil, err + } + return &app, nil +} + +func (s *AppService) Create(app App) error { + return s.db.Create(&app).Error +} + +func (s *AppService) Update(id string, data map[string]any) error { + return s.db.Model(&App{}).Where("id = ?", id).Updates(data).Error +} + +func (s *AppService) Delete(id string) error { + return s.db.Delete(&App{}, "id = ?", id).Error +} diff --git a/internal/asset/model.go b/internal/asset/model.go new file mode 100644 index 0000000..d59f7f7 --- /dev/null +++ b/internal/asset/model.go @@ -0,0 +1,18 @@ +package asset + +import "time" + +type Asset struct { + ID uint `gorm:"primaryKey" json:"id"` + ReleaseID uint `gorm:"not null" json:"releaseId"` + Platform string `json:"platform"` + Arch string `json:"arch"` + StoragePath string `json:"-"` // 存储路径,不在 API 中返回 + + URL string `gorm:"not null" json:"url"` + Signature string `json:"signature"` + Checksum string `json:"checksum"` + + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} diff --git a/internal/asset/service.go b/internal/asset/service.go new file mode 100644 index 0000000..2776752 --- /dev/null +++ b/internal/asset/service.go @@ -0,0 +1,255 @@ +package asset + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "mime/multipart" + "nebula/internal/storage" + + "gorm.io/gorm" +) + +type AssetService struct { + db *gorm.DB + storage storage.Storage +} + +func NewService(db *gorm.DB, storage storage.Storage) *AssetService { + return &AssetService{ + db: db, + storage: storage, + } +} + +// List 获取所有资源列表 +func (s *AssetService) List() ([]Asset, error) { + var assets []Asset + err := s.db.Order("created_at DESC").Find(&assets).Error + return assets, err +} + +// ListByRelease 获取指定发布版本的所有资源 +func (s *AssetService) ListByRelease(releaseID uint) ([]Asset, error) { + var assets []Asset + err := s.db.Where("release_id = ?", releaseID).Find(&assets).Error + return assets, err +} + +// Get 获取单个资源详情 +func (s *AssetService) Get(id uint) (*Asset, error) { + var asset Asset + err := s.db.First(&asset, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("asset not found") + } + return nil, err + } + return &asset, nil +} + +// GetByReleaseAndPlatform 根据发布版本、平台和架构获取资源 +func (s *AssetService) GetByReleaseAndPlatform(releaseID uint, platform, arch string) (*Asset, error) { + var asset Asset + err := s.db.Where("release_id = ? AND platform = ? AND arch = ?", releaseID, platform, arch). + First(&asset).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("asset not found for this platform and architecture") + } + return nil, err + } + return &asset, nil +} + +// Upload 上传文件并创建资源记录 +func (s *AssetService) Upload(releaseID uint, platform, arch string, file *multipart.FileHeader) (*Asset, error) { + // 检查 Release 是否存在 + var count int64 + err := s.db.Table("releases").Where("id = ?", releaseID).Count(&count).Error + if err != nil { + return nil, err + } + if count == 0 { + return nil, errors.New("release not found") + } + + // 检查是否已存在相同平台和架构的资源 + var existing Asset + err = s.db.Where("release_id = ? AND platform = ? AND arch = ?", releaseID, platform, arch). + First(&existing).Error + if err == nil { + return nil, errors.New("asset already exists for this platform and architecture") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + // 打开上传的文件 + src, err := file.Open() + if err != nil { + return nil, fmt.Errorf("failed to open uploaded file: %w", err) + } + defer src.Close() + + // 计算文件校验和 + hash := sha256.New() + if _, err := io.Copy(hash, src); err != nil { + return nil, fmt.Errorf("failed to calculate checksum: %w", err) + } + checksum := hex.EncodeToString(hash.Sum(nil)) + + // 重新打开文件用于保存(因为已经读取过了) + src.Close() + src, err = file.Open() + if err != nil { + return nil, fmt.Errorf("failed to reopen file: %w", err) + } + defer src.Close() + + // 构建存储路径: releases/{releaseID}/{platform}-{arch}/{filename} + storagePath := fmt.Sprintf("releases/%d/%s-%s/%s", releaseID, platform, arch, file.Filename) + + // 保存文件 + savedPath, err := s.storage.Save(storagePath, src) + if err != nil { + return nil, fmt.Errorf("failed to save file: %w", err) + } + + // 获取文件访问 URL + url := s.storage.GetURL(savedPath) + + // 创建数据库记录 + asset := Asset{ + ReleaseID: releaseID, + Platform: platform, + Arch: arch, + URL: url, + StoragePath: savedPath, + Checksum: checksum, + Signature: "", // TODO: 实现文件签名 + } + + if err := s.db.Create(&asset).Error; err != nil { + // 如果数据库操作失败,删除已上传的文件 + s.storage.Delete(savedPath) + return nil, err + } + + return &asset, nil +} + +// Create 创建资源记录(用于外部URL) +func (s *AssetService) Create(asset Asset) error { + // 检查 Release 是否存在 + var count int64 + err := s.db.Table("releases").Where("id = ?", asset.ReleaseID).Count(&count).Error + if err != nil { + return err + } + if count == 0 { + return errors.New("release not found") + } + + // 检查是否已存在相同平台和架构的资源 + err = s.db.Where("release_id = ? AND platform = ? AND arch = ?", asset.ReleaseID, asset.Platform, asset.Arch). + First(&Asset{}).Error + if err == nil { + return errors.New("asset already exists for this platform and architecture") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + return s.db.Create(&asset).Error +} + +// Update 更新资源信息(不包括文件) +func (s *AssetService) Update(id uint, data map[string]any) error { + // 检查资源是否存在 + var asset Asset + err := s.db.First(&asset, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("asset not found") + } + return err + } + + // 如果更新平台或架构,检查是否会造成重复 + newPlatform, hasPlatform := data["platform"].(string) + newArch, hasArch := data["arch"].(string) + + if hasPlatform || hasArch { + checkPlatform := asset.Platform + checkArch := asset.Arch + if hasPlatform { + checkPlatform = newPlatform + } + if hasArch { + checkArch = newArch + } + + err := s.db.Where("release_id = ? AND platform = ? AND arch = ? AND id != ?", + asset.ReleaseID, checkPlatform, checkArch, id). + First(&Asset{}).Error + if err == nil { + return errors.New("asset already exists for this platform and architecture") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + } + + return s.db.Model(&Asset{}).Where("id = ?", id).Updates(data).Error +} + +// Delete 删除资源(包括文件) +func (s *AssetService) Delete(id uint) error { + // 获取资源信息 + var asset Asset + err := s.db.First(&asset, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("asset not found") + } + return err + } + + // 删除存储的文件 + if asset.StoragePath != "" { + if err := s.storage.Delete(asset.StoragePath); err != nil { + // 记录错误但继续删除数据库记录 + // TODO: 添加日志 + } + } + + // 删除数据库记录 + return s.db.Delete(&Asset{}, id).Error +} + +// GetStoragePath 获取资源的存储路径 +func (s *AssetService) GetStoragePath(id uint) (string, error) { + var asset Asset + err := s.db.First(&asset, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", errors.New("asset not found") + } + return "", err + } + + if asset.StoragePath == "" { + return "", errors.New("storage path not available") + } + + // 如果是本地存储,返回完整路径 + if localStorage, ok := s.storage.(*storage.LocalStorage); ok { + return localStorage.GetFullPath(asset.StoragePath), nil + } + + return asset.StoragePath, nil +} diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go new file mode 100644 index 0000000..3ef9450 --- /dev/null +++ b/internal/auth/jwt.go @@ -0,0 +1,131 @@ +package auth + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// Claims JWT 声明 +type Claims struct { + UserID string `json:"userId"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +// TokenPair 访问令牌和刷新令牌 +type TokenPair struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + ExpiresIn int64 `json:"expiresIn"` // 秒 +} + +// JWTService JWT 服务 +type JWTService struct { + secret string + accessTokenDuration time.Duration + refreshTokenDuration time.Duration +} + +// NewJWTService 创建 JWT 服务 +func NewJWTService(secret string, accessTokenDuration, refreshTokenDuration time.Duration) *JWTService { + return &JWTService{ + secret: secret, + accessTokenDuration: accessTokenDuration, + refreshTokenDuration: refreshTokenDuration, + } +} + +// GenerateTokenPair 生成访问令牌和刷新令牌 +func (s *JWTService) GenerateTokenPair(userID, username, role string) (*TokenPair, error) { + // 生成访问令牌 + accessToken, err := s.generateToken(userID, username, role, s.accessTokenDuration) + if err != nil { + return nil, err + } + + // 生成刷新令牌(更长的过期时间,不包含敏感信息) + refreshToken, err := s.generateToken(userID, "", "", s.refreshTokenDuration) + if err != nil { + return nil, err + } + + return &TokenPair{ + AccessToken: accessToken, + RefreshToken: refreshToken, + ExpiresIn: int64(s.accessTokenDuration.Seconds()), + }, nil +} + +// generateToken 生成 token +func (s *JWTService) generateToken(userID, username, role string, duration time.Duration) (string, error) { + now := time.Now() + claims := Claims{ + UserID: userID, + Username: username, + Role: role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(duration)), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(s.secret)) +} + +// ParseToken 解析 token +func (s *JWTService) ParseToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + // 验证签名算法 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return []byte(s.secret), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} + +// ValidateToken 验证 token 是否有效 +func (s *JWTService) ValidateToken(tokenString string) (*Claims, error) { + claims, err := s.ParseToken(tokenString) + if err != nil { + return nil, err + } + + // 检查是否过期 + if time.Now().Unix() > claims.ExpiresAt.Unix() { + return nil, errors.New("token expired") + } + + return claims, nil +} + +// RefreshAccessToken 使用刷新令牌生成新的访问令牌 +func (s *JWTService) RefreshAccessToken(refreshToken string, user *User) (*TokenPair, error) { + // 验证刷新令牌 + claims, err := s.ValidateToken(refreshToken) + if err != nil { + return nil, errors.New("invalid refresh token") + } + + // 确保刷新令牌属于该用户 + if claims.UserID != user.ID { + return nil, errors.New("token does not match user") + } + + // 生成新的 token 对 + return s.GenerateTokenPair(user.ID, user.Username, user.Role) +} diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go new file mode 100644 index 0000000..2dc9695 --- /dev/null +++ b/internal/auth/middleware.go @@ -0,0 +1,104 @@ +package auth + +import ( + "nebula/internal/api/response" + "strings" + + "github.com/gin-gonic/gin" +) + +// JWTMiddleware JWT 认证中间件 +func JWTMiddleware(jwtService *JWTService) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + response.Fail(c, 401, "missing authorization header") + c.Abort() + return + } + + // 解析 Bearer token + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + response.Fail(c, 401, "invalid authorization header format") + c.Abort() + return + } + + tokenString := parts[1] + + // 验证 token + claims, err := jwtService.ValidateToken(tokenString) + if err != nil { + response.Fail(c, 401, "invalid or expired token") + c.Abort() + return + } + + // 将用户信息保存到上下文 + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + + c.Next() + } +} + +// OptionalJWTMiddleware 可选的 JWT 中间件(token 无效不报错) +func OptionalJWTMiddleware(jwtService *JWTService) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader != "" { + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) == 2 && parts[0] == "Bearer" { + tokenString := parts[1] + if claims, err := jwtService.ValidateToken(tokenString); err == nil { + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + } + } + } + c.Next() + } +} + +// AdminMiddleware 管理员权限中间件(需要先使用 JWTMiddleware) +func AdminMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + role, exists := c.Get("role") + if !exists || role != "admin" { + response.Fail(c, 403, "admin access required") + c.Abort() + return + } + c.Next() + } +} + +// GetCurrentUserID 从上下文获取当前用户 ID +func GetCurrentUserID(c *gin.Context) (string, bool) { + userID, exists := c.Get("user_id") + if !exists { + return "", false + } + return userID.(string), true +} + +// GetCurrentUsername 从上下文获取当前用户名 +func GetCurrentUsername(c *gin.Context) (string, bool) { + username, exists := c.Get("username") + if !exists { + return "", false + } + return username.(string), true +} + +// GetCurrentUserRole 从上下文获取当前用户角色 +func GetCurrentUserRole(c *gin.Context) (string, bool) { + role, exists := c.Get("role") + if !exists { + return "", false + } + return role.(string), true +} diff --git a/internal/auth/model.go b/internal/auth/model.go new file mode 100644 index 0000000..06adffd --- /dev/null +++ b/internal/auth/model.go @@ -0,0 +1,29 @@ +package auth + +import ( + "time" + + "golang.org/x/crypto/bcrypt" +) + +type User struct { + ID string `gorm:"primaryKey" json:"id"` + Username string `gorm:"uniqueIndex;not null" json:"username"` + Email string `gorm:"uniqueIndex;not null" json:"email"` + Password string `gorm:"not null" json:"-"` // 不在 JSON 中返回 + Role string `gorm:"default:'user'" json:"role"` // user, admin + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// HashPassword 加密密码 +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +// CheckPassword 验证密码 +func (u *User) CheckPassword(password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) + return err == nil +} diff --git a/internal/auth/service.go b/internal/auth/service.go new file mode 100644 index 0000000..0f12e7d --- /dev/null +++ b/internal/auth/service.go @@ -0,0 +1,209 @@ +package auth + +import ( + "errors" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type AuthService struct { + db *gorm.DB + jwtService *JWTService + adminUsername string + adminPassword string +} + +func NewAuthService(db *gorm.DB, jwtService *JWTService, adminUsername, adminPassword string) *AuthService { + return &AuthService{ + db: db, + jwtService: jwtService, + adminUsername: adminUsername, + adminPassword: adminPassword, + } +} + +// RegisterRequest 注册请求 +type RegisterRequest struct { + Username string `json:"username" binding:"required,min=3,max=20"` + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` +} + +// LoginRequest 登录请求 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// RefreshTokenRequest 刷新令牌请求 +type RefreshTokenRequest struct { + RefreshToken string `json:"refreshToken" binding:"required"` +} + +// Register 用户注册 +func (s *AuthService) Register(req RegisterRequest) (*User, *TokenPair, error) { + // 检查用户名是否已存在 + var existingUser User + err := s.db.Where("username = ?", req.Username).First(&existingUser).Error + if err == nil { + return nil, nil, errors.New("username already exists") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, err + } + + // 检查邮箱是否已存在 + err = s.db.Where("email = ?", req.Email).First(&existingUser).Error + if err == nil { + return nil, nil, errors.New("email already exists") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, err + } + + // 加密密码 + hashedPassword, err := HashPassword(req.Password) + if err != nil { + return nil, nil, err + } + + // 创建用户 + user := User{ + ID: uuid.New().String(), + Username: req.Username, + Email: req.Email, + Password: hashedPassword, + Role: "user", // 默认角色 + } + + if err := s.db.Create(&user).Error; err != nil { + return nil, nil, err + } + + // 生成 token + tokens, err := s.jwtService.GenerateTokenPair(user.ID, user.Username, user.Role) + if err != nil { + return nil, nil, err + } + + return &user, tokens, nil +} + +// Login 用户登录 +func (s *AuthService) Login(req LoginRequest) (*User, *TokenPair, error) { + // 1. 先检查是否为配置的管理员账号 + if req.Username == s.adminUsername && req.Password == s.adminPassword { + // 管理员登录成功,创建虚拟用户对象 + adminUser := &User{ + ID: "admin", + Username: s.adminUsername, + Email: s.adminUsername + "@admin.local", + Role: "admin", + } + + // 生成 token + tokens, err := s.jwtService.GenerateTokenPair(adminUser.ID, adminUser.Username, adminUser.Role) + if err != nil { + return nil, nil, err + } + + return adminUser, tokens, nil + } + + // 2. 如果不是管理员,查找数据库中的用户 + var user User + err := s.db.Where("username = ? OR email = ?", req.Username, req.Username).First(&user).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, errors.New("invalid username or password") + } + return nil, nil, err + } + + // 验证密码 + if !user.CheckPassword(req.Password) { + return nil, nil, errors.New("invalid username or password") + } + + // 生成 token + tokens, err := s.jwtService.GenerateTokenPair(user.ID, user.Username, user.Role) + if err != nil { + return nil, nil, err + } + + return &user, tokens, nil +} + +// RefreshToken 刷新访问令牌 +func (s *AuthService) RefreshToken(req RefreshTokenRequest) (*TokenPair, error) { + // 解析刷新令牌获取用户 ID + claims, err := s.jwtService.ParseToken(req.RefreshToken) + if err != nil { + return nil, errors.New("invalid refresh token") + } + + // 获取用户信息 + var user User + err = s.db.First(&user, "id = ?", claims.UserID).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("user not found") + } + return nil, err + } + + // 使用刷新令牌生成新的 token 对 + tokens, err := s.jwtService.RefreshAccessToken(req.RefreshToken, &user) + if err != nil { + return nil, err + } + + return tokens, nil +} + +// GetUserByID 根据 ID 获取用户 +func (s *AuthService) GetUserByID(userID string) (*User, error) { + // 特殊处理管理员 + if userID == "admin" { + return &User{ + ID: "admin", + Username: s.adminUsername, + Email: s.adminUsername + "@admin.local", + Role: "admin", + }, nil + } + + var user User + err := s.db.First(&user, "id = ?", userID).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("user not found") + } + return nil, err + } + return &user, nil +} + +// ChangePassword 修改密码 +func (s *AuthService) ChangePassword(userID, oldPassword, newPassword string) error { + var user User + err := s.db.First(&user, "id = ?", userID).Error + if err != nil { + return errors.New("user not found") + } + + // 验证旧密码 + if !user.CheckPassword(oldPassword) { + return errors.New("invalid old password") + } + + // 加密新密码 + hashedPassword, err := HashPassword(newPassword) + if err != nil { + return err + } + + // 更新密码 + return s.db.Model(&user).Update("password", hashedPassword).Error +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..52df024 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,154 @@ +package config + +import ( + "fmt" + "os" + "time" + + "github.com/goccy/go-yaml" +) + +type Config struct { + Server struct { + Address string `yaml:"address"` + Mode string `yaml:"mode"` // dev, prod + } `yaml:"server"` + Database struct { + DSN string `yaml:"dsn"` + } `yaml:"database"` + Storage struct { + Type string `yaml:"type"` // local, oss, s3 + BasePath string `yaml:"basePath"` // 本地存储的基础路径 + BaseURL string `yaml:"baseUrl"` // 文件访问的 URL 前缀 + } `yaml:"storage"` + JWT struct { + Secret string `yaml:"secret"` + AccessTokenDuration int `yaml:"accessTokenDuration"` // 秒 + RefreshTokenDuration int `yaml:"refreshTokenDuration"` // 秒 + } `yaml:"jwt"` + Frontend struct { + Enabled bool `yaml:"enabled"` // 是否启用前端静态资源服务 + Path string `yaml:"path"` // 前端静态资源路径 + } `yaml:"frontend"` + Admin struct { + Username string `yaml:"username"` // 管理员用户名 + Password string `yaml:"password"` // 管理员密码 + } `yaml:"admin"` +} + +func Load() *Config { + // 确定配置文件路径 + configFile := getConfigFile() + + // 读取配置文件 + data, err := os.ReadFile(configFile) + if err != nil { + fmt.Printf("Warning: failed to read config file %s: %v, using defaults\n", configFile, err) + return loadDefaults() + } + + // 解析 YAML + config := &Config{} + if err := yaml.Unmarshal(data, config); err != nil { + fmt.Printf("Warning: failed to parse config file %s: %v, using defaults\n", configFile, err) + return loadDefaults() + } + + // 环境变量覆盖 + applyEnvOverrides(config) + + // 自动设置 frontend.enabled(生产模式才启用) + if config.Server.Mode == "prod" && !config.Frontend.Enabled { + config.Frontend.Enabled = true + } + + return config +} + +func getConfigFile() string { + // 1. 优先使用环境变量指定的配置文件 + if configFile := os.Getenv("CONFIG_FILE"); configFile != "" { + return configFile + } + + // 2. 根据 SERVER_MODE 选择配置文件 + mode := os.Getenv("SERVER_MODE") + if mode == "" { + mode = "dev" + } + + // 检查 config.{mode}.yaml + modeConfig := fmt.Sprintf("config.%s.yaml", mode) + if _, err := os.Stat(modeConfig); err == nil { + return modeConfig + } + + // 3. 使用默认 config.yaml + if _, err := os.Stat("config.yaml"); err == nil { + return "config.yaml" + } + + // 4. 都不存在,使用默认配置 + return "" +} + +func loadDefaults() *Config { + config := &Config{} + config.Server.Address = ":9050" + config.Server.Mode = "dev" + config.Database.DSN = "nebula.db" + config.Storage.Type = "local" + config.Storage.BasePath = "./uploads" + config.Storage.BaseURL = "http://localhost:9050/files" + config.JWT.Secret = "dev-secret-key-change-in-production" + config.JWT.AccessTokenDuration = 7200 // 2 hours + config.JWT.RefreshTokenDuration = 604800 // 7 days + config.Frontend.Enabled = false + config.Frontend.Path = "./web/dist" + config.Admin.Username = "admin" + config.Admin.Password = "admin123" // 默认密码,生产环境必须修改 + return config +} + +func applyEnvOverrides(config *Config) { + if val := os.Getenv("SERVER_ADDRESS"); val != "" { + config.Server.Address = val + } + if val := os.Getenv("SERVER_MODE"); val != "" { + config.Server.Mode = val + } + if val := os.Getenv("DATABASE_DSN"); val != "" { + config.Database.DSN = val + } + if val := os.Getenv("STORAGE_TYPE"); val != "" { + config.Storage.Type = val + } + if val := os.Getenv("STORAGE_BASE_PATH"); val != "" { + config.Storage.BasePath = val + } + if val := os.Getenv("STORAGE_BASE_URL"); val != "" { + config.Storage.BaseURL = val + } + if val := os.Getenv("JWT_SECRET"); val != "" { + config.JWT.Secret = val + } + if val := os.Getenv("FRONTEND_PATH"); val != "" { + config.Frontend.Path = val + } + if val := os.Getenv("ADMIN_USERNAME"); val != "" { + config.Admin.Username = val + } + if val := os.Getenv("ADMIN_PASSWORD"); val != "" { + config.Admin.Password = val + } +} + +// GetAccessTokenDuration 返回 Access Token 有效期 +func (c *Config) GetAccessTokenDuration() time.Duration { + return time.Duration(c.JWT.AccessTokenDuration) * time.Second +} + +// GetRefreshTokenDuration 返回 Refresh Token 有效期 +func (c *Config) GetRefreshTokenDuration() time.Duration { + return time.Duration(c.JWT.RefreshTokenDuration) * time.Second +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..bbb4d3c --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,27 @@ +package db + +import ( + "nebula/internal/app" + "nebula/internal/asset" + "nebula/internal/auth" + "nebula/internal/release" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +func Init(dsn string) *gorm.DB { + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + if err != nil { + panic(err) + } + + db.AutoMigrate( + &app.App{}, + &release.Release{}, + &asset.Asset{}, + &auth.User{}, + ) + + return db +} diff --git a/internal/release/model.go b/internal/release/model.go new file mode 100644 index 0000000..19780da --- /dev/null +++ b/internal/release/model.go @@ -0,0 +1,14 @@ +package release + +import "time" + +type Release struct { + ID uint `gorm:"primaryKey"` + AppID string `gorm:"not null"` + Version string `gorm:"not null"` + Notes string + Channel string + PubDate time.Time + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/release/service.go b/internal/release/service.go new file mode 100644 index 0000000..99432f6 --- /dev/null +++ b/internal/release/service.go @@ -0,0 +1,141 @@ +package release + +import ( + "errors" + + "gorm.io/gorm" +) + +type ReleaseService struct { + db *gorm.DB +} + +func NewService(db *gorm.DB) *ReleaseService { + return &ReleaseService{db: db} +} + +// List 获取所有版本列表 +func (s *ReleaseService) List() ([]Release, error) { + var releases []Release + err := s.db.Order("created_at DESC").Find(&releases).Error + return releases, err +} + +// ListByApp 获取指定应用的所有版本 +func (s *ReleaseService) ListByApp(appID string) ([]Release, error) { + var releases []Release + err := s.db.Where("app_id = ?", appID).Order("created_at DESC").Find(&releases).Error + return releases, err +} + +// ListByAppAndChannel 获取指定应用和渠道的版本 +func (s *ReleaseService) ListByAppAndChannel(appID, channel string) ([]Release, error) { + var releases []Release + query := s.db.Where("app_id = ?", appID) + if channel != "" { + query = query.Where("channel = ?", channel) + } + err := query.Order("created_at DESC").Find(&releases).Error + return releases, err +} + +// Get 获取单个版本详情 +func (s *ReleaseService) Get(id uint) (*Release, error) { + var release Release + err := s.db.First(&release, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("release not found") + } + return nil, err + } + return &release, nil +} + +// GetLatest 获取应用的最新版本 +func (s *ReleaseService) GetLatest(appID string, channel string) (*Release, error) { + var release Release + query := s.db.Where("app_id = ?", appID) + if channel != "" { + query = query.Where("channel = ?", channel) + } + err := query.Order("pub_date DESC").First(&release).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("no release found for this app") + } + return nil, err + } + return &release, nil +} + +// Create 创建新版本 +func (s *ReleaseService) Create(release Release) error { + // 检查应用是否存在 + var count int64 + err := s.db.Table("apps").Where("id = ?", release.AppID).Count(&count).Error + if err != nil { + return err + } + if count == 0 { + return errors.New("app not found") + } + + // 检查版本是否已存在 + err = s.db.Where("app_id = ? AND version = ?", release.AppID, release.Version).First(&Release{}).Error + if err == nil { + return errors.New("version already exists for this app") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + return s.db.Create(&release).Error +} + +// Update 更新版本信息 +func (s *ReleaseService) Update(id uint, data map[string]any) error { + // 检查版本是否存在 + var release Release + err := s.db.First(&release, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("release not found") + } + return err + } + + // 如果更新版本号,检查是否重复 + if newVersion, ok := data["version"].(string); ok { + if newVersion != release.Version { + err := s.db.Where("app_id = ? AND version = ? AND id != ?", release.AppID, newVersion, id). + First(&Release{}).Error + if err == nil { + return errors.New("version already exists for this app") + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + } + } + + return s.db.Model(&Release{}).Where("id = ?", id).Updates(data).Error +} + +// Delete 删除版本 +func (s *ReleaseService) Delete(id uint) error { + // 检查版本是否存在 + var release Release + err := s.db.First(&release, id).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("release not found") + } + return err + } + + // TODO: 同时删除相关的 Assets + // 可以通过数据库外键级联删除,或者在这里手动删除 + + return s.db.Delete(&Release{}, id).Error +} diff --git a/internal/storage/local.go b/internal/storage/local.go new file mode 100644 index 0000000..380c67c --- /dev/null +++ b/internal/storage/local.go @@ -0,0 +1,87 @@ +package storage + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +// LocalStorage 本地文件系统存储实现 +type LocalStorage struct { + basePath string // 文件存储的根目录 + baseURL string // 文件访问的 URL 前缀 +} + +// NewLocalStorage 创建本地存储实例 +func NewLocalStorage(basePath, baseURL string) (*LocalStorage, error) { + // 确保存储目录存在 + if err := os.MkdirAll(basePath, 0755); err != nil { + return nil, fmt.Errorf("failed to create storage directory: %w", err) + } + + return &LocalStorage{ + basePath: basePath, + baseURL: baseURL, + }, nil +} + +// Save 保存文件到本地 +func (s *LocalStorage) Save(filename string, content io.Reader) (string, error) { + // 构建完整的文件路径 + fullPath := filepath.Join(s.basePath, filename) + + // 确保目标目录存在 + dir := filepath.Dir(fullPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return "", fmt.Errorf("failed to create directory: %w", err) + } + + // 创建文件 + file, err := os.Create(fullPath) + if err != nil { + return "", fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + // 复制内容 + if _, err := io.Copy(file, content); err != nil { + // 如果保存失败,尝试删除文件 + os.Remove(fullPath) + return "", fmt.Errorf("failed to write file: %w", err) + } + + // 返回相对路径(存储在数据库中) + return filename, nil +} + +// Delete 删除本地文件 +func (s *LocalStorage) Delete(path string) error { + fullPath := filepath.Join(s.basePath, path) + if err := os.Remove(fullPath); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to delete file: %w", err) + } + return nil +} + +// GetURL 获取文件的访问 URL +func (s *LocalStorage) GetURL(path string) string { + if path == "" { + return "" + } + // 将路径中的反斜杠替换为正斜杠(Windows 兼容) + path = filepath.ToSlash(path) + return s.baseURL + "/" + path +} + +// Exists 检查文件是否存在 +func (s *LocalStorage) Exists(path string) bool { + fullPath := filepath.Join(s.basePath, path) + _, err := os.Stat(fullPath) + return err == nil +} + +// GetFullPath 获取文件的完整本地路径 +func (s *LocalStorage) GetFullPath(path string) string { + return filepath.Join(s.basePath, path) +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..73865d8 --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,20 @@ +package storage + +import ( + "io" +) + +// Storage 定义文件存储接口,支持本地存储、OSS、S3 等 +type Storage interface { + // Save 保存文件,返回存储路径 + Save(filename string, content io.Reader) (string, error) + + // Delete 删除文件 + Delete(path string) error + + // GetURL 获取文件的访问 URL + GetURL(path string) string + + // Exists 检查文件是否存在 + Exists(path string) bool +} diff --git a/internal/updater/service.go b/internal/updater/service.go new file mode 100644 index 0000000..0983306 --- /dev/null +++ b/internal/updater/service.go @@ -0,0 +1,84 @@ +package updater + +import ( + "errors" + "nebula/internal/asset" + "nebula/internal/release" + "nebula/pkg/util" + + "gorm.io/gorm" +) + +type CheckRequest struct { + App string + Version string + Platform string + Arch string +} + +type CheckResponse struct { + Update bool `json:"update"` + Version string `json:"version,omitempty"` + Notes string `json:"notes,omitempty"` + URL string `json:"url,omitempty"` + Checksum string `json:"checksum,omitempty"` +} + +func CheckUpdate(db *gorm.DB, req CheckRequest) (*CheckResponse, error) { + // 参数验证 + if req.App == "" { + return nil, errors.New("app is required") + } + if req.Version == "" { + return nil, errors.New("version is required") + } + if req.Platform == "" { + return nil, errors.New("platform is required") + } + if req.Arch == "" { + return nil, errors.New("arch is required") + } + + // 查找最新版本(按发布日期排序) + var latest release.Release + err := db.Where("app_id = ?", req.App). + Order("pub_date DESC"). + First(&latest).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("no release found for this app") + } + return nil, err + } + + // 比较版本号 + if !util.IsNewerVersion(req.Version, latest.Version) { + return &CheckResponse{Update: false}, nil + } + + // 查找对应平台和架构的资源 + var ast asset.Asset + err = db.Where("release_id = ? AND platform = ? AND arch = ?", + latest.ID, req.Platform, req.Arch).First(&ast).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("no asset found for this platform and architecture") + } + return nil, err + } + + // 验证资源 URL 是否存在 + if ast.URL == "" { + return nil, errors.New("asset URL is empty") + } + + return &CheckResponse{ + Update: true, + Version: latest.Version, + Notes: latest.Notes, + URL: ast.URL, + Checksum: ast.Checksum, + }, nil +} diff --git a/nebula.db b/nebula.db new file mode 100644 index 0000000..c0d636c Binary files /dev/null and b/nebula.db differ diff --git a/pkg/util/version.go b/pkg/util/version.go new file mode 100644 index 0000000..e932f60 --- /dev/null +++ b/pkg/util/version.go @@ -0,0 +1,110 @@ +package util + +import ( + "strconv" + "strings" +) + +// CompareVersion 比较两个语义化版本号 +// 返回值:1 表示 v1 > v2,-1 表示 v1 < v2,0 表示 v1 == v2 +// 支持格式:1.0.0, v1.0.0, 1.0.0-beta.1, 1.0.0+build.123 +func CompareVersion(v1, v2 string) int { + // 移除 v 前缀 + v1 = strings.TrimPrefix(v1, "v") + v2 = strings.TrimPrefix(v2, "v") + + // 分离主版本号和预发布/构建元数据 + v1Main, v1Pre := splitVersion(v1) + v2Main, v2Pre := splitVersion(v2) + + // 比较主版本号 + result := compareMainVersion(v1Main, v2Main) + if result != 0 { + return result + } + + // 主版本号相同,比较预发布版本 + return comparePreRelease(v1Pre, v2Pre) +} + +// splitVersion 分离主版本号和预发布标识 +func splitVersion(version string) (main, preRelease string) { + // 移除构建元数据 (+build.xxx) + if idx := strings.Index(version, "+"); idx != -1 { + version = version[:idx] + } + + // 分离预发布标识 (-beta.1) + if idx := strings.Index(version, "-"); idx != -1 { + return version[:idx], version[idx+1:] + } + + return version, "" +} + +// compareMainVersion 比较主版本号 (major.minor.patch) +func compareMainVersion(v1, v2 string) int { + parts1 := strings.Split(v1, ".") + parts2 := strings.Split(v2, ".") + + // 补齐到相同长度 + maxLen := len(parts1) + if len(parts2) > maxLen { + maxLen = len(parts2) + } + + for i := 0; i < maxLen; i++ { + var num1, num2 int + + if i < len(parts1) { + num1, _ = strconv.Atoi(parts1[i]) + } + if i < len(parts2) { + num2, _ = strconv.Atoi(parts2[i]) + } + + if num1 > num2 { + return 1 + } + if num1 < num2 { + return -1 + } + } + + return 0 +} + +// comparePreRelease 比较预发布版本 +// 没有预发布标识的版本 > 有预发布标识的版本 +// 例如:1.0.0 > 1.0.0-beta +func comparePreRelease(pre1, pre2 string) int { + // 都没有预发布标识,相等 + if pre1 == "" && pre2 == "" { + return 0 + } + + // v1 是正式版,v2 是预发布版 + if pre1 == "" && pre2 != "" { + return 1 + } + + // v1 是预发布版,v2 是正式版 + if pre1 != "" && pre2 == "" { + return -1 + } + + // 都是预发布版,按字典序比较 + if pre1 < pre2 { + return -1 + } + if pre1 > pre2 { + return 1 + } + + return 0 +} + +// IsNewerVersion 检查 newVer 是否比 currentVer 新 +func IsNewerVersion(currentVer, newVer string) bool { + return CompareVersion(newVer, currentVer) > 0 +} diff --git a/scripts/build-prod.bat b/scripts/build-prod.bat new file mode 100644 index 0000000..615fc65 --- /dev/null +++ b/scripts/build-prod.bat @@ -0,0 +1,33 @@ +@echo off +echo Building Nebula for production... + +echo. +echo Building frontend... +cd web +call pnpm build +if errorlevel 1 ( + echo Frontend build failed! + exit /b 1 +) + +cd .. + +echo. +echo Building backend... +go build -o nebula-server.exe ./cmd/nebula-server +if errorlevel 1 ( + echo Backend build failed! + exit /b 1 +) + +echo. +echo ======================================== +echo Build completed successfully! +echo ======================================== +echo. +echo To run in production mode: +echo set SERVER_MODE=prod +echo .\nebula-server.exe +echo. +echo Or use the start script: +echo .\scripts\start.bat diff --git a/scripts/build-prod.sh b/scripts/build-prod.sh new file mode 100644 index 0000000..4af1917 --- /dev/null +++ b/scripts/build-prod.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +echo "Building Nebula for production..." + +echo "" +echo "Building frontend..." +cd web +pnpm build +if [ $? -ne 0 ]; then + echo "Frontend build failed!" + exit 1 +fi + +cd .. + +echo "" +echo "Building backend..." +go build -o nebula-server ./cmd/nebula-server +if [ $? -ne 0 ]; then + echo "Backend build failed!" + exit 1 +fi + +echo "" +echo "========================================" +echo "Build completed successfully!" +echo "========================================" +echo "" +echo "To run in production mode:" +echo " SERVER_MODE=prod ./nebula-server" +echo "" +echo "Or use the start script:" +echo " ./scripts/start.sh" diff --git a/scripts/build.bat b/scripts/build.bat new file mode 100644 index 0000000..8a66a6e --- /dev/null +++ b/scripts/build.bat @@ -0,0 +1,24 @@ +@echo off +setlocal +set BINARY=nebula-server +set OUT_DIR=dist +set WEB_DIR=web + +echo Building frontend... +cd %WEB_DIR% +call pnpm run build +if %ERRORLEVEL% neq 0 ( + echo Frontend build failed. + exit /b 1 +) +cd .. + +if not exist %OUT_DIR% mkdir %OUT_DIR% +echo Building %BINARY%... +go build -o %OUT_DIR%\%BINARY%.exe ./cmd/nebula-server/ +if %ERRORLEVEL% neq 0 ( + echo Build failed. + exit /b 1 +) +echo Build done: %OUT_DIR%\%BINARY%.exe +endlocal diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..876b00b --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -e +BINARY=nebula-server +OUT_DIR=dist +WEB_DIR=web + +echo "Building frontend..." +(cd "$WEB_DIR" && pnpm run build) + +[[ -d "$OUT_DIR" ]] || mkdir -p "$OUT_DIR" +SUFFIX="" +[[ "$GOOS" == "windows" ]] && SUFFIX=".exe" + +echo "Building $BINARY..." +go build -o "$OUT_DIR/${BINARY}${SUFFIX}" ./cmd/nebula-server/ +echo "Build done: $OUT_DIR/${BINARY}${SUFFIX}" diff --git a/scripts/dev.bat b/scripts/dev.bat new file mode 100644 index 0000000..ce66204 --- /dev/null +++ b/scripts/dev.bat @@ -0,0 +1,14 @@ +@echo off +echo Starting Nebula development environment... + +echo. +echo Starting backend server (dev mode)... +start "Nebula Backend" cmd /c "set SERVER_MODE=dev && go run ./cmd/nebula-server/main.go" + +echo Waiting for backend to start... +timeout /t 3 /nobreak >nul + +echo. +echo Starting frontend (Vite dev server)... +cd web +pnpm dev \ No newline at end of file diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100644 index 0000000..7c014c2 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "Starting Nebula development environment..." + +echo "" +echo "Starting backend server (dev mode)..." +SERVER_MODE=dev go run ./cmd/nebula-server/main.go & +BACKEND_PID=$! + +echo "Backend started with PID: $BACKEND_PID" +echo "Waiting for backend to start..." +sleep 3 + +echo "" +echo "Starting frontend (Vite dev server)..." +cd web +pnpm dev + +# Cleanup function to kill backend when script exits +cleanup() { + echo "" + echo "Shutting down backend server..." + kill $BACKEND_PID 2>/dev/null + exit +} + +trap cleanup INT TERM diff --git a/scripts/start.bat b/scripts/start.bat new file mode 100644 index 0000000..38b9a8a --- /dev/null +++ b/scripts/start.bat @@ -0,0 +1,5 @@ +@echo off +echo Starting Nebula in production mode... + +set SERVER_MODE=prod +nebula-server.exe diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100644 index 0000000..b9b534e --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +echo "Starting Nebula in production mode..." + +SERVER_MODE=prod ./nebula-server diff --git a/types/json_time.go b/types/json_time.go new file mode 100644 index 0000000..76c2177 --- /dev/null +++ b/types/json_time.go @@ -0,0 +1,73 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "time" +) + +type JSONTime time.Time + +const TimeFormat = "2006-01-02 15:04:05" + +// timeFormatWithFrac SQLite 等可能带小数秒 +const timeFormatWithFrac = "2006-01-02 15:04:05.999999999" + +func (t JSONTime) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Time(t).Format(TimeFormat)) +} + +func (t *JSONTime) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + parsed, err := time.Parse(TimeFormat, s) + if err != nil { + return err + } + *t = JSONTime(parsed) + return nil +} + +// Value 实现 driver.Valuer,供 GORM 写入数据库 +func (t JSONTime) Value() (driver.Value, error) { + return time.Time(t), nil +} + +// Scan 实现 sql.Scanner,供 GORM 从数据库读取 +func (t *JSONTime) Scan(value interface{}) error { + if value == nil { + *t = JSONTime(time.Time{}) + return nil + } + switch v := value.(type) { + case time.Time: + *t = JSONTime(v) + return nil + case []byte: + tm, err := parseTime(string(v)) + if err != nil { + return err + } + *t = JSONTime(tm) + return nil + case string: + tm, err := parseTime(v) + if err != nil { + return err + } + *t = JSONTime(tm) + return nil + default: + *t = JSONTime(time.Time{}) + return nil + } +} + +func parseTime(s string) (time.Time, error) { + if t, err := time.Parse(timeFormatWithFrac, s); err == nil { + return t, nil + } + return time.Parse(TimeFormat, s) +} diff --git a/web/.editorconfig b/web/.editorconfig new file mode 100644 index 0000000..3b510aa --- /dev/null +++ b/web/.editorconfig @@ -0,0 +1,8 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 diff --git a/web/.gitattributes b/web/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/web/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..cd68f14 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,39 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +.eslintcache + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json new file mode 100644 index 0000000..d5648b9 --- /dev/null +++ b/web/.oxlintrc.json @@ -0,0 +1,10 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"], + "env": { + "browser": true + }, + "categories": { + "correctness": "error" + } +} diff --git a/web/.prettierrc.json b/web/.prettierrc.json new file mode 100644 index 0000000..29a2402 --- /dev/null +++ b/web/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..3f84126 --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "Vue.volar", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "oxc.oxc-vscode", + "esbenp.prettier-vscode" + ] +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..851b044 --- /dev/null +++ b/web/README.md @@ -0,0 +1,48 @@ +# web + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Recommended Browser Setup + +- Chromium-based browsers (Chrome, Edge, Brave, etc.): + - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) + - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) +- Firefox: + - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) + - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +pnpm install +``` + +### Compile and Hot-Reload for Development + +```sh +pnpm dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +pnpm build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +pnpm lint +``` diff --git a/web/env.d.ts b/web/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/eslint.config.ts b/web/eslint.config.ts new file mode 100644 index 0000000..89fdc89 --- /dev/null +++ b/web/eslint.config.ts @@ -0,0 +1,26 @@ +import { globalIgnores } from 'eslint/config' +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' +import pluginVue from 'eslint-plugin-vue' +import pluginOxlint from 'eslint-plugin-oxlint' +import skipFormatting from 'eslint-config-prettier/flat' + +// To allow more languages other than `ts` in `.vue` files, uncomment the following lines: +// import { configureVueProject } from '@vue/eslint-config-typescript' +// configureVueProject({ scriptLangs: ['ts', 'tsx'] }) +// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup + +export default defineConfigWithVueTs( + { + name: 'app/files-to-lint', + files: ['**/*.{vue,ts,mts,tsx}'], + }, + + globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), + + ...pluginVue.configs['flat/essential'], + vueTsConfigs.recommended, + + ...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'), + + skipFormatting, +) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..9e5fc8f --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..83b8ed6 --- /dev/null +++ b/web/package.json @@ -0,0 +1,48 @@ +{ + "name": "web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "lint": "run-s lint:*", + "lint:oxlint": "oxlint . --fix", + "lint:eslint": "eslint . --fix --cache", + "format": "prettier --write --experimental-cli src/" + }, + "dependencies": { + "@tailwindcss/vite": "^4.2.1", + "lucide-vue-next": "^0.577.0", + "pinia": "^3.0.4", + "tailwindcss": "^4.2.1", + "vue": "^3.5.29", + "vue-router": "^5.0.3" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.11.0", + "@vitejs/plugin-vue": "^6.0.4", + "@vue/eslint-config-typescript": "^14.7.0", + "@vue/tsconfig": "^0.8.1", + "eslint": "^10.0.2", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-oxlint": "~1.50.0", + "eslint-plugin-vue": "~10.8.0", + "jiti": "^2.6.1", + "naive-ui": "^2.44.1", + "npm-run-all2": "^8.0.4", + "oxlint": "~1.50.0", + "prettier": "3.8.1", + "typescript": "~5.9.3", + "vite": "^7.3.1", + "vite-plugin-vue-devtools": "^8.0.6", + "vue-tsc": "^3.2.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..a0eeb23 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,3793 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + lucide-vue-next: + specifier: ^0.577.0 + version: 0.577.0(vue@3.5.29(typescript@5.9.3)) + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)) + tailwindcss: + specifier: ^4.2.1 + version: 4.2.1 + vue: + specifier: ^3.5.29 + version: 3.5.29(typescript@5.9.3) + vue-router: + specifier: ^5.0.3 + version: 5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)) + devDependencies: + '@tsconfig/node24': + specifier: ^24.0.4 + version: 24.0.4 + '@types/node': + specifier: ^24.11.0 + version: 24.12.0 + '@vitejs/plugin-vue': + specifier: ^6.0.4 + version: 6.0.4(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + '@vue/eslint-config-typescript': + specifier: ^14.7.0 + version: 14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@vue/tsconfig': + specifier: ^0.8.1 + version: 0.8.1(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)) + eslint: + specifier: ^10.0.2 + version: 10.0.3(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.0.3(jiti@2.6.1)) + eslint-plugin-oxlint: + specifier: ~1.50.0 + version: 1.50.0 + eslint-plugin-vue: + specifier: ~10.8.0 + version: 10.8.0(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) + jiti: + specifier: ^2.6.1 + version: 2.6.1 + naive-ui: + specifier: ^2.44.1 + version: 2.44.1(vue@3.5.29(typescript@5.9.3)) + npm-run-all2: + specifier: ^8.0.4 + version: 8.0.4 + oxlint: + specifier: ~1.50.0 + version: 1.50.0 + prettier: + specifier: 3.8.1 + version: 3.8.1 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) + vite-plugin-vue-devtools: + specifier: ^8.0.6 + version: 8.0.7(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + vue-tsc: + specifier: ^3.2.5 + version: 3.2.5(typescript@5.9.3) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@css-render/plugin-bem@0.15.14': + resolution: {integrity: sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==} + peerDependencies: + css-render: ~0.15.14 + + '@css-render/vue3-ssr@0.15.14': + resolution: {integrity: sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==} + peerDependencies: + vue: ^3.0.11 + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.3': + resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.3': + resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.1.1': + resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/object-schema@3.0.3': + resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.6.1': + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxlint/binding-android-arm-eabi@1.50.0': + resolution: {integrity: sha512-G7MRGk/6NCe+L8ntonRdZP7IkBfEpiZ/he3buLK6JkLgMHgJShXZ+BeOwADmspXez7U7F7L1Anf4xLSkLHiGTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.50.0': + resolution: {integrity: sha512-GeSuMoJWCVpovJi/e3xDSNgjeR8WEZ6MCXL6EtPiCIM2NTzv7LbflARINTXTJy2oFBYyvdf/l2PwHzYo6EdXvg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.50.0': + resolution: {integrity: sha512-w3SY5YtxGnxCHPJ8Twl3KmS9oja1gERYk3AMoZ7Hv8P43ZtB6HVfs02TxvarxfL214Tm3uzvc2vn+DhtUNeKnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.50.0': + resolution: {integrity: sha512-hNfogDqy7tvmllXKBSlHo6k5x7dhTUVOHbMSE15CCAcXzmqf5883aPvBYPOq9AE7DpDUQUZ1kVE22YbiGW+tuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.50.0': + resolution: {integrity: sha512-ykZevOWEyu0nsxolA911ucxpEv0ahw8jfEeGWOwwb/VPoE4xoexuTOAiPNlWZNJqANlJl7yp8OyzCtXTUAxotw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.50.0': + resolution: {integrity: sha512-hif3iDk7vo5GGJ4OLCCZAf2vjnU9FztGw4L0MbQL0M2iY9LKFtDMMiQAHmkF0PQGQMVbTYtPdXCLKVgdkiqWXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.50.0': + resolution: {integrity: sha512-dVp9iSssiGAnTNey2Ruf6xUaQhdnvcFOJyRWd/mu5o2jVbFK15E5fbWGeFRfmuobu5QXuROtFga44+7DOS3PLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.50.0': + resolution: {integrity: sha512-1cT7yz2HA910CKA9NkH1ZJo50vTtmND2fkoW1oyiSb0j6WvNtJ0Wx2zoySfXWc/c+7HFoqRK5AbEoL41LOn9oA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.50.0': + resolution: {integrity: sha512-++B3k/HEPFVlj89cOz8kWfQccMZB/aWL9AhsW7jPIkG++63Mpwb2cE9XOEsd0PATbIan78k2Gky+09uWM1d/gQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.50.0': + resolution: {integrity: sha512-Z9b/KpFMkx66w3gVBqjIC1AJBTZAGoI9+U+K5L4QM0CB/G0JSNC1es9b3Y0Vcrlvtdn8A+IQTkYjd/Q0uCSaZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.50.0': + resolution: {integrity: sha512-jvmuIw8wRSohsQlFNIST5uUwkEtEJmOQYr33bf/K2FrFPXHhM4KqGekI3ShYJemFS/gARVacQFgBzzJKCAyJjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.50.0': + resolution: {integrity: sha512-x+UrN47oYNh90nmAAyql8eQaaRpHbDPu5guasDg10+OpszUQ3/1+1J6zFMmV4xfIEgTcUXG/oI5fxJhF4eWCNA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.50.0': + resolution: {integrity: sha512-i/JLi2ljLUIVfekMj4ISmdt+Hn11wzYUdRRrkVUYsCWw7zAy5xV7X9iA+KMyM156LTFympa7s3oKBjuCLoTAUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.50.0': + resolution: {integrity: sha512-/C7brhn6c6UUPccgSPCcpLQXcp+xKIW/3sji/5VZ8/OItL3tQ2U7KalHz887UxxSQeEOmd1kY6lrpuwFnmNqOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.50.0': + resolution: {integrity: sha512-oDR1f+bGOYU8LfgtEW8XtotWGB63ghtcxk5Jm6IDTCk++rTA/IRMsjOid2iMd+1bW+nP9Mdsmcdc7VbPD3+iyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.50.0': + resolution: {integrity: sha512-4CmRGPp5UpvXyu4jjP9Tey/SrXDQLRvZXm4pb4vdZBxAzbFZkCyh0KyRy4txld/kZKTJlW4TO8N1JKrNEk+mWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.50.0': + resolution: {integrity: sha512-Fq0M6vsGcFsSfeuWAACDhd5KJrO85ckbEfe1EGuBj+KPyJz7KeWte2fSFrFGmNKNXyhEMyx4tbgxiWRujBM2KQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.50.0': + resolution: {integrity: sha512-qTdWR9KwY/vxJGhHVIZG2eBOhidOQvOwzDxnX+jhW/zIVacal1nAhR8GLkiywW8BIFDkQKXo/zOfT+/DY+ns/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.50.0': + resolution: {integrity: sha512-682t7npLC4G2Ca+iNlI9fhAKTcFPYYXJjwoa88H4q+u5HHHlsnL/gHULapX3iqp+A8FIJbgdylL5KMYo2LaluQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/pluginutils@1.0.0-rc.2': + resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': + resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + resolution: {integrity: sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.1': + resolution: {integrity: sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + resolution: {integrity: sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + resolution: {integrity: sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + resolution: {integrity: sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + resolution: {integrity: sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.1': + resolution: {integrity: sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.1': + resolution: {integrity: sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tsconfig/node24@24.0.4': + resolution: {integrity: sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + '@typescript-eslint/eslint-plugin@8.56.1': + resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.56.1': + resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.56.1': + resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.56.1': + resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.56.1': + resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.56.1': + resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.56.1': + resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.56.1': + resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.56.1': + resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.56.1': + resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@6.0.4': + resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vue: ^3.2.25 + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue-macros/common@3.1.2': + resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.29': + resolution: {integrity: sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==} + + '@vue/compiler-dom@3.5.29': + resolution: {integrity: sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==} + + '@vue/compiler-sfc@3.5.29': + resolution: {integrity: sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==} + + '@vue/compiler-ssr@3.5.29': + resolution: {integrity: sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-api@8.0.7': + resolution: {integrity: sha512-tc1TXAxclsn55JblLkFVcIRG7MeSJC4fWsPjfM7qu/IcmPUYnQ5Q8vzWwBpyDY24ZjmZTUCCwjRSNbx58IhlAA==} + + '@vue/devtools-core@8.0.7': + resolution: {integrity: sha512-PmpiPxvg3Of80ODHVvyckxwEW1Z02VIAvARIZS1xegINn3VuNQLm9iHUmKD+o6cLkMNWV8OG8x7zo0kgydZgdg==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-kit@8.0.7': + resolution: {integrity: sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/devtools-shared@8.0.7': + resolution: {integrity: sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA==} + + '@vue/eslint-config-typescript@14.7.0': + resolution: {integrity: sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.10.0 || ^10.0.0 + eslint-plugin-vue: ^9.28.0 || ^10.0.0 + typescript: '>=4.8.4' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@3.2.5': + resolution: {integrity: sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==} + + '@vue/reactivity@3.5.29': + resolution: {integrity: sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==} + + '@vue/runtime-core@3.5.29': + resolution: {integrity: sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==} + + '@vue/runtime-dom@3.5.29': + resolution: {integrity: sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==} + + '@vue/server-renderer@3.5.29': + resolution: {integrity: sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==} + peerDependencies: + vue: 3.5.29 + + '@vue/shared@3.5.29': + resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} + + '@vue/tsconfig@0.8.1': + resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + alien-signals@3.1.2: + resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-walker-scope@0.8.3: + resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} + engines: {node: '>=20.19.0'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.0: + resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + engines: {node: '>=6.0.0'} + hasBin: true + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + caniuse-lite@1.0.30001777: + resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-render@0.15.14: + resolution: {integrity: sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.0.11: + resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.307: + resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + + enhanced-resolve@5.20.0: + resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + engines: {node: '>=10.13.0'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-oxlint@1.50.0: + resolution: {integrity: sha512-QAxeFeUHuekmLkuRLdzHH8Z0JvC7482OaQ3jlUMdEd0gcS6m+MYHei3Favoew9DdvTQT7yHxrm7BL0iXoenb6w==} + + eslint-plugin-vue@10.8.0: + resolution: {integrity: sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.0.3: + resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + evtd@0.2.4: + resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.0: + resolution: {integrity: sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@4.0.0: + resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==} + engines: {node: ^18.17.0 || >=20.5.0} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} + engines: {node: '>= 12.0.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-vue-next@0.577.0: + resolution: {integrity: sha512-py05bAfv9SHVJqscbiOnjcnLlEmOffA58a+7XhZuFxrs6txe1E8VoR1ngWGTYO+9aVKABAz8l3ee3PqiQN9QPA==} + peerDependencies: + vue: '>=3.0.1' + + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + naive-ui@2.44.1: + resolution: {integrity: sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==} + engines: {node: '>=20'} + peerDependencies: + vue: ^3.0.0 + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.36: + resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-run-all2@8.0.4: + resolution: {integrity: sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==} + engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'} + hasBin: true + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + oxlint@1.50.0: + resolution: {integrity: sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.14.1' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-package-json-fast@4.0.0: + resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} + engines: {node: ^18.17.0 || >=20.5.0} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + seemly@0.3.10: + resolution: {integrity: sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + treemate@0.3.11: + resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.56.1: + resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vdirs@0.1.8: + resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==} + peerDependencies: + vue: ^3.0.11 + + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-inspect@11.3.3: + resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.0.7: + resolution: {integrity: sha512-BWj/ykGpqVAJVdPyHmSTUm44buz3jPv+6jnvuFdQSRH0kAgP1cEIE4doHiFyqHXOmuB5EQVR/nh2g9YRiRNs9g==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0-0 || ^8.0.0-0 + + vite-plugin-vue-inspector@5.3.2: + resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vooks@0.2.12: + resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==} + peerDependencies: + vue: ^3.0.0 + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-eslint-parser@10.4.0: + resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + vue-router@5.0.3: + resolution: {integrity: sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.17 + pinia: ^3.0.4 + vue: ^3.5.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + + vue-tsc@3.2.5: + resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.29: + resolution: {integrity: sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vueuc@0.4.65: + resolution: {integrity: sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==} + peerDependencies: + vue: ^3.0.11 + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@css-render/plugin-bem@0.15.14(css-render@0.15.14)': + dependencies: + css-render: 0.15.14 + + '@css-render/vue3-ssr@0.15.14(vue@3.5.29(typescript@5.9.3))': + dependencies: + vue: 3.5.29(typescript@5.9.3) + + '@emotion/hash@0.8.0': {} + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.6.1))': + dependencies: + eslint: 10.0.3(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.3': + dependencies: + '@eslint/object-schema': 3.0.3 + debug: 4.4.3 + minimatch: 10.2.4 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.3': + dependencies: + '@eslint/core': 1.1.1 + + '@eslint/core@1.1.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/object-schema@3.0.3': {} + + '@eslint/plugin-kit@0.6.1': + dependencies: + '@eslint/core': 1.1.1 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@juggle/resize-observer@3.4.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxlint/binding-android-arm-eabi@1.50.0': + optional: true + + '@oxlint/binding-android-arm64@1.50.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.50.0': + optional: true + + '@oxlint/binding-darwin-x64@1.50.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.50.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.50.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.50.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.50.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.50.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.50.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.50.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.50.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.50.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.50.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.50.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/pluginutils@1.0.0-rc.2': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@tailwindcss/node@4.2.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.20.0 + jiti: 2.6.1 + lightningcss: 1.31.1 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.1 + + '@tailwindcss/oxide-android-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.1': + optional: true + + '@tailwindcss/oxide@4.2.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-arm64': 4.2.1 + '@tailwindcss/oxide-darwin-x64': 4.2.1 + '@tailwindcss/oxide-freebsd-x64': 4.2.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.1 + '@tailwindcss/oxide-linux-x64-musl': 4.2.1 + '@tailwindcss/oxide-wasm32-wasi': 4.2.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 + + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))': + dependencies: + '@tailwindcss/node': 4.2.1 + '@tailwindcss/oxide': 4.2.1 + tailwindcss: 4.2.1 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) + + '@tsconfig/node24@24.0.4': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.24 + + '@types/lodash@4.17.24': {} + + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/type-utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + eslint: 10.0.3(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + eslint: 10.0.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + + '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 10.0.3(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.56.1': {} + + '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/visitor-keys': 8.56.1 + debug: 4.4.3 + minimatch: 10.2.4 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.1 + '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + eslint: 10.0.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.56.1': + dependencies: + '@typescript-eslint/types': 8.56.1 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.2 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) + vue: 3.5.29(typescript@5.9.3) + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue-macros/common@3.1.2(vue@3.5.29(typescript@5.9.3))': + dependencies: + '@vue/compiler-sfc': 3.5.29 + ast-kit: 2.2.0 + local-pkg: 1.1.2 + magic-string-ast: 1.0.3 + unplugin-utils: 0.3.1 + optionalDependencies: + vue: 3.5.29(typescript@5.9.3) + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.0) + '@vue/shared': 3.5.29 + optionalDependencies: + '@babel/core': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.0)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/parser': 7.29.0 + '@vue/compiler-sfc': 3.5.29 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.29 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.29': + dependencies: + '@vue/compiler-core': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/compiler-sfc@3.5.29': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.29 + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.8 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.29': + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-api@8.0.7': + dependencies: + '@vue/devtools-kit': 8.0.7 + + '@vue/devtools-core@8.0.7(vue@3.5.29(typescript@5.9.3))': + dependencies: + '@vue/devtools-kit': 8.0.7 + '@vue/devtools-shared': 8.0.7 + vue: 3.5.29(typescript@5.9.3) + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-kit@8.0.7': + dependencies: + '@vue/devtools-shared': 8.0.7 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@8.0.7': {} + + '@vue/eslint-config-typescript@14.7.0(eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.3(jiti@2.6.1) + eslint-plugin-vue: 10.8.0(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))) + fast-glob: 3.3.3 + typescript-eslint: 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@vue/language-core@3.2.5': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.29 + '@vue/shared': 3.5.29 + alien-signals: 3.1.2 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + + '@vue/reactivity@3.5.29': + dependencies: + '@vue/shared': 3.5.29 + + '@vue/runtime-core@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/shared': 3.5.29 + + '@vue/runtime-dom@3.5.29': + dependencies: + '@vue/reactivity': 3.5.29 + '@vue/runtime-core': 3.5.29 + '@vue/shared': 3.5.29 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.29(vue@3.5.29(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.29 + '@vue/shared': 3.5.29 + vue: 3.5.29(typescript@5.9.3) + + '@vue/shared@3.5.29': {} + + '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3))': + optionalDependencies: + typescript: 5.9.3 + vue: 3.5.29(typescript@5.9.3) + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@3.1.2: {} + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.0 + pathe: 2.0.3 + + ast-walker-scope@0.8.3: + dependencies: + '@babel/parser': 7.29.0 + ast-kit: 2.2.0 + + async-validator@4.2.5: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.0: {} + + birpc@2.9.0: {} + + boolbase@1.0.0: {} + + brace-expansion@5.0.4: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.0 + caniuse-lite: 1.0.30001777 + electron-to-chromium: 1.5.307 + node-releases: 2.0.36 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + caniuse-lite@1.0.30001777: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + confbox@0.1.8: {} + + confbox@0.2.4: {} + + convert-source-map@2.0.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-render@0.15.14: + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.0.11 + + cssesc@3.0.0: {} + + csstype@3.0.11: {} + + csstype@3.2.3: {} + + date-fns-tz@3.2.0(date-fns@4.1.0): + dependencies: + date-fns: 4.1.0 + + date-fns@4.1.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.307: {} + + enhanced-resolve@5.20.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@7.0.1: {} + + error-stack-parser-es@1.0.5: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)): + dependencies: + eslint: 10.0.3(jiti@2.6.1) + + eslint-plugin-oxlint@1.50.0: + dependencies: + jsonc-parser: 3.3.1 + + eslint-plugin-vue@10.8.0(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1))): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + eslint: 10.0.3(jiti@2.6.1) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.1 + semver: 7.7.4 + vue-eslint-parser: 10.4.0(eslint@10.0.3(jiti@2.6.1)) + xml-name-validator: 4.0.0 + optionalDependencies: + '@typescript-eslint/parser': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.0.3(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.3 + '@eslint/config-helpers': 0.5.3 + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + 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.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.4 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + evtd@0.2.4: {} + + exsolve@1.0.8: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.0 + keyv: 4.5.4 + + flatted@3.4.0: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + graceful-fs@4.2.11: {} + + highlight.js@11.11.1: {} + + hookable@5.5.3: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + imurmurhash@0.1.4: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-number@7.0.0: {} + + is-what@5.5.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@4.0.0: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.31.1: + optional: true + + lightningcss-darwin-arm64@1.31.1: + optional: true + + lightningcss-darwin-x64@1.31.1: + optional: true + + lightningcss-freebsd-x64@1.31.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.31.1: + optional: true + + lightningcss-linux-arm64-gnu@1.31.1: + optional: true + + lightningcss-linux-arm64-musl@1.31.1: + optional: true + + lightningcss-linux-x64-gnu@1.31.1: + optional: true + + lightningcss-linux-x64-musl@1.31.1: + optional: true + + lightningcss-win32-arm64-msvc@1.31.1: + optional: true + + lightningcss-win32-x64-msvc@1.31.1: + optional: true + + lightningcss@1.31.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.1 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.23: {} + + lodash@4.17.23: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-vue-next@0.577.0(vue@3.5.29(typescript@5.9.3)): + dependencies: + vue: 3.5.29(typescript@5.9.3) + + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + memorystream@0.3.1: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.4 + + mitt@3.0.1: {} + + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + naive-ui@2.44.1(vue@3.5.29(typescript@5.9.3)): + dependencies: + '@css-render/plugin-bem': 0.15.14(css-render@0.15.14) + '@css-render/vue3-ssr': 0.15.14(vue@3.5.29(typescript@5.9.3)) + '@types/lodash': 4.17.24 + '@types/lodash-es': 4.17.12 + async-validator: 4.2.5 + css-render: 0.15.14 + csstype: 3.2.3 + date-fns: 4.1.0 + date-fns-tz: 3.2.0(date-fns@4.1.0) + evtd: 0.2.4 + highlight.js: 11.11.1 + lodash: 4.17.23 + lodash-es: 4.17.23 + seemly: 0.3.10 + treemate: 0.3.11 + vdirs: 0.1.8(vue@3.5.29(typescript@5.9.3)) + vooks: 0.2.12(vue@3.5.29(typescript@5.9.3)) + vue: 3.5.29(typescript@5.9.3) + vueuc: 0.4.65(vue@3.5.29(typescript@5.9.3)) + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.36: {} + + npm-normalize-package-bin@4.0.0: {} + + npm-run-all2@8.0.4: + dependencies: + ansi-styles: 6.2.3 + cross-spawn: 7.0.6 + memorystream: 0.3.1 + picomatch: 4.0.3 + pidtree: 0.6.0 + read-package-json-fast: 4.0.0 + shell-quote: 1.8.3 + which: 5.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + ohash@2.0.11: {} + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + oxlint@1.50.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.50.0 + '@oxlint/binding-android-arm64': 1.50.0 + '@oxlint/binding-darwin-arm64': 1.50.0 + '@oxlint/binding-darwin-x64': 1.50.0 + '@oxlint/binding-freebsd-x64': 1.50.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.50.0 + '@oxlint/binding-linux-arm-musleabihf': 1.50.0 + '@oxlint/binding-linux-arm64-gnu': 1.50.0 + '@oxlint/binding-linux-arm64-musl': 1.50.0 + '@oxlint/binding-linux-ppc64-gnu': 1.50.0 + '@oxlint/binding-linux-riscv64-gnu': 1.50.0 + '@oxlint/binding-linux-riscv64-musl': 1.50.0 + '@oxlint/binding-linux-s390x-gnu': 1.50.0 + '@oxlint/binding-linux-x64-gnu': 1.50.0 + '@oxlint/binding-linux-x64-musl': 1.50.0 + '@oxlint/binding-openharmony-arm64': 1.50.0 + '@oxlint/binding-win32-arm64-msvc': 1.50.0 + '@oxlint/binding-win32-ia32-msvc': 1.50.0 + '@oxlint/binding-win32-x64-msvc': 1.50.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pidtree@0.6.0: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.29(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.1: {} + + punycode@2.3.1: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-package-json-fast@4.0.0: + dependencies: + json-parse-even-better-errors: 4.0.0 + npm-normalize-package-bin: 4.0.0 + + readdirp@5.0.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scule@1.3.0: {} + + seemly@0.3.10: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + tailwindcss@4.2.1: {} + + tapable@2.3.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + treemate@0.3.11: {} + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.3(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undici-types@7.16.0: {} + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vdirs@0.1.8(vue@3.5.29(typescript@5.9.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.29(typescript@5.9.3) + + vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)): + dependencies: + birpc: 2.9.0 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) + vite-hot-client: 2.1.0(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + + vite-hot-client@2.1.0(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)): + dependencies: + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) + + vite-plugin-inspect@11.3.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)): + dependencies: + ansis: 4.2.0 + debug: 4.4.3 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.2.0 + perfect-debounce: 2.1.0 + sirv: 3.0.2 + unplugin-utils: 0.3.1 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) + vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + transitivePeerDependencies: + - supports-color + + vite-plugin-vue-devtools@8.0.7(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)): + dependencies: + '@vue/devtools-core': 8.0.7(vue@3.5.29(typescript@5.9.3)) + '@vue/devtools-kit': 8.0.7 + '@vue/devtools-shared': 8.0.7 + sirv: 3.0.2 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) + vite-plugin-inspect: 11.3.3(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + vite-plugin-vue-inspector: 5.3.2(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + + vite-plugin-vue-inspector@5.3.2(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) + '@vue/compiler-dom': 3.5.29 + kolorist: 1.8.0 + magic-string: 0.30.21 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.12.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + yaml: 2.8.2 + + vooks@0.2.12(vue@3.5.29(typescript@5.9.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.29(typescript@5.9.3) + + vscode-uri@3.1.0: {} + + vue-eslint-parser@10.4.0(eslint@10.0.3(jiti@2.6.1)): + dependencies: + debug: 4.4.3 + eslint: 10.0.3(jiti@2.6.1) + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + vue-router@5.0.3(@vue/compiler-sfc@3.5.29)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)))(vue@3.5.29(typescript@5.9.3)): + dependencies: + '@babel/generator': 7.29.1 + '@vue-macros/common': 3.1.2(vue@3.5.29(typescript@5.9.3)) + '@vue/devtools-api': 8.0.7 + ast-walker-scope: 0.8.3 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.1 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.3 + scule: 1.3.0 + tinyglobby: 0.2.15 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + vue: 3.5.29(typescript@5.9.3) + yaml: 2.8.2 + optionalDependencies: + '@vue/compiler-sfc': 3.5.29 + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3)) + + vue-tsc@3.2.5(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.2.5 + typescript: 5.9.3 + + vue@3.5.29(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.29 + '@vue/compiler-sfc': 3.5.29 + '@vue/runtime-dom': 3.5.29 + '@vue/server-renderer': 3.5.29(vue@3.5.29(typescript@5.9.3)) + '@vue/shared': 3.5.29 + optionalDependencies: + typescript: 5.9.3 + + vueuc@0.4.65(vue@3.5.29(typescript@5.9.3)): + dependencies: + '@css-render/vue3-ssr': 0.15.14(vue@3.5.29(typescript@5.9.3)) + '@juggle/resize-observer': 3.4.0 + css-render: 0.15.14 + evtd: 0.2.4 + seemly: 0.3.10 + vdirs: 0.1.8(vue@3.5.29(typescript@5.9.3)) + vooks: 0.2.12(vue@3.5.29(typescript@5.9.3)) + vue: 3.5.29(typescript@5.9.3) + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@5.0.0: + dependencies: + isexe: 3.1.5 + + word-wrap@1.2.5: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + xml-name-validator@4.0.0: {} + + yallist@3.1.1: {} + + yaml@2.8.2: {} + + yocto-queue@0.1.0: {} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/web/public/favicon.ico differ diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..7402409 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/web/src/layout/default.vue b/web/src/layout/default.vue new file mode 100644 index 0000000..a4e1a23 --- /dev/null +++ b/web/src/layout/default.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..d6a3b60 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,14 @@ +import './style/index.css' + +import { createApp } from 'vue' +import { createPinia } from 'pinia' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) + +app.mount('#app') diff --git a/web/src/router/index.ts b/web/src/router/index.ts new file mode 100644 index 0000000..440d6c3 --- /dev/null +++ b/web/src/router/index.ts @@ -0,0 +1,45 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '@/stores/auth' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/login', + name: 'login', + component: () => import('@/view/auth/login.vue'), + meta: { requiresAuth: false }, + }, + { + path: '/', + component: () => import('@/layout/default.vue'), + meta: { requiresAuth: true }, + children: [ + { + path: '', + name: 'home', + component: () => import('@/view/home/index.vue'), + }, + ], + }, + ], +}) + +// 路由守卫 +router.beforeEach((to, from, next) => { + const authStore = useAuthStore() + const isAuthenticated = authStore.isAuthenticated() + + if (to.meta.requiresAuth && !isAuthenticated) { + // 需要登录但未登录,跳转到登录页 + next('/login') + } else if (to.path === '/login' && isAuthenticated) { + // 已登录访问登录页,跳转到首页 + next('/') + } else { + next() + } +}) + +export default router + diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts new file mode 100644 index 0000000..a94edf4 --- /dev/null +++ b/web/src/stores/auth.ts @@ -0,0 +1,58 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { User, TokenPair } from '@/types/api' + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null) + const accessToken = ref(null) + const refreshToken = ref(null) + + // 初始化时从 localStorage 恢复状态 + const init = () => { + const storedUser = localStorage.getItem('user') + const storedAccessToken = localStorage.getItem('accessToken') + const storedRefreshToken = localStorage.getItem('refreshToken') + + if (storedUser && storedAccessToken) { + user.value = JSON.parse(storedUser) + accessToken.value = storedAccessToken + refreshToken.value = storedRefreshToken + } + } + + const login = (userData: User, tokens: TokenPair) => { + user.value = userData + accessToken.value = tokens.accessToken + refreshToken.value = tokens.refreshToken + + // 保存到 localStorage + localStorage.setItem('user', JSON.stringify(userData)) + localStorage.setItem('accessToken', tokens.accessToken) + localStorage.setItem('refreshToken', tokens.refreshToken) + } + + const logout = () => { + user.value = null + accessToken.value = null + refreshToken.value = null + + localStorage.removeItem('user') + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + } + + const isAuthenticated = () => { + return !!accessToken.value + } + + init() + + return { + user, + accessToken, + refreshToken, + login, + logout, + isAuthenticated, + } +}) diff --git a/web/src/stores/counter.ts b/web/src/stores/counter.ts new file mode 100644 index 0000000..b6757ba --- /dev/null +++ b/web/src/stores/counter.ts @@ -0,0 +1,12 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const doubleCount = computed(() => count.value * 2) + function increment() { + count.value++ + } + + return { count, doubleCount, increment } +}) diff --git a/web/src/style/index.css b/web/src/style/index.css new file mode 100644 index 0000000..a461c50 --- /dev/null +++ b/web/src/style/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/web/src/types/api.ts b/web/src/types/api.ts new file mode 100644 index 0000000..5267e7b --- /dev/null +++ b/web/src/types/api.ts @@ -0,0 +1,31 @@ +// API 类型定义 +export interface ApiResponse { + code: number + message: string + data: T +} + +export interface User { + id: string + username: string + email: string + role: string + createdAt: string + updatedAt: string +} + +export interface TokenPair { + accessToken: string + refreshToken: string + expiresIn: number +} + +export interface LoginRequest { + username: string + password: string +} + +export interface LoginResponse { + user: User + tokens: TokenPair +} diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts new file mode 100644 index 0000000..23ad771 --- /dev/null +++ b/web/src/utils/api.ts @@ -0,0 +1,48 @@ +import type { ApiResponse, LoginRequest, LoginResponse } from '@/types/api' + +const API_BASE = '/api' + +class ApiClient { + private getAuthHeader(): HeadersInit { + const token = localStorage.getItem('accessToken') + return token ? { Authorization: `Bearer ${token}` } : {} + } + + async request(url: string, options: RequestInit = {}): Promise> { + const response = await fetch(`${API_BASE}${url}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...this.getAuthHeader(), + ...options.headers, + }, + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + return response.json() + } + + // 认证相关 + async login(data: LoginRequest): Promise> { + return this.request('/auth/login', { + method: 'POST', + body: JSON.stringify(data), + }) + } + + async getProfile(): Promise> { + return this.request('/auth/profile') + } + + async refreshToken(refreshToken: string): Promise> { + return this.request('/auth/refresh', { + method: 'POST', + body: JSON.stringify({ refreshToken }), + }) + } +} + +export const api = new ApiClient() diff --git a/web/src/view/auth/login.vue b/web/src/view/auth/login.vue new file mode 100644 index 0000000..dc470da --- /dev/null +++ b/web/src/view/auth/login.vue @@ -0,0 +1,195 @@ + + + diff --git a/web/src/view/home/index.vue b/web/src/view/home/index.vue new file mode 100644 index 0000000..c4d6467 --- /dev/null +++ b/web/src/view/home/index.vue @@ -0,0 +1,113 @@ + + + diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..c0f2d86 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,18 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + // Extra safety for array and object lookups, but may have false positives. + "noUncheckedIndexedAccess": true, + + // Path mapping for cleaner imports. + "paths": { + "@/*": ["./src/*"] + }, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + } +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..66b5e57 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..5988839 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,28 @@ +// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping. +{ + "extends": "@tsconfig/node24/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + // Most tools use transpilation instead of Node.js's native type-stripping. + // Bundler mode provides a smoother developer experience. + "module": "preserve", + "moduleResolution": "bundler", + + // Include Node.js types and avoid accidentally including other `@types/*` packages. + "types": ["node"], + + // Disable emitting output during `vue-tsc --build`, which is used for type-checking only. + "noEmit": true, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" + } +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..bdbdae8 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,42 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + tailwindcss(), + ], + server: { + port: 9040, + open: true, // 自动打开浏览器 + proxy: { + // 代理 API 请求到后端服务器 + '/api': { + target: 'http://localhost:9050', + changeOrigin: true, + }, + // 代理文件下载请求 + '/files': { + target: 'http://localhost:9050', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + assetsDir: 'assets', + // 清理输出目录 + emptyOutDir: true, + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, +})