diff --git a/.gitignore b/.gitignore index de28ba3..dccf43f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ dist/ build/ *.egg-info/ notes/ +node_modules/ +frontend/coverage/ +frontend/playwright-report/ +frontend/test-results/ .env .env.* *.sqlite diff --git a/README.md b/README.md index 585a9f2..11bf29d 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ For an isolated installation used outside this source checkout, see the Run `reposteward web` for the [read-only local workbench](docs/local-workbench.zh-CN.md): project guides, task continuity, review evidence and diagnostics in one browser window. +The FastAPI backend serves a bundled TypeScript/React frontend. Source development +requires Node 22.12+ and npm; installed release wheels do not require Node. `reposteward version` reports installation metadata; `reposteward doctor --local` checks configuration sources and database compatibility without authentication or migration. diff --git a/docs/local-workbench.zh-CN.md b/docs/local-workbench.zh-CN.md index 51900e3..9c2f614 100644 --- a/docs/local-workbench.zh-CN.md +++ b/docs/local-workbench.zh-CN.md @@ -27,7 +27,7 @@ reposteward overview refresh | 页面 | 可以完成的事 | 事实边界 | | --- | --- | --- | -| 今日待处理 | 跨项目查看待办、来源时间、未知/省略和下一步,跳入任务 | 复用 overview;缓存不是实时 GitHub 状态 | +| 维护总览 | 跨项目查看待办、来源时间、未知/省略和下一步,跳入任务 | 复用 overview;缓存不是实时 GitHub 状态 | | 项目空间 | 查找项目、切换 worktree、查看角色/分支/HEAD、按关注点读导览和代码依据 | 导览过期时要求重新扫描;代码文字不执行 | | 任务接续 | 查看最近任务尝试、目标/验收、未完成工作、决定、阻塞,复制接续数据 | 外部任务复用 CLI/MCP 的预算与适用性核对;维护任务显示原 Context Pack/Checkpoint | | 审阅依据 | 查看验证适用性、历史状态、任务轨迹和来源 | 历史通过/合并判定不是新的提交或合并授权 | @@ -55,9 +55,8 @@ API 没有 CORS 授权或写操作,拒绝任意文件路径和跨项目任务 文本显示,页面使用 CSP 限定脚本和资源。接口重新核对登记身份与工作区绑定。 已加载的配置文件变化会要求重启;新增配置层也应重启后重新核对生效配置。 -这是单机维护入口,使用标准库 HTTP 服务的受限适配层,不提供公网部署、反向代理、 -远程账户或多用户隔离。标准库 HTTP 服务本身只提供基础检查,本项目的固定路由、 -会话和请求边界不能被解释为面向公网的服务器方案。 +这是单机维护入口,使用 FastAPI + Uvicorn 的本地适配层。启动一个服务进程, +前后端同源;默认不会启用反向代理头信任、多进程 worker 或远程账户隔离。 不存在的数据库不创建,旧数据库不由网页迁移。需要升级时退出相关写入客户端,按 [升级与备份说明](state-upgrades.zh-CN.md)操作,再启动工作台。更换安装参见 @@ -65,15 +64,40 @@ API 没有 CORS 授权或写操作,拒绝任意文件路径和跨项目任务 ## 接口与验证 -`workbench.Workbench` 组合现有只读应用服务;`web_server` 仅负责本机 HTTP 边界。 -前端调用 `/api/overview`、`projects`、`workspace`、`code`、`tasks`、`task`、`review` -和 `settings`。接口只接受声明参数,项目/工作区/任务均使用登记 ID;不暴露 Pipeline、 +`workbench.Workbench` 组合现有只读应用服务;`web_api` 提供 FastAPI 查询与 Pydantic +契约,`web_server` 管理本机 socket 与服务生命周期。React 前端调用 `/api/v1/overview`、 +`projects`、`workspace`、`code`、`tasks`、`task`、`review` 和 `settings`。 +旧 `/api/*` 读取入口保留兼容;新接口使用 `data/meta` 响应。 +接口只接受声明参数,项目/工作区/任务均使用登记 ID;不暴露 Pipeline、 SQL、shell、原始配置或任意文件服务。没有新增网页任务表或另一套状态机。 回归测试覆盖会话/请求来源、固定路由、写方法拒绝、跨项目隔离、绑定变化、旧库、 读取不变性以及 CLI/MCP 与网页查询的一致性。浏览器交互与打包检查在加固验证容器中 执行;用于浏览器验证的额外工具不成为日常安装依赖。 -设计依据:[Python HTTP 服务](https://docs.python.org/3.12/library/http.server.html)、 +## 前端开发与分发 + +前端源码在 `frontend/`,采用 TypeScript、React、React Router、TanStack Query 和 Vite。 +项目选择进入浏览器路由;Query 缓存只代表本地读取缓存,不能替代 GitHub 观测时间。 +切换项目后旧请求结果不会进入新项目页面。 + +源码开发需要 Node 22.12+ 与 npm。`uv sync --locked` 的构建步骤安装锁定前端依赖, +从 FastAPI 导出 OpenAPI、生成 TypeScript 类型并构建静态页面;`uv build` 将页面与 +逐文件摘要清单打入 wheel / sdist。已构建发行包的日常安装和运行不需要 Node。 +构建使用清理后的环境,不把 GitHub 凭据传给 Node;依赖脚本默认禁用。 + +`frontend/openapi.json` 和 `frontend/src/api/generated/schema.ts` 是生成契约; +后端协议测试核对 OpenAPI 漂移。`npm run check` 在前端目录执行类型、ESLint 和交互检查, +Python 回归也包含这项检查。测试应在加固 verifier 中运行。构建产物缺失或摘要 +不匹配会明确失败,避免安装包静默使用旧页面。 + +需要热更新时,先启动 `reposteward web --port 8787`,在 `frontend/` 运行 +`npm run dev`。浏览器打开 `http://127.0.0.1:5173/#session=<终端链接中的会话>`, +会话仅由浏览器携带;不要将它设置成 Vite 环境变量或写入文件。开发代理固定在 +127.0.0.1:5173,校验来源后将 `/api/` 转发到本机后端。后端端口可通过 +`REPOSTEWARD_DEV_BACKEND=http://127.0.0.1:` 指定。正式服务仍直接提供打包页面。 + +设计依据:[FastAPI](https://fastapi.tiangolo.com/tutorial/bigger-applications/)、 +[Vite 后端集成](https://vite.dev/guide/backend-integration)、 [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy)、 [Fetch Metadata](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site)。 diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..ec64537 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,11 @@ +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; + +export default defineConfig( + { ignores: ["dist/**", "src/api/generated/**", "coverage/**"] }, + { + files: ["**/*.{ts,tsx}"], + extends: [js.configs.recommended, tseslint.configs.recommended], + }, +); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..f4976b6 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + RepoSteward · 本地工作台 + + +
+ + + diff --git a/frontend/openapi.json b/frontend/openapi.json new file mode 100644 index 0000000..2da9a70 --- /dev/null +++ b/frontend/openapi.json @@ -0,0 +1,1401 @@ +{ + "components": { + "schemas": { + "Attention": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "issue_number": { + "default": 0, + "title": "Issue Number", + "type": "integer" + }, + "next_command": { + "default": "", + "title": "Next Command", + "type": "string" + }, + "priority": { + "title": "Priority", + "type": "integer" + }, + "pull_number": { + "default": 0, + "title": "Pull Number", + "type": "integer" + }, + "reason_code": { + "title": "Reason Code", + "type": "string" + }, + "run_id": { + "default": "", + "title": "Run Id", + "type": "string" + }, + "source_updated_at": { + "default": "", + "title": "Source Updated At", + "type": "string" + }, + "summary": { + "title": "Summary", + "type": "string" + } + }, + "required": [ + "id", + "priority", + "summary", + "reason_code" + ], + "title": "Attention", + "type": "object" + }, + "Code": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "evidence_id": { + "title": "Evidence Id", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/components/schemas/Source" + }, + { + "type": "null" + } + ] + }, + "status": { + "title": "Status", + "type": "string" + }, + "text": { + "title": "Text", + "type": "string" + }, + "truncated": { + "default": false, + "title": "Truncated", + "type": "boolean" + } + }, + "required": [ + "evidence_id", + "status", + "text" + ], + "title": "Code", + "type": "object" + }, + "Commands": { + "properties": { + "mcp_clients": { + "additionalProperties": { + "type": "string" + }, + "title": "Mcp Clients", + "type": "object" + }, + "scan": { + "title": "Scan", + "type": "string" + } + }, + "required": [ + "scan", + "mcp_clients" + ], + "title": "Commands", + "type": "object" + }, + "Guide": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "limitations": { + "items": { + "type": "string" + }, + "title": "Limitations", + "type": "array" + }, + "reading_path": { + "items": { + "$ref": "#/components/schemas/ReadingStep" + }, + "title": "Reading Path", + "type": "array" + }, + "repository": { + "title": "Repository", + "type": "string" + }, + "scanned_at": { + "default": "", + "title": "Scanned At", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "status", + "repository" + ], + "title": "Guide", + "type": "object" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array" + } + }, + "title": "HTTPValidationError", + "type": "object" + }, + "JsonValue": {}, + "Meta": { + "properties": { + "api_version": { + "const": "1", + "default": "1", + "title": "Api Version", + "type": "string" + }, + "generated_at": { + "title": "Generated At", + "type": "string" + }, + "request_id": { + "title": "Request Id", + "type": "string" + } + }, + "required": [ + "request_id", + "generated_at" + ], + "title": "Meta", + "type": "object" + }, + "Overview": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "complete": { + "title": "Complete", + "type": "boolean" + }, + "excluded_projects": { + "title": "Excluded Projects", + "type": "integer" + }, + "observed_at": { + "title": "Observed At", + "type": "string" + }, + "omitted_projects": { + "title": "Omitted Projects", + "type": "integer" + }, + "overview_digest": { + "title": "Overview Digest", + "type": "string" + }, + "projects": { + "items": { + "$ref": "#/components/schemas/OverviewProject" + }, + "title": "Projects", + "type": "array" + } + }, + "required": [ + "projects", + "complete", + "omitted_projects", + "excluded_projects", + "observed_at", + "overview_digest" + ], + "title": "Overview", + "type": "object" + }, + "OverviewProject": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "complete": { + "title": "Complete", + "type": "boolean" + }, + "items": { + "items": { + "$ref": "#/components/schemas/Attention" + }, + "title": "Items", + "type": "array" + }, + "omitted": { + "title": "Omitted", + "type": "integer" + }, + "project_id": { + "title": "Project Id", + "type": "string" + }, + "repository": { + "title": "Repository", + "type": "string" + }, + "sources": { + "$ref": "#/components/schemas/SourceStatus" + } + }, + "required": [ + "project_id", + "repository", + "complete", + "items", + "omitted", + "sources" + ], + "title": "OverviewProject", + "type": "object" + }, + "Policy": { + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "mode": { + "title": "Mode", + "type": "string" + }, + "task_access": { + "title": "Task Access", + "type": "boolean" + } + }, + "required": [ + "mode", + "enabled", + "task_access" + ], + "title": "Policy", + "type": "object" + }, + "Project": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "created_at": { + "title": "Created At", + "type": "string" + }, + "host": { + "title": "Host", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "identity": { + "title": "Identity", + "type": "string" + }, + "missing_workspaces": { + "default": 0, + "title": "Missing Workspaces", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "policy": { + "$ref": "#/components/schemas/Policy" + }, + "repository": { + "title": "Repository", + "type": "string" + }, + "workspace_check_omitted": { + "default": 0, + "title": "Workspace Check Omitted", + "type": "integer" + }, + "workspace_count": { + "default": 0, + "title": "Workspace Count", + "type": "integer" + }, + "workspaces": { + "items": { + "$ref": "#/components/schemas/WorkspaceRef" + }, + "title": "Workspaces", + "type": "array" + } + }, + "required": [ + "id", + "identity", + "host", + "repository", + "name", + "created_at", + "policy" + ], + "title": "Project", + "type": "object" + }, + "Projects": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "omitted": { + "title": "Omitted", + "type": "integer" + }, + "projects": { + "items": { + "$ref": "#/components/schemas/Project" + }, + "title": "Projects", + "type": "array" + }, + "public_write": { + "const": false, + "default": false, + "title": "Public Write", + "type": "boolean" + } + }, + "required": [ + "projects", + "omitted" + ], + "title": "Projects", + "type": "object" + }, + "ReadingStep": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "language": { + "title": "Language", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/Source" + }, + "step": { + "title": "Step", + "type": "integer" + }, + "summary": { + "default": "", + "title": "Summary", + "type": "string" + } + }, + "required": [ + "step", + "path", + "reason", + "language", + "source" + ], + "title": "ReadingStep", + "type": "object" + }, + "Response_Code_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Code" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Code]", + "type": "object" + }, + "Response_Overview_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Overview" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Overview]", + "type": "object" + }, + "Response_Projects_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Projects" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Projects]", + "type": "object" + }, + "Response_Review_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Review" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Review]", + "type": "object" + }, + "Response_Session_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Session" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Session]", + "type": "object" + }, + "Response_Settings_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Settings" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Settings]", + "type": "object" + }, + "Response_Task_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Task" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Task]", + "type": "object" + }, + "Response_Tasks_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Tasks" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Tasks]", + "type": "object" + }, + "Response_Workspace_": { + "properties": { + "data": { + "$ref": "#/components/schemas/Workspace" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "Response[Workspace]", + "type": "object" + }, + "Review": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "decision_freshness": { + "const": "historical_only", + "title": "Decision Freshness", + "type": "string" + }, + "publication_eligible": { + "const": false, + "title": "Publication Eligible", + "type": "boolean" + }, + "run_id": { + "title": "Run Id", + "type": "string" + }, + "trace": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Trace", + "type": "object" + }, + "verification": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Verification" + } + }, + "required": [ + "run_id", + "trace", + "verification", + "decision_freshness", + "publication_eligible" + ], + "title": "Review", + "type": "object" + }, + "Session": { + "properties": { + "api_version": { + "const": "1", + "default": "1", + "title": "Api Version", + "type": "string" + }, + "capabilities": { + "items": { + "const": "read_local", + "type": "string" + }, + "title": "Capabilities", + "type": "array" + }, + "expires_in_seconds": { + "title": "Expires In Seconds", + "type": "integer" + }, + "frontend_digest": { + "title": "Frontend Digest", + "type": "string" + } + }, + "required": [ + "capabilities", + "expires_in_seconds", + "frontend_digest" + ], + "title": "Session", + "type": "object" + }, + "Settings": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "configuration": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Configuration", + "type": "object" + }, + "databases": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Databases", + "type": "object" + }, + "installation": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Installation", + "type": "object" + }, + "next_actions": { + "items": { + "type": "string" + }, + "title": "Next Actions", + "type": "array" + } + }, + "required": [ + "installation", + "configuration", + "databases", + "next_actions" + ], + "title": "Settings", + "type": "object" + }, + "Source": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "digest": { + "title": "Digest", + "type": "string" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "evidence_id": { + "title": "Evidence Id", + "type": "string" + }, + "line": { + "title": "Line", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "trust": { + "title": "Trust", + "type": "string" + }, + "url": { + "default": "", + "title": "Url", + "type": "string" + } + }, + "required": [ + "evidence_id", + "path", + "line", + "end_line", + "digest", + "trust" + ], + "title": "Source", + "type": "object" + }, + "SourceStatus": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "error": { + "default": "", + "title": "Error", + "type": "string" + }, + "fetched_at": { + "default": "", + "title": "Fetched At", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "SourceStatus", + "type": "object" + }, + "Task": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "command": { + "title": "Command", + "type": "string" + }, + "context": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Context" + }, + "context_status": { + "title": "Context Status", + "type": "string" + }, + "current_applicability": { + "title": "Current Applicability", + "type": "string" + }, + "kind": { + "enum": [ + "external", + "managed" + ], + "title": "Kind", + "type": "string" + }, + "run_id": { + "title": "Run Id", + "type": "string" + } + }, + "required": [ + "run_id", + "kind", + "context", + "context_status", + "current_applicability", + "command" + ], + "title": "Task", + "type": "object" + }, + "TaskAttempt": { + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "issue_number": { + "title": "Issue Number", + "type": "integer" + }, + "stage": { + "title": "Stage", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "updated_at": { + "title": "Updated At", + "type": "string" + } + }, + "required": [ + "id", + "issue_number", + "stage", + "status", + "updated_at", + "title" + ], + "title": "TaskAttempt", + "type": "object" + }, + "Tasks": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "omitted": { + "title": "Omitted", + "type": "integer" + }, + "status": { + "title": "Status", + "type": "string" + }, + "tasks": { + "items": { + "$ref": "#/components/schemas/TaskAttempt" + }, + "title": "Tasks", + "type": "array" + } + }, + "required": [ + "tasks", + "omitted", + "status" + ], + "title": "Tasks", + "type": "object" + }, + "ValidationError": { + "properties": { + "ctx": { + "title": "Context", + "type": "object" + }, + "input": { + "title": "Input" + }, + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "title": "Location", + "type": "array" + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + }, + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError", + "type": "object" + }, + "Workspace": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "properties": { + "binding": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Binding", + "type": "object" + }, + "command_shell": { + "title": "Command Shell", + "type": "string" + }, + "commands": { + "$ref": "#/components/schemas/Commands" + }, + "guide": { + "$ref": "#/components/schemas/Guide" + }, + "policy": { + "$ref": "#/components/schemas/Policy" + }, + "project": { + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + }, + "title": "Project", + "type": "object" + }, + "workspace": { + "$ref": "#/components/schemas/WorkspaceState" + } + }, + "required": [ + "project", + "binding", + "workspace", + "policy", + "guide", + "commands", + "command_shell" + ], + "title": "Workspace", + "type": "object" + }, + "WorkspaceRef": { + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "root": { + "title": "Root", + "type": "string" + } + }, + "required": [ + "id", + "root" + ], + "title": "WorkspaceRef", + "type": "object" + }, + "WorkspaceState": { + "properties": { + "branch": { + "title": "Branch", + "type": "string" + }, + "dirty": { + "title": "Dirty", + "type": "boolean" + }, + "head": { + "title": "Head", + "type": "string" + } + }, + "required": [ + "head", + "branch", + "dirty" + ], + "title": "WorkspaceState", + "type": "object" + } + } + }, + "info": { + "title": "RepoSteward local workbench", + "version": "1" + }, + "openapi": "3.1.0", + "paths": { + "/api/v1/code": { + "get": { + "operationId": "code", + "parameters": [ + { + "in": "query", + "name": "project_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Project Id", + "type": "string" + } + }, + { + "in": "query", + "name": "binding_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Binding Id", + "type": "string" + } + }, + { + "in": "query", + "name": "evidence_id", + "required": true, + "schema": { + "pattern": "^code:[0-9a-f]{64}$", + "title": "Evidence Id", + "type": "string" + } + }, + { + "in": "query", + "name": "start_line", + "required": false, + "schema": { + "default": "1", + "pattern": "^[1-9][0-9]{0,6}$", + "title": "Start Line", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Code_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Code" + } + }, + "/api/v1/overview": { + "get": { + "operationId": "overview", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Overview_" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Overview" + } + }, + "/api/v1/projects": { + "get": { + "operationId": "projects", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Projects_" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Projects" + } + }, + "/api/v1/review": { + "get": { + "operationId": "review", + "parameters": [ + { + "in": "query", + "name": "project_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Project Id", + "type": "string" + } + }, + { + "in": "query", + "name": "run_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Run Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Review_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Review" + } + }, + "/api/v1/session": { + "get": { + "operationId": "session", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Session_" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Current Session" + } + }, + "/api/v1/settings": { + "get": { + "operationId": "settings", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Settings_" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Settings" + } + }, + "/api/v1/task": { + "get": { + "operationId": "task", + "parameters": [ + { + "in": "query", + "name": "project_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Project Id", + "type": "string" + } + }, + { + "in": "query", + "name": "run_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Run Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Task_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Task" + } + }, + "/api/v1/tasks": { + "get": { + "operationId": "tasks", + "parameters": [ + { + "in": "query", + "name": "project_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Project Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Tasks_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Tasks" + } + }, + "/api/v1/workspace": { + "get": { + "operationId": "workspace", + "parameters": [ + { + "in": "query", + "name": "project_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Project Id", + "type": "string" + } + }, + { + "in": "query", + "name": "binding_id", + "required": true, + "schema": { + "pattern": "^[0-9a-f]{32}$", + "title": "Binding Id", + "type": "string" + } + }, + { + "in": "query", + "name": "focus", + "required": false, + "schema": { + "default": "", + "maxLength": 2000, + "title": "Focus", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Response_Workspace_" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Workspace" + } + } + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..7ff888e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4822 @@ +{ + "name": "reposteward-workbench", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reposteward-workbench", + "version": "0.1.0", + "dependencies": { + "@tanstack/react-query": "^5.90.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.14.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^5.1.0", + "eslint": "^9.39.0", + "jsdom": "^27.0.0", + "openapi-typescript": "^7.10.0", + "typescript": "^5.9.0", + "typescript-eslint": "^8.46.0", + "vite": "^7.2.0", + "vitest": "^3.2.0" + }, + "engines": { + "node": ">=22.12" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", + "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz", + "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.19", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.19.tgz", + "integrity": "sha512-o/0VgsBXgwcY1lyeqcVtSGdTQAPnVggo0fbFVPlxl5XVDKUcVH0OLRqt3CbkwByT5FU305E0iE0O7MzThjDblw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.3.1", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tanstack/query-core": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", + "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", + "integrity": "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.69.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.422", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", + "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", + "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", + "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", + "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.11" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", + "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.69.0.tgz", + "integrity": "sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.69.0", + "@typescript-eslint/parser": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "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 + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1c772d7 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "reposteward-workbench", + "private": true, + "version": "0.1.0", + "type": "module", + "engines": {"node": ">=22.12"}, + "scripts": { + "dev": "vite --host 127.0.0.1", + "generate": "openapi-typescript openapi.json -o src/api/generated/schema.ts", + "build": "npm run generate && tsc --noEmit && vite build", + "check": "tsc --noEmit && eslint . && vitest run" + }, + "dependencies": { + "@tanstack/react-query": "^5.90.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.1.0", + "jsdom": "^27.0.0", + "eslint": "^9.39.0", + "openapi-typescript": "^7.10.0", + "typescript": "^5.9.0", + "typescript-eslint": "^8.46.0", + "vite": "^7.2.0", + "vitest": "^3.2.0" + } +} diff --git a/frontend/src/api/client.test.ts b/frontend/src/api/client.test.ts new file mode 100644 index 0000000..6db8be0 --- /dev/null +++ b/frontend/src/api/client.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { legacyRoute } from './client'; + +describe('bookmarks from the original workbench', () => { + it('preserves the selected workspace and task review', () => { + const project = 'a'.repeat(32), item = 'b'.repeat(32); + expect(legacyRoute(`#projects/${project}/${item}`)).toBe(`/projects/${project}/workspaces/${item}`); + expect(legacyRoute(`#tasks/${project}/${item}`)).toBe(`/projects/${project}/tasks/${item}`); + expect(legacyRoute(`#review/${project}/${item}`)).toBe(`/projects/${project}/tasks/${item}/review`); + expect(legacyRoute('#today')).toBe('/'); + expect(legacyRoute('#settings')).toBe('/settings'); + }); + it('does not interpret unknown fragments as resource identifiers', () => { + for (const hash of ['#projects/../../settings', '#session=private', '#projects/%2f', '#https://foreign.example']) { + expect(legacyRoute(hash)).toBeUndefined(); + } + }); +}); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..f4908d6 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,102 @@ +import { useQuery } from "@tanstack/react-query"; +import type { components } from "./generated/schema"; + +type Schemas = components["schemas"]; +export type Project = Schemas["Project"]; +export type Workspace = Schemas["Workspace"]; +export type ReadModels = { + projects: Schemas["Projects"]; + overview: Schemas["Overview"]; + workspace: Workspace; + code: Schemas["Code"]; + tasks: Schemas["Tasks"]; + task: Schemas["Task"]; + review: Schemas["Review"]; + settings: Schemas["Settings"]; + session: Schemas["Session"]; +}; + +const storageKey = "reposteward-local-session"; +let token = ""; + +export function legacyRoute(hash: string): string | undefined { + const match = /^(today|projects|tasks|review|settings)(?:\/([0-9a-f]{32})(?:\/([0-9a-f]{32}))?)?$/.exec(hash.slice(1)); + if (!match) return; + const [, view, project, item] = match; + if (view === "today") return "/"; + if (view === "settings") return "/settings"; + if (!project) return view === "projects" ? "/projects" : "/tasks"; + if (view === "projects") return `/projects/${project}${item ? `/workspaces/${item}` : ""}`; + return `/projects/${project}/tasks${item ? `/${item}${view === "review" ? "/review" : ""}` : ""}`; +} + +export function connectSession() { + try { + token = sessionStorage.getItem(storageKey) || ""; + } catch { + /* memory fallback */ + } + if (location.hash.startsWith("#session=")) { + const candidate = location.hash.slice(9); + if (/^[A-Za-z0-9_-]{43}$/.test(candidate)) { + token = candidate; + try { + sessionStorage.setItem(storageKey, token); + } catch { + /* memory fallback */ + } + } + history.replaceState(null, "", location.pathname + location.search); + } else { + const route = legacyRoute(location.hash); + if (route) history.replaceState(null, "", route); + } + return Boolean(token); +} + +export class ApiError extends Error { + constructor( + public status: number, + public code: string, + message: string, + ) { + super(message); + } +} + +export async function read( + name: K, + params: Record = {}, + signal?: AbortSignal, +): Promise { + const query = new URLSearchParams(params).toString(); + const response = await fetch(`/api/v1/${name}${query ? "?" + query : ""}`, { + headers: { Authorization: "Bearer " + token }, + signal, + cache: "no-store", + credentials: "omit", + }); + const value = await response.json(); + if (!response.ok) + throw new ApiError( + response.status, + value.error?.code || "unavailable", + value.error?.message || "读取失败,请重试。", + ); + return value.data; +} + +export function useRead( + name: K, + params: Record = {}, + enabled = true, +) { + return useQuery({ + queryKey: [name, params], + queryFn: ({ signal }) => read(name, params, signal), + enabled, + staleTime: name === "code" ? 0 : 15_000, + refetchOnWindowFocus: false, + retry: false, + }); +} diff --git a/frontend/src/api/generated/schema.ts b/frontend/src/api/generated/schema.ts new file mode 100644 index 0000000..0cd8f04 --- /dev/null +++ b/frontend/src/api/generated/schema.ts @@ -0,0 +1,866 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/v1/code": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Code */ + get: operations["code"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/overview": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Overview */ + get: operations["overview"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Projects */ + get: operations["projects"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/review": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Review */ + get: operations["review"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Current Session */ + get: operations["session"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/settings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Settings */ + get: operations["settings"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/task": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Task */ + get: operations["task"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/tasks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Tasks */ + get: operations["tasks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/workspace": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Workspace */ + get: operations["workspace"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** Attention */ + Attention: { + /** Id */ + id: string; + /** + * Issue Number + * @default 0 + */ + issue_number: number; + /** + * Next Command + * @default + */ + next_command: string; + /** Priority */ + priority: number; + /** + * Pull Number + * @default 0 + */ + pull_number: number; + /** Reason Code */ + reason_code: string; + /** + * Run Id + * @default + */ + run_id: string; + /** + * Source Updated At + * @default + */ + source_updated_at: string; + /** Summary */ + summary: string; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Code */ + Code: { + /** Evidence Id */ + evidence_id: string; + source?: components["schemas"]["Source"] | null; + /** Status */ + status: string; + /** Text */ + text: string; + /** + * Truncated + * @default false + */ + truncated: boolean; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Commands */ + Commands: { + /** Mcp Clients */ + mcp_clients: { + [key: string]: string; + }; + /** Scan */ + scan: string; + }; + /** Guide */ + Guide: { + /** Limitations */ + limitations?: string[]; + /** Reading Path */ + reading_path?: components["schemas"]["ReadingStep"][]; + /** Repository */ + repository: string; + /** + * Scanned At + * @default + */ + scanned_at: string; + /** Status */ + status: string; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + JsonValue: unknown; + /** Meta */ + Meta: { + /** + * Api Version + * @default 1 + * @constant + */ + api_version: "1"; + /** Generated At */ + generated_at: string; + /** Request Id */ + request_id: string; + }; + /** Overview */ + Overview: { + /** Complete */ + complete: boolean; + /** Excluded Projects */ + excluded_projects: number; + /** Observed At */ + observed_at: string; + /** Omitted Projects */ + omitted_projects: number; + /** Overview Digest */ + overview_digest: string; + /** Projects */ + projects: components["schemas"]["OverviewProject"][]; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** OverviewProject */ + OverviewProject: { + /** Complete */ + complete: boolean; + /** Items */ + items: components["schemas"]["Attention"][]; + /** Omitted */ + omitted: number; + /** Project Id */ + project_id: string; + /** Repository */ + repository: string; + sources: components["schemas"]["SourceStatus"]; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Policy */ + Policy: { + /** Enabled */ + enabled: boolean; + /** Mode */ + mode: string; + /** Task Access */ + task_access: boolean; + }; + /** Project */ + Project: { + /** Created At */ + created_at: string; + /** Host */ + host: string; + /** Id */ + id: string; + /** Identity */ + identity: string; + /** + * Missing Workspaces + * @default 0 + */ + missing_workspaces: number; + /** Name */ + name: string; + policy: components["schemas"]["Policy"]; + /** Repository */ + repository: string; + /** + * Workspace Check Omitted + * @default 0 + */ + workspace_check_omitted: number; + /** + * Workspace Count + * @default 0 + */ + workspace_count: number; + /** Workspaces */ + workspaces?: components["schemas"]["WorkspaceRef"][]; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Projects */ + Projects: { + /** Omitted */ + omitted: number; + /** Projects */ + projects: components["schemas"]["Project"][]; + /** + * Public Write + * @default false + * @constant + */ + public_write: false; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** ReadingStep */ + ReadingStep: { + /** Language */ + language: string; + /** Path */ + path: string; + /** Reason */ + reason: string; + source: components["schemas"]["Source"]; + /** Step */ + step: number; + /** + * Summary + * @default + */ + summary: string; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Response[Code] */ + Response_Code_: { + data: components["schemas"]["Code"]; + meta: components["schemas"]["Meta"]; + }; + /** Response[Overview] */ + Response_Overview_: { + data: components["schemas"]["Overview"]; + meta: components["schemas"]["Meta"]; + }; + /** Response[Projects] */ + Response_Projects_: { + data: components["schemas"]["Projects"]; + meta: components["schemas"]["Meta"]; + }; + /** Response[Review] */ + Response_Review_: { + data: components["schemas"]["Review"]; + meta: components["schemas"]["Meta"]; + }; + /** Response[Session] */ + Response_Session_: { + data: components["schemas"]["Session"]; + meta: components["schemas"]["Meta"]; + }; + /** Response[Settings] */ + Response_Settings_: { + data: components["schemas"]["Settings"]; + meta: components["schemas"]["Meta"]; + }; + /** Response[Task] */ + Response_Task_: { + data: components["schemas"]["Task"]; + meta: components["schemas"]["Meta"]; + }; + /** Response[Tasks] */ + Response_Tasks_: { + data: components["schemas"]["Tasks"]; + meta: components["schemas"]["Meta"]; + }; + /** Response[Workspace] */ + Response_Workspace_: { + data: components["schemas"]["Workspace"]; + meta: components["schemas"]["Meta"]; + }; + /** Review */ + Review: { + /** + * Decision Freshness + * @constant + */ + decision_freshness: "historical_only"; + /** + * Publication Eligible + * @constant + */ + publication_eligible: false; + /** Run Id */ + run_id: string; + /** Trace */ + trace: { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Verification */ + verification: { + [key: string]: components["schemas"]["JsonValue"]; + } | null; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Session */ + Session: { + /** + * Api Version + * @default 1 + * @constant + */ + api_version: "1"; + /** Capabilities */ + capabilities: "read_local"[]; + /** Expires In Seconds */ + expires_in_seconds: number; + /** Frontend Digest */ + frontend_digest: string; + }; + /** Settings */ + Settings: { + /** Configuration */ + configuration: { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Databases */ + databases: { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Installation */ + installation: { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Next Actions */ + next_actions: string[]; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Source */ + Source: { + /** Digest */ + digest: string; + /** End Line */ + end_line: number; + /** Evidence Id */ + evidence_id: string; + /** Line */ + line: number; + /** Path */ + path: string; + /** Trust */ + trust: string; + /** + * Url + * @default + */ + url: string; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** SourceStatus */ + SourceStatus: { + /** + * Error + * @default + */ + error: string; + /** + * Fetched At + * @default + */ + fetched_at: string; + /** Status */ + status: string; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Task */ + Task: { + /** Command */ + command: string; + /** Context */ + context: { + [key: string]: components["schemas"]["JsonValue"]; + } | null; + /** Context Status */ + context_status: string; + /** Current Applicability */ + current_applicability: string; + /** + * Kind + * @enum {string} + */ + kind: "external" | "managed"; + /** Run Id */ + run_id: string; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** TaskAttempt */ + TaskAttempt: { + /** Id */ + id: string; + /** Issue Number */ + issue_number: number; + /** Stage */ + stage: string; + /** Status */ + status: string; + /** Title */ + title: string | null; + /** Updated At */ + updated_at: string; + }; + /** Tasks */ + Tasks: { + /** Omitted */ + omitted: number; + /** Status */ + status: string; + /** Tasks */ + tasks: components["schemas"]["TaskAttempt"][]; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** ValidationError */ + ValidationError: { + /** Context */ + ctx?: Record; + /** Input */ + input?: unknown; + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + /** Workspace */ + Workspace: { + /** Binding */ + binding: { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** Command Shell */ + command_shell: string; + commands: components["schemas"]["Commands"]; + guide: components["schemas"]["Guide"]; + policy: components["schemas"]["Policy"]; + /** Project */ + project: { + [key: string]: components["schemas"]["JsonValue"]; + }; + workspace: components["schemas"]["WorkspaceState"]; + } & { + [key: string]: components["schemas"]["JsonValue"]; + }; + /** WorkspaceRef */ + WorkspaceRef: { + /** Id */ + id: string; + /** Root */ + root: string; + }; + /** WorkspaceState */ + WorkspaceState: { + /** Branch */ + branch: string; + /** Dirty */ + dirty: boolean; + /** Head */ + head: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + code: { + parameters: { + query: { + project_id: string; + binding_id: string; + evidence_id: string; + start_line?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Code_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + overview: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Overview_"]; + }; + }; + }; + }; + projects: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Projects_"]; + }; + }; + }; + }; + review: { + parameters: { + query: { + project_id: string; + run_id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Review_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + session: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Session_"]; + }; + }; + }; + }; + settings: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Settings_"]; + }; + }; + }; + }; + task: { + parameters: { + query: { + project_id: string; + run_id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Task_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + tasks: { + parameters: { + query: { + project_id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Tasks_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + workspace: { + parameters: { + query: { + project_id: string; + binding_id: string; + focus?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Response_Workspace_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} diff --git a/frontend/src/components.test.tsx b/frontend/src/components.test.tsx new file mode 100644 index 0000000..7bbd36b --- /dev/null +++ b/frontend/src/components.test.tsx @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { Copy, Detail, SafeLink } from "./components"; + +afterEach(cleanup); +describe("repository data and task handoff", () => { + it("renders remote HTML as inert text and rejects executable URLs", () => { + render( + <> + unsafe + " }} + /> + , + ); + expect(document.querySelector("a")).toBeNull(); + expect(document.querySelector("img")).toBeNull(); + expect(screen.getByText(/onerror/).textContent).toContain(" { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText }, + }); + render( + , + ); + fireEvent.click(screen.getByText("复制接续数据")); + expect(await screen.findByText("已复制")).toBeTruthy(); + expect(writeText).toHaveBeenCalledWith( + '{"next_action":"inspect current changes"}', + ); + }); + it("preserves a manual fallback if clipboard access is denied", async () => { + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText: vi.fn().mockRejectedValue(new Error("denied")) }, + }); + render(); + fireEvent.click(screen.getByText("复制")); + expect(await screen.findByText("请手动选择文本复制")).toBeTruthy(); + }); +}); diff --git a/frontend/src/components.tsx b/frontend/src/components.tsx new file mode 100644 index 0000000..37133da --- /dev/null +++ b/frontend/src/components.tsx @@ -0,0 +1,177 @@ +import { useState, type ReactNode } from "react"; + +const names: Record = { + current: "当前有效", + stale: "已过期", + not_scanned: "尚未扫描", + missing: "尚未创建", + compatible: "兼容", + migration_required: "需要升级", + newer_than_supported: "安装版本过旧", + unavailable: "暂不可用", + snapshot_required: "等待稳定快照", + not_checked: "未核对当前适用性", + matches: "适用于当前快照", + not_verified: "当前未验证", + passed: "历史验证通过", + failed: "失败", + running: "进行中", + ready: "准备完成", + submitted: "已提交", + merged: "已合并", + blocked: "有阻塞", + complete: "完成", + cached: "GitHub 缓存", + not_refreshed: "尚未同步", + refresh_failed: "同步失败", + maintainer: "维护", + contributor: "贡献", + unconfigured: "待配置", + unknown: "未知", +}; +export const statusName = (value: string) => names[value] || value || "未知"; +export const obj = (value: unknown): Record => + value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : {}; +export const arr = (value: unknown): unknown[] => + Array.isArray(value) ? value : []; +export const str = (value: unknown): string => + typeof value === "string" + ? value + : value == null + ? "" + : JSON.stringify(value); +export const when = (value: unknown) => + value && Number.isFinite(new Date(str(value)).getTime()) + ? new Date(str(value)).toLocaleString("zh-CN", { hour12: false }) + : "尚无时间记录"; + +export function Badge({ value, tone = "" }: { value: string; tone?: string }) { + return {statusName(value)}; +} +export function Empty({ + title, + children, +}: { + title: string; + children?: ReactNode; +}) { + return ( +
+ {title} +
{children}
+
+ ); +} +export function Notice({ children }: { children: ReactNode }) { + return
{children}
; +} +export function Detail({ title, value }: { title: string; value: unknown }) { + return ( +
+ {title} +
{JSON.stringify(value, null, 2)}
+
+ ); +} +export function List({ + items, + empty = "尚无记录", +}: { + items: unknown; + empty?: string; +}) { + return arr(items).length ? ( +
    + {arr(items).map((item, i) => ( +
  • {str(item)}
  • + ))} +
+ ) : ( +

{empty}

+ ); +} +export function Copy({ + value, + label = "复制", +}: { + value: string; + label?: string; +}) { + const [result, setResult] = useState(""); + return ( + + + {result} + + ); +} +export function Command({ value }: { value: string }) { + return ( +
+ {value} + +
+ ); +} +export function SafeLink({ + href, + children, +}: { + href: string; + children: ReactNode; +}) { + try { + const url = new URL(href); + if (url.protocol === "https:" && !url.username && !url.password) + return ( + + {children} ↗ + + ); + } catch { + /* inert remote input */ + } + return {children}; +} +export function ReadState({ + query, +}: { + query: { isPending: boolean; error: Error | null }; +}) { + if (query.error) + return ( +
+ {query.error.message} +
+ ); + return query.isPending ? ( +

+ 正在读取本地数据… +

+ ) : null; +} +export function Pairs({ values }: { values: [string, unknown][] }) { + return ( +
+ {values.map(([label, value]) => ( +
+
{label}
+
{str(value) || "未知"}
+
+ ))} +
+ ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..f460b5c --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,165 @@ +import { Component, type ReactNode } from "react"; +import { createRoot } from "react-dom/client"; +import { + BrowserRouter, + Link, + NavLink, + Route, + Routes, + useLocation, +} from "react-router-dom"; +import { + QueryClient, + QueryClientProvider, + useQueryClient, +} from "@tanstack/react-query"; +import { connectSession, useRead } from "./api/client"; +import { Badge, Command, Empty, Notice } from "./components"; +import { OverviewPage } from "./pages/overview"; +import { ProjectPage, ProjectsPage } from "./pages/projects"; +import { TasksPage } from "./pages/tasks"; +import { SettingsPage } from "./pages/settings"; +import "./style.css"; + +class PageBoundary extends Component< + { children: ReactNode }, + { failed: boolean } +> { + state = { failed: false }; + static getDerivedStateFromError() { + return { failed: true }; + } + render() { + return this.state.failed ? ( + 页面暂不可用,请重新读取或查看设置诊断。 + ) : ( + this.props.children + ); + } +} + +function App({ connected }: { connected: boolean }) { + const cache = useQueryClient(); + const location = useLocation(); + const session = useRead("session", {}, connected); + const projects = useRead("projects", {}, connected); + return ( +
+ + 跳到主要内容 + + +
+
+ + 工作空间 /{" "} + {location.pathname.startsWith("/projects") + ? "项目" + : location.pathname.startsWith("/settings") + ? "设置" + : "维护"} + +
+ + +
+
+
+ {!connected ? ( + + 请使用终端打印的完整会话链接打开。 + + + ) : session.error ? ( + + {session.error.message} + + + ) : ( + + + } /> + } /> + } /> + } + /> + } + /> + } + /> + } + /> + } /> + } /> + } /> + + 返回总览 + + } + /> + + + )} +
+
+
+ ); +} + +const queryClient = new QueryClient(); +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/frontend/src/pages/overview.tsx b/frontend/src/pages/overview.tsx new file mode 100644 index 0000000..0eb0c68 --- /dev/null +++ b/frontend/src/pages/overview.tsx @@ -0,0 +1,123 @@ +import { Link } from "react-router-dom"; +import { useRead } from "../api/client"; +import { + Badge, + Command, + Copy, + Detail, + Empty, + Notice, + ReadState, + when, +} from "../components"; + +export function OverviewPage() { + const query = useRead("overview"); + const result = query.data; + return ( + <> +
+

维护总览

+

查看需要关注的事项,再进入项目继续处理。

+
+ + {result && ( + <> +
+
+ {result.projects.length} + 当前范围内项目 +
+
+ + {result.projects.reduce((n, p) => n + p.items.length, 0)} + + 本页关注项 +
+
+ {result.complete ? "范围内完整" : "部分数据"} + 覆盖不代表已实时同步 +
+
+ {!result.projects.length && ( + + 关联本地目录并配置仓库策略后,它会出现在维护总览。 + + + )} + {result.projects.map((project) => ( +
+
+

+ + {project.repository} + +

+ +
+

+ 最近 GitHub 缓存 · {when(project.sources.fetched_at)} +

+ {project.items.map((item) => ( +
+
+
+ = 90 ? "需要介入" : "待核对"} + tone={item.priority >= 90 ? "warn" : ""} + /> + + {item.pull_number + ? `PR #${item.pull_number}` + : item.issue_number + ? `Issue #${item.issue_number}` + : "本地记录"} + +
+

{item.summary}

+ {when(item.source_updated_at)} +
+
+ {item.run_id && ( + + 查看任务 + + )} + {item.next_command && ( + + )} +
+
+ ))} + {!project.items.length && ( +

当前范围内没有需要介入的事项。

+ )} + {project.sources.error && ( + {project.sources.error} + )} + {project.omitted > 0 && ( + 另有 {project.omitted} 项未展示。 + )} +
+ ))} + {!!result.omitted_projects && ( + 另有 {result.omitted_projects} 个项目未展示。 + )} + {!!result.excluded_projects && ( + + {result.excluded_projects}{" "} + 个项目因策略、来源或工作区不可用未进入维护总览。 + + )} +

+ “重新读取”只读取本地数据。更新 GitHub 缓存请执行: +

+ + + + )} + + ); +} diff --git a/frontend/src/pages/projects.tsx b/frontend/src/pages/projects.tsx new file mode 100644 index 0000000..b6493b8 --- /dev/null +++ b/frontend/src/pages/projects.tsx @@ -0,0 +1,352 @@ +import { useState } from "react"; +import { + Link, + useNavigate, + useParams, + useSearchParams, +} from "react-router-dom"; +import { useRead, type Project } from "../api/client"; +import { + Badge, + Command, + Detail, + Empty, + Notice, + ReadState, + SafeLink, + when, +} from "../components"; + +export function ProjectPicker({ + projects, + value, + target = "", +}: { + projects: Project[]; + value: string; + target?: string; +}) { + const navigate = useNavigate(); + return ( + + ); +} + +export function ProjectsPage() { + const query = useRead("projects"); + const [search, setSearch] = useState(""); + return ( + <> +
+

项目

+

已登记的仓库与本地工作区。

+
+ + setSearch(e.target.value)} + /> +
+ + + + + + + + + + + {query.data?.projects + .filter((p) => + `${p.name} ${p.repository}` + .toLowerCase() + .includes(search.toLowerCase()), + ) + .map((p) => ( + + + + + + + ))} + +
项目用途工作区下一步
+ {p.name} + {p.repository} + + + + {p.workspace_count} 个 + {p.missing_workspaces > 0 && ( + {p.missing_workspaces} 个路径不可用 + )} + + 代码导览 ·{" "} + 开发任务 +
+
+ {query.data && !query.data.projects.length && ( + + 目前可通过终端关联已有目录。 + + + )} + {!!query.data?.omitted && ( + + 另有 {query.data.omitted} 个项目未展示,可通过 CLI 查询。 + + )} + + ); +} + +function Evidence({ + project, + binding, + evidence, + line, +}: { + project: string; + binding: string; + evidence: string; + line: number; +}) { + const [open, setOpen] = useState(false); + const query = useRead( + "code", + { + project_id: project, + binding_id: binding, + evidence_id: evidence, + start_line: String(line), + }, + open, + ); + return ( + <> + + {open && ( +
+ + {query.isFetching &&

正在核对代码…

} + {query.data && !query.isFetching && ( + <> + {query.data.status === "current_source" ? ( +
{query.data.text}
+ ) : ( + 代码依据已变化,请重新扫描后读取。 + )} + {query.data.truncated && ( +

当前只展示部分代码。

+ )} + + + )} +
+ )} + + ); +} + +export function ProjectPage() { + const { projectId = "", workspaceId } = useParams(); + const projects = useRead("projects"); + const [search, setSearch] = useSearchParams(); + const [input, setInput] = useState(search.get("focus") || ""); + const [client, setClient] = useState("codex"); + const navigate = useNavigate(); + const project = projects.data?.projects.find((p) => p.id === projectId); + const workspaces = project?.workspaces || []; + const binding = workspaceId + ? workspaces.find((w) => w.id === workspaceId) + : workspaces[0]; + const query = useRead( + "workspace", + { + project_id: projectId, + binding_id: binding?.id || "", + focus: search.get("focus") || "", + }, + Boolean(binding), + ); + const result = query.data; + return ( + <> +
+

{project?.name || "项目空间"}

+

{project?.repository || "工作区、代码导览与开发接续。"}

+
+ + {projects.data && ( + + )} +
+ + 工作区与代码导览 + + 开发任务 +
+ {project && !binding && ( + + 使用 project link 关联本地目录后,即可阅读代码导览。 + + )} + {binding && ( + <> +
+ +
+
{ + e.preventDefault(); + setSearch(input ? { focus: input } : {}); + }} + > + setInput(e.target.value)} + /> + +
+ + {result && ( + <> +
+
+ + + + {result.workspace.branch || "游离 HEAD"} ·{" "} + {result.workspace.head.slice(0, 10)} + {result.workspace.dirty && " · 有本地修改"} + +
+

+ 导览来源 · {when(result.guide.scanned_at)} +

+ {result.guide.status !== "current" && ( + <> + + 代码导览尚未就绪或已过期。请显式扫描后重新读取。 + + + + )} +
+
+

建议阅读路线

+

+ 根据静态事实与仓库声明组织,每一步均可查看代码依据。 +

+ {(result.guide.reading_path || []).map((step) => ( +
+ {step.step} +
+

{step.path}

+

{step.summary || step.reason}

+

+ {step.reason} · {step.language} +

+
+ + {step.source.url && ( + + 在 GitHub 查看 + + )} +
+
+
+ ))} + {!result.guide.reading_path?.length && ( +

当前范围内没有可用的阅读路线。

+ )} + +
+
+

在目标项目继续

+ + +

+ 复制的是 POSIX 终端配置预览命令。实际会话由你使用的 Agent + 提供。 +

+
+ {!!project?.workspace_check_omitted && ( + + 另有 {project.workspace_check_omitted} 个工作区未检查。 + + )} + + )} + + )} + {projects.data && !project && ( + + 请重新读取,或使用 CLI 检查登记信息。 + + )} + + ); +} diff --git a/frontend/src/pages/settings.tsx b/frontend/src/pages/settings.tsx new file mode 100644 index 0000000..8b96de4 --- /dev/null +++ b/frontend/src/pages/settings.tsx @@ -0,0 +1,74 @@ +import { useRead } from "../api/client"; +import { + Badge, + Command, + Detail, + List, + Pairs, + ReadState, + obj, + str, +} from "../components"; + +export function SettingsPage() { + const query = useRead("settings"); + const result = query.data; + return ( + <> +
+

设置与诊断

+

核对安装、配置来源和本地台账。

+
+ + {result && ( + <> +
+

当前安装

+ +
+
+

本地台账

+ {Object.entries(result.databases).map(([key, value]) => { + const database = obj(value); + return ( +
+
+

{key === "tasks" ? "任务数据库" : "项目注册表"}

+

{str(database.path)}

+ + 当前 {str(database.schema) || "未知"} / 支持{" "} + {str(database.supported_schema)} + +
+ +
+ ); + })} +
+
+

生效配置与来源

+ +
+
+

诊断建议

+ +
+ +

+ 配置改变后需重启工作台。本机会话最长有效 12 小时。 +

+ + )} + + ); +} diff --git a/frontend/src/pages/tasks.tsx b/frontend/src/pages/tasks.tsx new file mode 100644 index 0000000..b1d1f67 --- /dev/null +++ b/frontend/src/pages/tasks.tsx @@ -0,0 +1,305 @@ +import { Link, Navigate, useParams } from "react-router-dom"; +import { useRead } from "../api/client"; +import { + Badge, + Command, + Copy, + Detail, + Empty, + List, + Notice, + Pairs, + ReadState, + SafeLink, + arr, + obj, + statusName, + str, + when, +} from "../components"; +import { ProjectPicker } from "./projects"; + +export function TasksPage({ review = false }: { review?: boolean }) { + const { projectId = "", runId = "" } = useParams(); + const projects = useRead("projects"); + const project = projects.data?.projects.find((p) => p.id === projectId); + const accessible = Boolean(project?.policy.task_access); + const listing = useRead( + "tasks", + { project_id: projectId }, + accessible && !runId, + ); + const task = useRead( + "task", + { project_id: projectId, run_id: runId }, + accessible && Boolean(runId) && !review, + ); + const evidence = useRead( + "review", + { project_id: projectId, run_id: runId }, + accessible && Boolean(runId) && review, + ); + if (!projectId && projects.data?.projects.length) + return ( + + ); + return ( + <> +
+

{review ? "审阅依据" : "开发任务"}

+

+ {review + ? "核对历史验证、当前适用性与交付轨迹。" + : "找回目标、已有决定与尚未完成的工作。"} +

+
+ + {projects.data && ( + + )} + {!projects.data?.projects.length && !projects.isPending && ( + + 先关联一个本地项目,再查看任务与检查点。 + + )} + {project && !accessible && ( + + 请在 CLI 中配置仓库角色与贡献策略。 + + )} + {accessible && !runId && ( + <> + +
+

最近任务尝试

+

每次尝试保留自己的上下文和验证记录。

+ {listing.data?.tasks.map((item) => ( +
+
+ + {item.title || `Issue #${item.issue_number}`} + + + #{item.issue_number} ·{" "} + {item.stage === "external" ? "外部 Agent" : "仓库维护"} ·{" "} + {when(item.updated_at)} + +
+ + + 审阅依据 + +
+ ))} + {listing.data && !listing.data.tasks.length && ( + + 从已审阅 Issue 开始一次任务后,上下文和检查点会出现在这里。 + + )} + {!!listing.data?.omitted && ( + 另有 {listing.data.omitted} 次尝试未展示。 + )} +
+ + )} + {accessible && runId && ( + <> +
+ 返回任务列表 + + 任务上下文 + + + 审阅依据 + +
+ + {task.data && !review && } + {evidence.data && review && } + + )} + + ); +} + +function ContextView({ + result, +}: { + result: NonNullable>["data"]>; +}) { + if (!result.context) + return ( + + 这次尝试尚未保存上下文或检查点,可先查看审阅轨迹。 + + ); + const context = result.context; + const external = result.kind === "external"; + const pack = external ? context : obj(context.context_pack); + const checkpoint = external ? context : obj(context.checkpoint); + const task = obj(pack.task); + const contract = obj(external ? context.contract : pack.task_contract); + return ( + <> +
+

+ {str(task.title) || str(obj(context.work_item).title) || "任务目标"} +

+ {Boolean(task.url) && ( + 查看原始 Issue + )} +

+ {external + ? `检查点版本 ${str(context.revision)} · ${statusName(str(context.remote_freshness))}` + : `上下文保存于 ${when(obj(context.context_metadata).created_at)}`} +

+
+ {!external && ( + 这是已保存的维护上下文,当前工作区适用性尚未重新核对。 + )} + {external && arr(context.validity).length > 0 && ( + + 当前基线存在变化: + + + )} + {!external && !context.checkpoint && ( + 仍可阅读任务目标与原始上下文。 + )} +
+
+
+

未完成工作

+ +
+
+

已经做出的决定

+ +
+
+
+
+

下一步

+

{str(checkpoint.next_action) || "尚未记录"}

+

阻塞

+ +
+
+

接续入口

+ +
+
+
+ {Object.keys(contract).length > 0 && ( +
+

目标与验收约束

+

{str(contract.goal)}

+

验收条件

+ +

范围约束

+ + {Boolean(contract.source_requirements) && ( +

{str(contract.source_requirements)}

+ )} + +
+ )} + + + + + ); +} + +function ReviewView({ + result, +}: { + result: NonNullable>["data"]>; +}) { + const trace = result.trace, + current = obj(trace.current), + stats = obj(trace.stats); + return ( + <> + + 以下为已有证据与审计记录。历史验证通过不等于当前可以提交或合并。 + +
+

Issue 最新记录摘要

+

+ #{str(trace.issue_number)} · 包含该 Issue 的多次尝试 +

+ +
+ {arr(result.verification?.evidence).map((item, index) => { + const evidence = obj(item); + return ( +
+
+ + +
+

+ {str(evidence.profile)} · {when(evidence.updated_at)} +

+ + +
+ ); + })} +
+

任务轨迹

+ {arr(trace.events).map((item, i) => { + const event = obj(item); + return ( +
+
+ + {when(event.occurred_at)} +
+

+ {str(event.summary || event.event || event.kind || event.id)} +

+ +
+ ); + })} + {Number(stats.events_omitted) > 0 && ( + 另有 {str(stats.events_omitted)} 条事件未展示。 + )} +
+ + + ); +} diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..978adfe --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,552 @@ +:root { + font-family: + Inter, "Noto Sans CJK SC", "Microsoft YaHei", system-ui, sans-serif; + color: #202c3a; + background: #f5f7fa; + font-synthesis: none; + line-height: 1.65; + font-size: 14px; + --line: #e1e7ee; + --accent: #276459; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +a { + color: var(--accent); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +button, +input, +select { + font: inherit; +} +button { + border: 1px solid #ccd6df; + background: white; + color: #283c4c; + border-radius: 7px; + padding: 6px 13px; + cursor: pointer; + white-space: nowrap; +} +button:hover { + background: #edf4f2; + border-color: #7dafa2; +} +input, +select { + border: 1px solid #ccd6df; + border-radius: 7px; + padding: 8px 11px; + background: white; + color: inherit; + min-width: 0; + max-width: 100%; +} +input[type="search"] { + width: min(100%, 440px); + margin-bottom: 14px; +} +:focus-visible { + outline: 3px solid #63b5a0; + outline-offset: 3px; +} +.shell { + display: flex; + min-height: 100vh; +} +aside { + width: 235px; + background: #fff; + border-right: 1px solid var(--line); + padding: 28px 20px; + position: sticky; + top: 0; + height: 100vh; + display: flex; + flex-direction: column; + flex-shrink: 0; +} +.brand { + display: flex; + align-items: center; + gap: 10px; + color: #243743; + font-size: 18px; + font-weight: 700; + margin-bottom: 30px; +} +.brand > span { + display: grid; + place-items: center; + width: 37px; + height: 39px; + background: #e5f0ec; + color: var(--accent); + border-radius: 9px; +} +small { + display: block; + color: #6b7b89; + font-size: 12px; + overflow-wrap: anywhere; +} +.brand small { + font-weight: 400; +} +nav { + display: grid; + gap: 5px; +} +nav a { + border-radius: 7px; + color: #536472; + padding: 9px 12px; +} +nav a[aria-current="page"] { + background: #e9f1ee; + color: #245e52; + font-weight: 650; +} +.sidebar-projects { + margin-top: 28px; + border-top: 1px solid var(--line); +} +.sidebar-projects p, +.sidebar-note { + color: #84929d; + font-size: 12px; +} +.sidebar-projects a { + display: block; + color: #445765; + padding: 8px 10px; + border-radius: 6px; +} +.sidebar-projects a:hover { + background: #f6f8fa; +} +.sidebar-note { + margin-top: auto; + padding-top: 20px; +} +.workspace { + flex: 1; + min-width: 0; +} +.topbar { + min-height: 68px; + padding: 12px 32px; + border-bottom: 1px solid var(--line); + background: #ffffffdb; + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; +} +main { + max-width: 1400px; + margin: auto; + padding: 28px 36px 70px; + outline: none; +} +h1 { + font-size: 25px; + margin: 0; + letter-spacing: -0.5px; +} +h2 { + font-size: 16px; + margin: 0 0 12px; +} +h3 { + font-size: 14px; + margin: 0 0 6px; +} +p { + margin: 7px 0 12px; + overflow-wrap: anywhere; +} +.page-heading { + margin-bottom: 24px; +} +.page-heading p, +.muted { + color: #6a7b89; +} +.panel { + background: white; + border: 1px solid var(--line); + border-radius: 10px; + padding: 20px 23px; + margin-bottom: 18px; +} +.row, +.actions, +.toolbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; +} +.toolbar { + margin-bottom: 15px; +} +.toolbar input { + flex: 1; +} +.field { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 13px; + min-width: 0; +} +.field select { + max-width: min(740px, 85vw); +} +.badge { + display: inline-flex; + border: 1px solid #e0e6eb; + background: #f4f6f8; + color: #5f6e7c; + font-size: 12px; + padding: 2px 8px; + border-radius: 5px; + white-space: nowrap; +} +.badge.good { + color: #23614f; + background: #eaf4ee; + border-color: #d5e9de; +} +.badge.warn { + color: #94601e; + background: #fcf4e5; + border-color: #eddfc6; +} +.notice { + border: 1px solid #e3dcc9; + border-radius: 8px; + background: #fcf9ef; + padding: 12px 16px; + margin: 14px 0; + overflow-wrap: anywhere; +} +.notice.error { + background: #fff1ef; + border-color: #edcbc6; +} +.empty { + border: 1px dashed #cbd7df; + background: #fbfcfd; + border-radius: 10px; + padding: 28px; + margin: 18px 0; + color: #6b7b88; +} +.empty strong { + color: #354b59; + display: block; + font-size: 17px; + margin-bottom: 8px; +} +.stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-bottom: 24px; +} +.stats > div { + background: white; + padding: 20px; + border: 1px solid var(--line); + border-radius: 10px; +} +.stats strong { + display: block; + font-size: 24px; + color: #294a45; +} +.stats span { + color: #7c8c97; + font-size: 12px; +} +.section-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; +} +.section-heading h2 { + margin: 0; +} +.attention { + padding: 17px 0; + border-top: 1px solid #edf0f3; + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; +} +.attention > div:first-child { + flex: 1; + min-width: 0; +} +.attention p { + margin: 5px 0; +} +.copy { + display: inline-flex; + align-items: center; + gap: 8px; +} +.copy [role="status"] { + font-size: 12px; + color: #3d7664; +} +.command { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; + border: 1px solid #dee6eb; + background: #f7f9fb; + padding: 13px; + border-radius: 8px; + margin: 12px 0; +} +code { + overflow-wrap: anywhere; + white-space: pre-wrap; + font-size: 12px; +} +details { + margin: 12px 0; +} +summary { + cursor: pointer; + color: #607784; + font-size: 12px; +} +pre { + white-space: pre-wrap; + overflow-wrap: anywhere; + background: #f2f5f8; + padding: 16px; + border-radius: 7px; + max-height: 460px; + overflow: auto; + font-size: 12px; +} +.table-wrap { + overflow-x: auto; + background: white; + border: 1px solid var(--line); + border-radius: 10px; +} +table { + border-collapse: collapse; + width: 100%; + text-align: left; +} +th { + color: #82919c; + font-weight: 500; + background: #fafbfd; + font-size: 12px; +} +th, +td { + border-bottom: 1px solid var(--line); + padding: 15px 18px; +} +tbody tr:last-child td { + border-bottom: none; +} +.tabs { + display: flex; + flex-wrap: wrap; + gap: 23px; + border-bottom: 1px solid var(--line); + margin: 20px 0; + padding-bottom: 11px; +} +.tabs [aria-current] { + color: #205c4e; + font-weight: 650; +} +.reading-step { + display: grid; + grid-template-columns: 28px minmax(0, 1fr); + gap: 13px; + padding: 20px 0; + border-top: 1px solid #edf0f3; +} +.step-number { + background: #edf4f1; + color: #32785e; + border-radius: 6px; + text-align: center; + height: 28px; +} +.reading-step h3 { + overflow-wrap: anywhere; + font-family: ui-monospace, monospace; +} +.evidence { + width: 100%; +} +.split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; +} +.split > div { + min-width: 0; +} +dl { + margin: 0; +} +dl > div { + display: grid; + grid-template-columns: 130px minmax(0, 1fr); + gap: 14px; + margin: 10px 0; +} +dt { + color: #7c8c98; +} +dd { + margin: 0; + overflow-wrap: anywhere; +} +.event { + border-top: 1px solid #edf0f3; + padding: 14px 0; +} +.skip { + position: absolute; + top: -100px; + left: 10px; + background: white; + padding: 10px; + z-index: 10; +} +.skip:focus { + top: 8px; +} +@media (max-width: 850px) { + aside { + width: 185px; + padding: 22px 13px; + } + main { + padding: 24px 20px; + } + .topbar { + padding: 12px 20px; + } + .split { + grid-template-columns: 1fr; + gap: 0; + } +} +@media (max-width: 620px) { + .shell { + display: block; + } + aside { + width: 100%; + height: auto; + position: static; + padding: 13px 16px; + border-right: none; + border-bottom: 1px solid var(--line); + } + .brand { + margin-bottom: 8px; + font-size: 16px; + } + .brand small, + .sidebar-projects, + .sidebar-note { + display: none; + } + nav { + display: flex; + gap: 3px; + flex-wrap: wrap; + } + nav a { + padding: 6px 9px; + font-size: 12px; + } + .topbar { + padding: 10px 16px; + font-size: 12px; + } + main { + padding: 20px 15px 50px; + } + .stats { + gap: 7px; + } + .stats > div { + padding: 12px; + } + .stats strong { + font-size: 17px; + } + .panel { + padding: 17px; + } + .attention { + align-items: flex-start; + flex-wrap: wrap; + } + .command { + flex-wrap: wrap; + } + .field { + flex-wrap: wrap; + } + th, + td { + padding: 12px; + } + .projects-table, + .projects-table tbody { + display: block; + } + .projects-table thead { + display: none; + } + .projects-table tr { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + padding: 16px; + border-bottom: 1px solid var(--line); + } + .projects-table tr:last-child { + border-bottom: none; + } + .projects-table td { + padding: 0; + border: none; + min-width: 0; + overflow-wrap: anywhere; + } + .projects-table td:first-child, + .projects-table td:last-child { + grid-column: 1 / -1; + } +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..f4547ac --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", + "strict": true, "skipLibCheck": true, "noEmit": true, + "resolveJsonModule": true, "esModuleInterop": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..90f49fa --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,66 @@ +import { defineConfig, type Plugin } from "vite"; +import react from "@vitejs/plugin-react"; + +// This proxy is only active during explicit source development. It carries the +// browser's local session capability; it never reads GitHub credentials. +function localBoundary(): Plugin { + return { + name: "reposteward-local-development", + configureServer(server) { + server.middlewares.use((req, res, next) => { + const origin = req.headers.origin; + const site = req.headers["sec-fetch-site"]; + if ( + req.socket.remoteAddress !== "127.0.0.1" || + req.headers.host !== "127.0.0.1:5173" || + (origin && origin !== "http://127.0.0.1:5173") || + (site && site !== "none" && site !== "same-origin") + ) { + res.statusCode = 403; + res.end("Local development requests only"); + return; + } + next(); + }); + }, + }; +} + +export default defineConfig(({ command }) => { + const target = new URL( + process.env.REPOSTEWARD_DEV_BACKEND || "http://127.0.0.1:8787", + ); + if ( + command === "serve" && + (target.protocol !== "http:" || + target.hostname !== "127.0.0.1" || + target.username || + target.password || + target.pathname !== "/" || + target.search || + target.hash) + ) { + throw new Error("REPOSTEWARD_DEV_BACKEND must be a loopback HTTP origin"); + } + return { + plugins: [localBoundary(), react()], + build: { manifest: true, sourcemap: false }, + server: { + host: "127.0.0.1", + port: 5173, + strictPort: true, + cors: false, + proxy: { + "/api/": { + target: target.origin, + changeOrigin: true, + configure(proxy) { + proxy.on("proxyReq", (request) => + request.setHeader("Origin", target.origin), + ); + }, + }, + }, + }, + }; +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..c219158 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + test: { environment: "jsdom" }, +}); diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 0000000..d06b939 --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,169 @@ +"""Build and verify the frontend before packaging; wheels need no Node runtime.""" + +import hashlib +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +def frontend_digest(root): + paths = [root / "hatch_build.py", root / "pyproject.toml", root / "uv.lock"] + excluded = { + "node_modules", + "dist", + "__pycache__", + "generated", + "coverage", + "test-results", + "playwright-report", + } + for folder in ("frontend", "src/reposteward/web_api"): + for parent, directories, files in os.walk(root / folder): + directories[:] = sorted( + name for name in directories if name not in excluded + ) + paths.extend( + Path(parent) / name + for name in files + if name not in {"openapi.json", ".DS_Store"} + ) + records = [ + (str(path.relative_to(root)), hashlib.sha256(path.read_bytes()).hexdigest()) + for path in sorted(set(paths)) + ] + return hashlib.sha256( + json.dumps(records, separators=(",", ":")).encode() + ).hexdigest() + + +def build_environment(): + allowed = { + "PATH", + "HOME", + "TMPDIR", + "LANG", + "LC_ALL", + "SYSTEMROOT", + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "NO_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + "no_proxy", + "UV_PROJECT_ENVIRONMENT", + "UV_CACHE_DIR", + "UV_PYTHON_INSTALL_DIR", + "UV_LINK_MODE", + "NPM_CONFIG_CACHE", + "npm_config_cache", + } + result = {k: v for k, v in os.environ.items() if k in allowed} + result.update( + { + "GIT_TERMINAL_PROMPT": "0", + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": os.devnull, + } + ) + return result + + +class CustomBuildHook(BuildHookInterface): + def initialize(self, version, build_data): + root = Path(self.root) + frontend = root / "frontend" + manifest = frontend / "dist/assets.json" + digest = frontend_digest(root) + if manifest.is_file(): + record = json.loads(manifest.read_text()) + if record.get("inputs_digest") == digest and all( + (frontend / "dist" / f["path"]).is_file() + and hashlib.sha256( + (frontend / "dist" / f["path"]).read_bytes() + ).hexdigest() + == f["sha256"] + for f in record["files"] + ): + build_data.setdefault("force_include", {})[str(frontend / "dist")] = ( + "frontend/dist" + if self.target_name == "sdist" + else "reposteward/web_dist" + ) + return + if not shutil.which("npm"): + raise RuntimeError( + "Source builds require Node/npm; install a release wheel for daily use." + ) + env = build_environment() + sys.path.insert(0, str(root / "src")) + try: + from reposteward.web_api.app import create_app + + schema = create_app().openapi() + finally: + sys.path.pop(0) + (frontend / "openapi.json").write_text( + json.dumps(schema, ensure_ascii=False, indent=2, sort_keys=True) + "\n" + ) + lock = frontend / "package-lock.json" + if not lock.is_file(): + raise RuntimeError( + "A reviewed frontend package-lock.json is required before building." + ) + stamp = frontend / "node_modules/.reposteward-lock" + lock_digest = hashlib.sha256(lock.read_bytes()).hexdigest() + if not stamp.is_file() or stamp.read_text() != lock_digest: + subprocess.run( + ["npm", "ci", "--ignore-scripts", "--no-audit", "--no-fund"], + cwd=frontend, + env=env, + check=True, + timeout=300, + ) + stamp.write_text(lock_digest) + subprocess.run( + ["npm", "run", "build"], cwd=frontend, env=env, check=True, timeout=180 + ) + vite = json.loads((frontend / "dist/.vite/manifest.json").read_text()) + entry = vite["index.html"] + records = [] + for path in sorted((frontend / "dist").rglob("*")): + name = str(path.relative_to(frontend / "dist")) + if path.is_file() and (name == "index.html" or name.startswith("assets/")): + mime = { + ".html": "text/html", + ".js": "text/javascript", + ".css": "text/css", + }.get(path.suffix) + if mime is None: + raise RuntimeError("Unexpected frontend asset type") + records.append( + { + "path": name, + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "mime": mime, + } + ) + manifest.write_text( + json.dumps( + { + "version": 1, + "inputs_digest": frontend_digest(root), + "entry_js": entry["file"], + "entry_css": entry["css"][0], + "files": records, + }, + indent=2, + ) + + "\n" + ) + build_data.setdefault("force_include", {})[str(frontend / "dist")] = ( + "frontend/dist" if self.target_name == "sdist" else "reposteward/web_dist" + ) diff --git a/pyproject.toml b/pyproject.toml index 9109d31..d0a4eab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "fastapi>=0.141.1,<0.142", "jsonschema>=4.23,<5"] build-backend = "hatchling.build" [project] @@ -10,7 +10,12 @@ readme = "README.md" requires-python = ">=3.12" license = "MIT" authors = [{ name = "tiammomo", email = "tiammomo@users.noreply.github.com" }] -dependencies = ["jsonschema>=4.23,<5"] +dependencies = [ + "jsonschema>=4.23,<5", + "fastapi>=0.141.1,<0.142", + "pydantic>=2.10,<3", + "uvicorn>=0.34,<1", +] [project.urls] Homepage = "https://github.com/tiammomo/RepoSteward" @@ -24,11 +29,23 @@ mcp = [ ] [dependency-groups] -dev = ["mcp>=2.1.1,<3"] +dev = ["mcp>=2.1.1,<3", "httpx>=0.28,<1"] [project.scripts] reposteward = "reposteward.cli:main" +[tool.uv] +cache-keys = [ + { file = "pyproject.toml" }, + { file = "hatch_build.py" }, + { file = "frontend/package*.json" }, + { file = "frontend/*.ts" }, + { file = "frontend/*.mjs" }, + { file = "frontend/index.html" }, + { file = "frontend/src/**" }, + { file = "src/reposteward/web_api/*.py" }, +] + [tool.hatch.build] exclude = ["/.git", "/.starfix", "/.reposteward", "/.venv", "/dist", "/notes"] skip-excluded-dirs = true @@ -38,6 +55,10 @@ packages = ["src/reposteward"] [tool.hatch.build.targets.wheel.force-include] "docker/Dockerfile.runner" = "reposteward/data/Dockerfile.runner" +"frontend/dist" = "reposteward/web_dist" + +[tool.hatch.build.hooks.custom] +path = "hatch_build.py" [tool.hatch.build.targets.sdist] include = [ @@ -51,4 +72,6 @@ include = [ "/reposteward.example.toml", "/pyproject.toml", "/uv.lock", + "/frontend", + "/hatch_build.py", ] diff --git a/src/reposteward/web/app.css b/src/reposteward/web/app.css deleted file mode 100644 index ebf1d20..0000000 --- a/src/reposteward/web/app.css +++ /dev/null @@ -1,102 +0,0 @@ -:root { color-scheme: light; --bg: #f4f5f1; --paper: #fff; --ink: #20352e; --muted: #6d7973; --line: #dee4dc; --green: #285c49; --pale: #e8f0e8; --amber: #86601e; --red: #a23d35; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif; } -* { box-sizing: border-box; } -body { margin: 0; background: var(--bg); color: var(--ink); font-size: 14px; line-height: 1.65; } -a { color: var(--green); text-decoration-thickness: 1px; text-underline-offset: 3px; } -button, input, select { font: inherit; } -button, a, select { -webkit-tap-highlight-color: transparent; } -button { cursor: pointer; } -:focus-visible { outline: 3px solid #84b7a0; outline-offset: 3px; } -.skip { position: absolute; top: -60px; left: 20px; z-index: 10; background: white; padding: 12px; } -.skip:focus { top: 12px; } -.shell { display: grid; grid-template-columns: 236px minmax(0, 1fr); min-height: 100vh; } -.sidebar { background: #fafbf8; border-right: 1px solid var(--line); padding: 32px 20px 22px; display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; } -.brand { display: flex; gap: 12px; align-items: center; text-decoration: none; font-size: 19px; font-weight: 650; letter-spacing: -.5px; margin: 0 6px 50px; } -.brand-mark { background: var(--green); color: white; width: 39px; height: 41px; border-radius: 9px; display: grid; place-items: center; font-family: Georgia, serif; font-size: 28px; } -.brand small { display: block; color: var(--muted); font-size: 10px; letter-spacing: 2px; font-weight: 400; margin-top: 2px; } -.nav-label, .eyebrow { font-size: 10px; font-weight: 700; letter-spacing: 2px; color: var(--muted); } -.nav-label { margin: 0 15px 12px; } -nav { display: grid; gap: 7px; } -nav a { display: flex; gap: 14px; align-items: center; padding: 12px 15px; color: #5c6b63; text-decoration: none; border-radius: 7px; } -nav a span { font-size: 19px; width: 21px; line-height: 1; } -nav a[aria-current="page"] { background: var(--pale); color: var(--green); font-weight: 650; } -nav a:hover { background: #edf1ea; } -.sidebar-bottom { margin-top: auto; padding: 24px 15px 0; border-top: 1px solid var(--line); color: var(--green); font-size: 12px; } -.sidebar-bottom p { color: var(--muted); margin: 12px 0 0; line-height: 1.8; } -.live-dot { display: inline-block; height: 6px; width: 6px; border-radius: 50%; background: #5b947a; margin-right: 7px; } -.main { min-width: 0; } -.topbar { height: 78px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; padding: 0 42px; background: #fafbf8; gap: 12px; } -.breadcrumb { color: var(--muted); font-size: 12px; } -.breadcrumb span { margin: 0 12px; color: #a7b2aa; } -.breadcrumb strong { font-weight: 500; color: var(--ink); } -.top-actions, .row, .actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } -main { max-width: 1400px; padding: 36px 42px 28px; margin: 0 auto; min-height: calc(100vh - 132px); } -main:focus { outline: none; } -.hero { margin-bottom: 28px; } -.hero h1 { font-size: clamp(25px, 3vw, 34px); font-weight: 600; letter-spacing: -1px; line-height: 1.4; margin: 8px 0 9px; } -.hero p { color: var(--muted); margin: 0; max-width: 740px; } -.stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-bottom: 30px; } -.stat { padding: 18px 22px; border: 1px solid var(--line); background: var(--paper); border-radius: 10px; } -.stat-label { font-size: 12px; color: var(--muted); } -.stat-number { font-size: 29px; line-height: 1.4; font-weight: 550; margin: 8px 0 1px; letter-spacing: -1px; } -.stat-note { font-size: 11px; color: var(--muted); } -.section-head { display: flex; justify-content: space-between; align-items: baseline; margin: 28px 0 13px; gap: 14px; flex-wrap: wrap; } -h2 { font-size: 17px; font-weight: 600; margin: 0; } -h3 { font-size: 15px; margin: 0 0 5px; font-weight: 600; } -p { margin: 7px 0; } -.muted, .meta { color: var(--muted); } -.meta { font-size: 12px; } -.badge { display: inline-flex; border-radius: 5px; background: #edf1ec; padding: 3px 8px; color: #55685c; font-size: 11px; font-weight: 550; white-space: nowrap; } -.badge.good { color: #276348; background: #e6f2e8; } -.badge.warn { color: var(--amber); background: #f6efde; } -.badge.bad { color: var(--red); background: #f9e9e5; } -.panel { background: var(--paper); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin-bottom: 18px; } -.panel-pad { padding: 22px; } -.panel-title { padding: 16px 21px; border-bottom: 1px solid var(--line); display: flex; gap: 12px; align-items: center; justify-content: space-between; } -.item { padding: 18px 21px; border-bottom: 1px solid #e9ede6; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 18px; align-items: center; } -.item:last-child { border-bottom: 0; } -.item-title { font-size: 14px; font-weight: 550; margin: 5px 0; overflow-wrap: anywhere; } -.item .meta { overflow-wrap: anywhere; } -.button { border: 1px solid var(--line); border-radius: 6px; padding: 7px 12px; background: white; color: var(--ink); font-size: 12px; font-weight: 500; text-decoration: none; display: inline-block; } -.button:hover { border-color: #9cae9e; background: #f4f7f1; } -.button.primary { color: white; background: var(--green); border-color: var(--green); } -.button.subtle { background: transparent; } -.button:disabled { opacity: .55; cursor: progress; } -.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; } -.project-card { background: white; border: 1px solid var(--line); border-radius: 10px; padding: 23px; } -.project-card h2 { margin: 16px 0 6px; font-size: 22px; overflow-wrap: anywhere; } -.project-card .actions { margin-top: 22px; } -.project-symbol { font-family: ui-monospace, monospace; background: var(--pale); color: var(--green); border-radius: 8px; padding: 7px 12px; font-size: 20px; } -.toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 20px 0; } -label { color: var(--muted); font-size: 12px; } -input, select { border: 1px solid #cfd9ce; background: white; color: var(--ink); border-radius: 6px; padding: 9px 12px; max-width: 100%; min-width: 0; } -input[type="search"] { flex: 1; min-width: 160px; } -select { max-width: 640px; } -.empty { border: 1px dashed #cdd8cb; border-radius: 10px; padding: 36px 25px; color: var(--muted); line-height: 1.9; background: #fafbf8; } -.empty strong { color: var(--ink); display: block; margin-bottom: 5px; } -.callout { padding: 14px 18px; border-left: 3px solid #b48d49; background: #faf5e9; border-radius: 0 6px 6px 0; margin: 15px 0; color: #6f5a34; } -.error { border-left-color: #b56155; background: #fbefec; color: #8a4237; } -.split { display: grid; grid-template-columns: minmax(0, 1.8fr) minmax(250px, 1fr); gap: 22px; align-items: start; } -.key-values { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 12px; margin: 0; font-size: 12px; } -dt { color: var(--muted); } dd { margin: 0; overflow-wrap: anywhere; } -pre, code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; } -pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 440px; overflow-y: auto; margin: 12px 0 0; padding: 16px; background: #f5f7f2; border-radius: 6px; border: 1px solid #e2e7dc; line-height: 1.7; } -.command { display: flex; gap: 10px; align-items: start; margin: 12px 0; background: #f5f7f2; padding: 12px; border-radius: 6px; } -.command code { flex: 1; overflow-wrap: anywhere; padding-top: 3px; } -.step { display: flex; gap: 14px; padding: 18px 0; border-bottom: 1px solid #e9ede6; } -.step:last-child { border-bottom: 0; } -.step-number { border-radius: 50%; width: 25px; height: 25px; flex-shrink: 0; display: grid; place-items: center; background: #edf2e9; color: #52684f; font-size: 11px; } -.step-content { min-width: 0; flex: 1; } -.step-content h3 { overflow-wrap: anywhere; font-family: ui-monospace, monospace; font-size: 13px; } -.step-content p { color: var(--muted); font-size: 12px; } -.step-content .actions { margin-top: 10px; } -details { margin: 15px 0; } -summary { cursor: pointer; font-size: 12px; color: var(--green); padding: 5px 0; } -ul.plain { padding-left: 19px; margin: 10px 0; } -ul.plain li { margin: 8px 0; overflow-wrap: anywhere; } -.task-button { text-align: left; border: 0; background: transparent; color: var(--ink); padding: 0; font: inherit; font-weight: 550; text-decoration: underline; text-underline-offset: 4px; } -.toast { position: fixed; bottom: 25px; left: 50%; transform: translateX(-50%); color: white; background: #203a2d; padding: 10px 18px; border-radius: 7px; z-index: 5; max-width: 90vw; } -footer { padding: 14px 42px 20px; color: #849080; font-size: 10px; display: flex; justify-content: space-between; gap: 15px; } -@media (min-width: 1500px) { main { padding-top: 46px; } } -@media (max-width: 1050px) { .shell { grid-template-columns: 200px minmax(0, 1fr); } .sidebar { padding-inline: 13px; } .topbar { padding-inline: 25px; } main { padding: 28px 25px; } .split { grid-template-columns: 1fr; } .grid { gap: 12px; } } -@media (max-width: 720px) { .shell { display: block; } .sidebar { height: auto; position: static; padding: 17px 18px 10px; border-right: 0; border-bottom: 1px solid var(--line); } .brand { margin: 0 0 15px; font-size: 18px; } .brand-mark { width: 33px; height: 35px; } .nav-label, .sidebar-bottom { display: none; } nav { display: flex; overflow-x: auto; gap: 5px; } nav a { white-space: nowrap; font-size: 12px; gap: 7px; padding: 9px 10px; } nav a span { font-size: 16px; width: auto; } .topbar { height: 60px; padding-inline: 18px; } .breadcrumb { font-size: 11px; } .breadcrumb span { margin: 0 5px; } main { padding: 25px 18px; } .stats { gap: 8px; margin-bottom: 20px; } .stat { padding: 14px 12px; } .stat-number { font-size: 25px; } .stat-note { font-size: 10px; } .grid { grid-template-columns: 1fr; } .item { grid-template-columns: 1fr; gap: 10px; } .item > .actions { justify-self: start; } .panel-pad, .panel-title { padding: 17px; } .toolbar { align-items: stretch; } .toolbar label { width: 100%; } select { width: 100%; } footer { padding-inline: 18px; } footer span { display: none; } } -@media (prefers-reduced-motion: no-preference) { .button, nav a { transition: background .15s ease; } } diff --git a/src/reposteward/web/app.js b/src/reposteward/web/app.js deleted file mode 100644 index e3458ea..0000000 --- a/src/reposteward/web/app.js +++ /dev/null @@ -1,297 +0,0 @@ -"use strict"; - -(() => { - const content = document.getElementById("content"); - const connection = document.getElementById("connection"); - const labels = {today: "今日待处理", projects: "项目空间", tasks: "任务接续", review: "审阅依据", settings: "设置与诊断"}; - const statusNames = {current: "当前有效", stale: "已过期", not_scanned: "尚未扫描", compatible: "兼容", missing: "尚未创建", migration_required: "需要升级", newer_than_supported: "安装版本过旧", snapshot_required: "等待稳定快照", unavailable: "暂不可用", not_checked: "未核对当前适用性", matches: "匹配当前快照", not_verified: "当前未验证", passed: "历史验证通过", failed: "验证失败", running: "进行中", ready: "准备完成", submitted: "已提交", merged: "已合入", blocked: "有阻塞", complete: "完成", cached: "线上缓存", not_refreshed: "未刷新线上状态", refresh_failed: "线上刷新失败", maintainer: "维护者", contributor: "贡献者", unconfigured: "未配置策略", unknown: "未知"}; - const storageKey = "reposteward-local-session"; - let token = ""; - try { token = sessionStorage.getItem(storageKey) || ""; } catch { /* Storage can be disabled. */ } - if (location.hash.startsWith("#session=")) { - const candidate = location.hash.slice(9); - if (/^[A-Za-z0-9_-]{43}$/.test(candidate)) { - token = candidate; - try { sessionStorage.setItem(storageKey, token); } catch { /* Keep it only in memory. */ } - } - history.replaceState(null, "", location.pathname + "#today"); - } - let controller = new AbortController(); - let generation = 0; - let projectCache = []; - let selectedProject = ""; - let noticeTimer; - - function node(tag, className, ...children) { - const element = document.createElement(tag); - if (className) element.className = className; - for (const child of children.flat(Infinity)) { - if (child !== null && child !== undefined) element.append(child instanceof Node ? child : document.createTextNode(String(child))); - } - return element; - } - const values = value => Array.isArray(value) ? value : []; - const text = value => typeof value === "string" ? value : JSON.stringify(value ?? ""); - const name = value => statusNames[value] || value || "未知"; - function badge(value, tone = "") { return node("span", "badge " + tone, name(value)); } - function button(label, action, primary = false) { - const b = node("button", "button" + (primary ? " primary" : ""), label); - b.type = "button"; b.addEventListener("click", action); return b; - } - function notify(message) { - const box = document.getElementById("notice"); - clearTimeout(noticeTimer); box.textContent = message; box.hidden = false; - noticeTimer = setTimeout(() => { box.hidden = true; }, 3000); - } - async function copy(value) { - try { await navigator.clipboard.writeText(value); notify("已复制,可在目标项目中继续。"); } - catch { notify("浏览器未允许复制,请选择下方文本手动复制。"); } - } - function command(value) { - return node("div", "command", node("code", "", value), button("复制", () => copy(value))); - } - function details(title, value) { - return node("details", "", node("summary", "", title), node("pre", "", JSON.stringify(value, null, 2))); - } - function list(items, empty = "尚无记录") { - return values(items).length ? node("ul", "plain", values(items).map(item => node("li", "", text(item)))) : node("p", "muted", empty); - } - function date(value) { - const d = new Date(value); - return value && Number.isFinite(d.getTime()) ? d.toLocaleString("zh-CN", {hour12: false}) : "时间未知"; - } - function pairs(entries) { - return node("dl", "key-values", entries.flatMap(([key, value]) => [node("dt", "", key), node("dd", "", value ?? "未知")])); - } - function link(label, raw) { - try { - const url = new URL(raw); - if (url.protocol !== "https:" || url.username || url.password) return node("span", "meta", label); - const a = node("a", "", label); a.href = url.href; a.target = "_blank"; a.rel = "noopener noreferrer"; return a; - } catch { return node("span", "meta", label); } - } - function hero(title, subtitle, eyebrow = "LOCAL WORKSPACE") { - return node("section", "hero", node("div", "eyebrow", eyebrow), node("h1", "", title), node("p", "", subtitle)); - } - function errorBox(error) { return node("div", "callout error", error.message || "读取失败,请重试。"); } - function empty(title, message) { return node("div", "empty", node("strong", "", title), message); } - function go(view, project = "", item = "") { - location.hash = [view, project, item].filter(Boolean).map(encodeURIComponent).join("/"); - } - async function api(route, params = {}) { - const response = await fetch("/api/" + route + (Object.keys(params).length ? "?" + new URLSearchParams(params) : ""), { - headers: {Authorization: "Bearer " + token}, signal: controller.signal, cache: "no-store", credentials: "omit" - }); - const data = await response.json(); - if (!response.ok) { - if (response.status === 401) { connection.textContent = "会话未连接"; connection.className = "badge warn"; } - throw new Error(data.error?.message || "读取失败,请重试。"); - } - return data; - } - function omitted(count, unit = "项") { return count ? node("p", "callout", `另有 ${count} ${unit}未显示;可使用 CLI 查询完整范围。`) : document.createDocumentFragment(); } - function section(title, detail = "") { return node("div", "section-head", node("h2", "", title), node("span", "meta", detail)); } - - async function today(host) { - host.append(hero("今天,先处理重要的事。", "集中查看需要你介入的事项,再回到目标项目继续工作。")); - let result; - try { result = await api("overview"); } catch (error) { host.append(errorBox(error)); return; } - const projects = values(result.projects); - const items = projects.flatMap(p => values(p.items)); - const unknown = projects.filter(p => !p.complete).length; - const stat = (label, number, note) => node("div", "stat", node("div", "stat-label", label), node("div", "stat-number", number), node("div", "stat-note", note)); - host.append(node("div", "stats", stat("已关联项目", projectCache.length, "从项目空间进入工作区"), stat("本页待处理", items.length, "按维护者介入优先级排列"), stat("数据覆盖", result.complete ? "完整" : "部分", unknown ? `${unknown} 个项目有未知或省略` : "依据本地台账与线上缓存"))); - host.append(section("需要关注", `本次读取 · ${date(result.observed_at)}`)); - if (!projects.length) host.append(empty("从一个项目开始", "在终端关联项目并配置仓库策略后,它会出现在这里。"), command("reposteward project link /absolute/path/to/project")); - for (const project of projects) { - const source = project.sources || {}; - const panel = node("section", "panel", node("div", "panel-title", button(project.repository, () => go("projects", project.project_id)), node("div", "row", badge(source.status, source.status === "cached" ? "" : "warn"), node("span", "meta", date(source.fetched_at))))); - if (!values(project.items).length) panel.append(node("div", "panel-pad muted", "当前范围内没有需要介入的事项。")); - for (const item of values(project.items)) { - const actions = node("div", "actions"); - if (item.run_id) actions.append(button("查看任务", () => go("tasks", project.project_id, item.run_id))); - if (item.next_command) actions.append(button("复制下一步", () => copy(item.next_command))); - panel.append(node("div", "item", node("div", "", node("div", "row", badge(item.priority >= 90 ? "需要介入" : "待核对", item.priority >= 90 ? "warn" : ""), node("span", "meta", item.pull_number ? `PR #${item.pull_number}` : "本地任务")), node("div", "item-title", item.summary), node("div", "meta", "来源更新 · " + date(item.source_updated_at))), actions)); - } - if (source.error) panel.append(node("div", "panel-pad", errorBox(new Error(source.error)))); - if (project.omitted) panel.append(node("div", "panel-pad", omitted(project.omitted))); - host.append(panel); - } - host.append(omitted(result.omitted_projects, "个项目")); - if (result.excluded_projects) host.append(node("p", "meta", `${result.excluded_projects} 个项目因策略、来源或工作区不可用未进入待办;请到项目空间检查。`)); - host.append(details("本页来源与覆盖信息", result), node("p", "meta", "“重新读取”只读取本地数据。更新 GitHub 缓存请在终端执行:"), command("reposteward overview refresh")); - } - - function projectSelector(host, projectId, onChange) { - const select = node("select", ""); select.id = "project-select"; - for (const project of projectCache) { - const option = node("option", "", project.repository); option.value = project.id; select.append(option); - } - select.value = projectId; - select.addEventListener("change", () => { selectedProject = select.value; onChange(select.value); }); - host.append(node("div", "toolbar", node("label", "", "当前项目"), select)); - select.setAttribute("aria-label", "当前项目"); - } - async function projects(host, projectId, bindingId) { - host.append(hero("每个项目,都有清晰的入口。", "从工作区、代码导览和阅读路线,快速找回项目的结构。", "PROJECT SPACES")); - if (!projectId) { - const grid = node("div", "grid"); - const input = node("input", ""); input.type = "search"; input.placeholder = "查找项目或仓库…"; input.setAttribute("aria-label", "查找项目"); - const draw = () => { - grid.replaceChildren(); - const matched = projectCache.filter(p => (p.repository + " " + p.name).toLowerCase().includes(input.value.toLowerCase())); - for (const project of matched) grid.append(node("article", "project-card", node("div", "row", node("span", "project-symbol", "⌘"), badge(project.policy.mode), !project.policy.enabled ? badge("策略未启用", "warn") : null), node("h2", "", project.name), node("p", "meta", project.repository), node("p", "meta", `${project.workspace_count} 个工作区 · ${project.missing_workspaces} 个路径不可用`), node("div", "actions", button("进入项目", () => go("projects", project.id), true), button("查看任务", () => go("tasks", project.id))))); - if (!matched.length) grid.append(empty("没有匹配项目", "可调整查找条件,或使用 project link 关联本地工作区。")); - }; - input.addEventListener("input", draw); host.append(node("div", "toolbar", input), grid); draw(); return; - } - selectedProject = projectId; - projectSelector(host, projectId, id => go("projects", id)); - const project = projectCache.find(p => p.id === projectId); - if (!project) { host.append(empty("项目不在当前列表中", "请重新读取项目列表,或从 CLI 检查登记信息。")); return; } - const workspaces = values(project.workspaces); - if (!workspaces.length) { host.append(empty("尚无有效关联", "使用 project link 关联这个项目的工作区。")); return; } - const selected = workspaces.find(w => w.id === bindingId) || workspaces[0]; - const select = node("select", ""); select.setAttribute("aria-label", "本地工作区"); - for (const workspace of workspaces) { const option = node("option", "", workspace.root); option.value = workspace.id; select.append(option); } - select.value = selected.id; select.addEventListener("change", () => go("projects", projectId, select.value)); - const input = node("input", ""); input.type = "search"; input.placeholder = "关注哪个模块或问题?"; input.maxLength = 2000; input.setAttribute("aria-label", "导览关注点"); - const detail = node("div", ""); - let guideGeneration = 0; - async function load() { - const currentGuide = ++guideGeneration; - detail.replaceChildren(empty("读取导览中", "正在核对工作区与代码来源…")); - try { - const result = await api("workspace", {project_id: projectId, binding_id: selected.id, focus: input.value}); - if (currentGuide !== guideGeneration) return; - const guide = result.guide; - detail.replaceChildren(node("div", "row", badge(guide.status, guide.status === "current" ? "good" : "warn"), badge(result.policy.mode), node("span", "meta", `分支 ${result.workspace.branch || "游离 HEAD"} · ${result.workspace.head.slice(0, 10)}${result.workspace.dirty ? " · 有本地修改" : ""}`))); - detail.append(node("p", "meta", "导览来源 · " + date(guide.scanned_at))); - if (guide.status !== "current") detail.append(node("div", "callout", "代码导览尚未就绪或已过期。请显式扫描后重新读取。"), command(result.commands.scan)); - const route = node("section", "panel panel-pad", section("建议阅读路线", "基于静态事实与仓库声明")); - for (const step of values(guide.reading_path)) { - const evidence = node("div", ""); - const actions = node("div", "actions", button("查看代码依据", async () => { - evidence.replaceChildren(node("p", "meta", "正在读取证据…")); - try { - const r = await api("code", {project_id: projectId, binding_id: selected.id, evidence_id: step.source.evidence_id, start_line: String(step.source.line)}); - evidence.replaceChildren(r.status === "current_source" ? node("pre", "", r.text) : node("div", "callout", "代码依据已变化,请重新扫描后读取。"), r.truncated ? node("p", "meta", "当前展示部分代码;可在源文件中继续阅读。") : document.createDocumentFragment(), details("证据来源", r)); - } - catch (error) { evidence.replaceChildren(errorBox(error)); } - })); - if (step.source.url) actions.append(link("在 GitHub 查看", step.source.url)); - route.append(node("div", "step", node("span", "step-number", step.step), node("div", "step-content", node("h3", "", step.path), node("p", "", step.summary || step.reason), node("div", "row", badge(step.language), node("span", "meta", step.reason)), actions, evidence))); - } - if (values(guide.reading_path).length) detail.append(route); - const clientSelect = node("select", ""); clientSelect.setAttribute("aria-label", "Coding Agent 客户端"); - const clientNames = {codex: "Codex", "claude-code": "Claude Code", "copilot-vscode": "Copilot · VS Code"}; - for (const client of Object.keys(result.commands.mcp_clients)) { const option = node("option", "", clientNames[client] || client); option.value = client; clientSelect.append(option); } - const preview = node("div", "", command(result.commands.mcp_clients[clientSelect.value])); - clientSelect.addEventListener("change", () => preview.replaceChildren(command(result.commands.mcp_clients[clientSelect.value]))); - detail.append(details("入口、关联模块与覆盖说明", guide), section("在目标项目继续"), clientSelect, preview, node("p", "meta", "复制的是 POSIX 终端命令,保留当前 RepoSteward 配置来源。它预览客户端配置;实际会话由你使用的 Agent 提供。")); - } catch (error) { if (error.name !== "AbortError" && currentGuide === guideGeneration) detail.replaceChildren(errorBox(error)); } - } - input.addEventListener("keydown", event => { if (event.key === "Enter") load(); }); - host.append(node("div", "toolbar", node("label", "", "本地工作区"), select), node("div", "toolbar", input, button("查阅导览", load)), detail); - host.append(omitted(project.workspace_check_omitted, "个工作区")); await load(); - } - - async function taskPage(host, view, projectId, runId) { - host.append(hero(view === "review" ? "让交付依据,可以核对。" : "从上次停下的地方继续。", view === "review" ? "查看历史验证、当前适用性和审阅轨迹。这里的历史判定不授予交付权限。" : "把目标、约束、决定与未完成工作带回你的 Coding Agent。", view === "review" ? "REVIEW EVIDENCE" : "TASK CONTINUITY")); - if (!projectCache.length) { host.append(empty("还没有项目", "先关联一个本地项目,再查看任务与检查点。")); return; } - projectId = projectId || selectedProject || projectCache[0].id; selectedProject = projectId; - projectSelector(host, projectId, id => go(view, id)); - const project = projectCache.find(p => p.id === projectId); - if (!project?.policy.task_access) { host.append(empty("项目策略未启用", "关联工作区后,还需要在 CLI 中配置仓库角色与贡献策略。")); return; } - if (!runId) { - const result = await api("tasks", {project_id: projectId}); - host.append(section("最近任务尝试", "每次尝试保留自己的记录")); - if (!values(result.tasks).length) host.append(empty("这里还没有任务", "从已审阅 Issue 开始一次任务后,目标、检查点和证据会出现在这里。")); - const panel = node("section", "panel"); - for (const task of values(result.tasks)) { - const title = button(task.title || `Issue #${task.issue_number}`, () => go(view, projectId, task.id)); title.className = "task-button"; - panel.append(node("div", "item", node("div", "", node("div", "row", badge(task.status), node("span", "meta", `#${task.issue_number} · ${task.stage === "external" ? "外部 Agent" : "仓库维护"}`)), node("div", "item-title", title), node("div", "meta", date(task.updated_at))), button("查看" + (view === "review" ? "依据" : "上下文"), () => go(view, projectId, task.id)))); - } - if (result.tasks.length) host.append(panel); host.append(omitted(result.omitted)); return; - } - host.append(node("div", "actions", button("返回任务列表", () => go(view, projectId)), button(view === "review" ? "任务上下文" : "审阅依据", () => go(view === "review" ? "tasks" : "review", projectId, runId))), node("p", "meta", "任务 · " + runId)); - if (view === "review") { - const result = await api("review", {project_id: projectId, run_id: runId}); - const trace = result.trace; - host.append(node("div", "callout", "以下为已有证据与审计记录。历史验证通过不等于当前可以提交或合并。")); - host.append(node("section", "panel panel-pad", section("Issue 最新记录摘要", `#${trace.issue_number} · 包含该 Issue 的多次尝试`), pairs([["后续动作", trace.next_action], ["记录状态", name(trace.current?.status)], ["合并结果", name(trace.current?.merge_outcome)], ["证据 HEAD", trace.current?.head_sha || "未知"], ["覆盖", trace.complete ? "范围内完整" : "有省略或未知"]]))); - if (result.verification) for (const evidence of values(result.verification.evidence)) host.append(node("section", "panel panel-pad", node("div", "row", badge(evidence.outcome, evidence.outcome === "failed" ? "bad" : ""), badge(evidence.current_applicability, evidence.current_applicability === "matches" ? "good" : "warn")), node("p", "meta", `${evidence.profile} · ${date(evidence.updated_at)}`), list(evidence.validity, "未观察到已检查项的差异。"), details("验证证据", evidence))); - host.append(section("任务轨迹", `${trace.stats.events} 条已展示事件`)); - const panel = node("section", "panel"); - for (const event of values(trace.events)) panel.append(node("div", "item", node("div", "", node("div", "row", badge(event.source || event.kind), node("span", "meta", date(event.occurred_at))), node("div", "item-title", event.summary || event.event || event.kind || event.id), details("事件依据", event)))); - host.append(panel, omitted(trace.stats.events_omitted), details("完整范围与来源", result)); return; - } - const result = await api("task", {project_id: projectId, run_id: runId}); - const c = result.context; - if (!c) { host.append(empty("缺少接续上下文", "这次尝试尚未保存上下文或检查点,可先查看审阅轨迹。")); return; } - const external = result.kind === "external"; - const pack = external ? c : (c.context_pack || {}); - const checkpoint = external ? c : c.checkpoint; - const task = pack.task || {}; - host.append(node("section", "panel panel-pad", section(task.title || c.work_item?.title || "任务目标"), task.url ? link("查看原始 Issue", task.url) : document.createDocumentFragment(), node("p", "meta", external ? `检查点版本 ${c.revision} · ${name(c.remote_freshness)}` : `上下文保存于 ${date(c.context_metadata?.created_at)}`))); - if (!external) host.append(node("div", "callout", "这是已保存的维护上下文,当前工作区适用性尚未重新核对。")); - if (external && values(c.validity).length) host.append(node("div", "callout", "当前基线存在变化:", list(c.validity))); - if (!checkpoint) host.append(empty("尚无检查点", "当前仍可阅读任务目标与原始上下文。")); - const left = node("div", "", node("section", "panel panel-pad", section("未完成工作"), list(external ? c.open_work : checkpoint?.remaining)), node("section", "panel panel-pad", section("已经做出的决定"), list(checkpoint?.decisions))); - const right = node("div", "", node("section", "panel panel-pad", section("下一步"), node("p", "", checkpoint?.next_action || "尚未记录"), section("阻塞"), list(checkpoint?.blockers, "尚未记录阻塞。")), node("section", "panel panel-pad", section("接续入口"), command(result.command))); - host.append(node("div", "split", left, right)); - const contract = external ? c.contract : pack.task_contract; - if (contract) host.append(node("section", "panel panel-pad", section("目标与验收约束"), node("p", "", contract.goal || "尚未记录目标"), node("h3", "", "验收条件"), list(contract.acceptance_criteria, "原始契约未单列验收条件。"), node("h3", "", "范围约束"), list(contract.scope_boundaries, "原始契约未单列范围约束。"), contract.source_requirements ? node("p", "", contract.source_requirements) : null, details("契约来源与版本", contract))); - host.append(details("Agent 自述(尚未独立核验)", external ? c.agent_claims : {completed: checkpoint?.completed, tests_observed: checkpoint?.tests_observed}), details("完整接续数据、来源与省略说明", c), button("复制接续数据", () => copy(JSON.stringify(c, null, 2)), true)); - } - - async function settings(host) { - host.append(hero("知道当前运行的是什么。", "核对安装、配置来源与台账状态;此页只诊断,不执行升级或认证。", "LOCAL DIAGNOSTICS")); - const result = await api("settings"); - const install = result.installation, config = result.configuration; - host.append(node("section", "panel panel-pad", section("当前安装"), pairs([["包版本", install.version], ["Python", install.python], ["代码位置", install.module_path], ["来源提交", install.source_revision || "wheel 未提供,请核对安装记录"]]))); - const panel = node("section", "panel panel-pad", section("本地台账")); - for (const [key, database] of Object.entries(result.databases || {})) panel.append(node("div", "item", node("div", "", node("h3", "", key === "tasks" ? "任务数据库" : "项目注册表"), node("p", "meta", database.path), node("span", "meta", `当前 ${database.schema ?? "未知"} / 支持 ${database.supported_schema}`)), badge(database.status, database.status === "compatible" ? "good" : "warn"))); - host.append(panel, node("section", "panel panel-pad", section("生效配置与来源"), node("pre", "", JSON.stringify(config, null, 2)))); - if (values(result.next_actions).length) host.append(node("section", "panel panel-pad", section("诊断建议"), list(result.next_actions))); - host.append(command("reposteward doctor --local"), node("p", "meta", "配置发生变化后需重启工作台。浏览器会话最长有效 12 小时,进程退出后立即失效。")); - } - - async function render() { - controller.abort(); controller = new AbortController(); const current = ++generation; - let parts; - try { parts = location.hash.slice(1).split("/").map(decodeURIComponent); } catch { parts = []; } - const view = labels[parts[0]] ? parts[0] : "today"; - document.getElementById("page-label").textContent = labels[view]; - document.title = labels[view] + " · RepoSteward"; - for (const anchor of document.querySelectorAll("nav a")) { - if (anchor.dataset.view === view) anchor.setAttribute("aria-current", "page"); else anchor.removeAttribute("aria-current"); - } - content.replaceChildren(); content.setAttribute("aria-busy", "true"); - if (!token) { - content.append(hero("连接你的本地工作空间。", "请使用终端打印的完整会话链接打开。链接只在本机、当前进程内有效。"), command("reposteward web")); - connection.textContent = "会话未连接"; connection.className = "badge warn"; content.setAttribute("aria-busy", "false"); return; - } - const host = node("div", ""); content.append(host); - const loading = node("p", "meta", "正在读取本地数据…"); host.append(loading); - try { - if (view !== "settings") { - const result = await api("projects"); - if (current !== generation) return; - projectCache = values(result.projects); - if (result.omitted) host.append(omitted(result.omitted, "个项目")); - } - loading.remove(); connection.textContent = "本地已连接"; connection.className = "badge good"; - if (view === "today") await today(host); - else if (view === "projects") await projects(host, parts[1], parts[2]); - else if (view === "settings") await settings(host); - else await taskPage(host, view, parts[1], parts[2]); - } catch (error) { - if (error.name !== "AbortError") { loading.remove(); host.append(errorBox(error)); } - } finally { if (current === generation) content.setAttribute("aria-busy", "false"); } - } - document.querySelector(".skip").addEventListener("click", event => { event.preventDefault(); content.focus(); content.scrollIntoView(); }); - document.getElementById("reload").addEventListener("click", render); - window.addEventListener("hashchange", render); - render(); -})(); diff --git a/src/reposteward/web/index.html b/src/reposteward/web/index.html deleted file mode 100644 index 7dcf6c6..0000000 --- a/src/reposteward/web/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - RepoSteward · 本地工作台 - - - - - -
- -
-
连接中
-
正在读取本地工作空间…
-
RepoSteward 项目事实 · 任务上下文 · 验证证据
-
-
- - - diff --git a/src/reposteward/web_api/__init__.py b/src/reposteward/web_api/__init__.py new file mode 100644 index 0000000..39a2b42 --- /dev/null +++ b/src/reposteward/web_api/__init__.py @@ -0,0 +1 @@ +"""Typed local HTTP adapter for shared workbench services.""" diff --git a/src/reposteward/web_api/app.py b/src/reposteward/web_api/app.py new file mode 100644 index 0000000..59cc509 --- /dev/null +++ b/src/reposteward/web_api/app.py @@ -0,0 +1,380 @@ +"""FastAPI queries with an exact loopback authority and bounded ASGI transport.""" + +from __future__ import annotations + +import hmac +import json +import re +import secrets +import sqlite3 +import time +import uuid +from dataclasses import dataclass, field +from datetime import UTC, datetime +from threading import BoundedSemaphore +from typing import Annotated + +from fastapi import FastAPI, HTTPException, Query, Request +from fastapi.exceptions import RequestValidationError, ResponseValidationError +from fastapi.responses import JSONResponse, Response +from starlette.concurrency import run_in_threadpool + +from ..external_tasks import TaskConflict +from ..projects import ProjectError +from ..workbench import Workbench +from . import schemas as dto +from .assets import load_assets + +MAX_RESPONSE_BYTES = 400_000 +SESSION_SECONDS = 12 * 60 * 60 +ID = Annotated[str, Query(pattern=r"^[0-9a-f]{32}$")] +SECURITY_HEADERS = { + "Cache-Control": "no-store", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Cross-Origin-Resource-Policy": "same-origin", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", + "Content-Security-Policy": "default-src 'none'; script-src 'self'; style-src 'self'; " + "connect-src 'self'; img-src 'self'; base-uri 'none'; " + "frame-ancestors 'none'; form-action 'none'", +} +LEGACY = { + "/api/overview": ("overview", (), ()), + "/api/projects": ("projects", (), ()), + "/api/workspace": ("workspace", ("project_id", "binding_id"), ("focus",)), + "/api/code": ("code", ("project_id", "binding_id", "evidence_id"), ("start_line",)), + "/api/tasks": ("tasks", ("project_id",), ()), + "/api/task": ("task", ("project_id", "run_id"), ()), + "/api/review": ("review", ("project_id", "run_id"), ()), + "/api/settings": ("settings", (), ()), +} +UI_ROUTE = re.compile( + r"/(?:projects(?:/[0-9a-f]{32}(?:/(?:workspaces/[0-9a-f]{32}|" + r"tasks(?:/[0-9a-f]{32}(?:/review)?)?))?)?|settings|tasks|review)?" +) + + +@dataclass +class LocalSession: + authority: str + token: str = field(default_factory=lambda: secrets.token_urlsafe(32)) + expires: float = field(default_factory=lambda: time.monotonic() + SESSION_SECONDS) + + @property + def origin(self) -> str: + return "http://" + self.authority + + +def error(status: int, code: str, message: str, request_id: str = "") -> JSONResponse: + return JSONResponse( + dto.ErrorResponse( + error=dto.ErrorDetail(code=code, message=message, request_id=request_id) + ).model_dump(), + status_code=status, + headers=SECURITY_HEADERS, + ) + + +class Boundary: + def __init__(self, app, *, session: LocalSession): + self.app, self.session = app, session + self.slots = BoundedSemaphore(4) + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + if scope["type"] == "websocket": + await send({"type": "websocket.close", "code": 1008}) + return + return await self.app(scope, receive, send) + headers = scope["headers"] + + def values(name): + return [v for k, v in headers if k == name] + + async def reject(status, code, message): + await error(status, code, message)(scope, receive, send) + + if ( + scope.get("client", ("127.0.0.1",))[0] != "127.0.0.1" + or values(b"host") != [self.session.authority.encode()] + or values(b"origin") not in ([], [self.session.origin.encode()]) + or values(b"sec-fetch-site") not in ([], [b"none"], [b"same-origin"]) + ): + return await reject(403, "origin", "仅接受当前本地工作台的请求。") + raw = scope.get("raw_path", b"") + if ( + len(raw) + len(scope["query_string"]) > 4096 + or sum(len(k) + len(v) for k, v in headers) > 16_000 + or b"%" in raw + or b"\\" in raw + or not raw.startswith(b"/") + or raw.startswith(b"//") + ): + return await reject(400, "invalid_request", "请求超出读取边界。") + if scope["method"] != "GET": + return await reject( + 405, "read_only", "此版本提供读取,请使用已有 CLI 执行操作。" + ) + if values(b"transfer-encoding") or values(b"content-length") not in ( + [], + [b"0"], + ): + return await reject(400, "invalid_request", "读取请求不能包含请求体。") + if scope["path"].startswith("/api/"): + auth = values(b"authorization") + if ( + len(auth) != 1 + or not hmac.compare_digest( + auth[0], ("Bearer " + self.session.token).encode() + ) + or time.monotonic() >= self.session.expires + ): + return await reject( + 401, "session", "请使用终端打印的本地会话链接重新连接。" + ) + if not self.slots.acquire(blocking=False): + return await reject(429, "busy", "本地读取繁忙,请稍后重试。") + start = None + body = bytearray() + too_large = False + + async def bounded_send(message): + nonlocal start, too_large + if message["type"] == "http.response.start": + start = message + elif message["type"] == "http.response.body": + chunk = message.get("body", b"") + limit = ( + MAX_RESPONSE_BYTES + if scope["path"].startswith("/api/") + else 2_000_000 + ) + if len(body) + len(chunk) > limit: + too_large = True + if not too_large: + body.extend(chunk) + if not message.get("more_body", False): + if too_large: + await reject( + 400, + "response_limit", + "读取结果超出范围,请缩小选择或使用 CLI。", + ) + else: + replaced = {key.lower().encode() for key in SECURITY_HEADERS} + start["headers"] = [ + (k, v) + for k, v in start["headers"] + if k.lower() not in replaced + ] + start["headers"].extend( + (k.encode(), v.encode()) + for k, v in SECURITY_HEADERS.items() + ) + await send(start) + await send({"type": "http.response.body", "body": bytes(body)}) + + try: + await self.app(scope, receive, bounded_send) + finally: + self.slots.release() + + +def create_app( + workbench: Workbench | None = None, *, session: LocalSession | None = None +) -> FastAPI: + """Construct routes without authentication, database initialization or network.""" + session = session or LocalSession("127.0.0.1:0") + app = FastAPI( + title="RepoSteward local workbench", + version="1", + docs_url=None, + redoc_url=None, + openapi_url=None, + ) + app.state.workbench = workbench + app.state.session = session + + async def failed(request: Request, exc: Exception): + if isinstance(exc, TaskConflict): + return error(409, "changed", "配置或工作区已变化,请核对后重新打开工作台。") + if isinstance(exc, ProjectError): + return error( + 409, "binding_or_policy", "项目绑定或策略不可用,请检查关联与配置。" + ) + if isinstance(exc, KeyError): + return error(404, "not_found", "所选对象不存在,或不属于当前项目。") + if isinstance(exc, (ValueError, RequestValidationError)): + return error(400, "invalid_request", "请求参数不可用或超出读取范围。") + if isinstance(exc, HTTPException): + return error(exc.status_code, "not_found", "没有这个读取入口。") + return error(503, "unavailable", "本地数据暂不可读,请检查设置诊断。") + + for cls in ( + TaskConflict, + ProjectError, + KeyError, + ValueError, + RequestValidationError, + HTTPException, + ResponseValidationError, + OSError, + RuntimeError, + sqlite3.Error, + ): + app.add_exception_handler(cls, failed) + + def read(method, **kwargs): + if workbench is None: + raise RuntimeError("no runtime service configured") + workbench.check_configuration() + result = getattr(workbench, method)(**kwargs) + workbench.check_configuration() + return { + "data": result, + "meta": { + "request_id": uuid.uuid4().hex, + "generated_at": datetime.now(UTC).isoformat(), + "api_version": "1", + }, + } + + @app.get( + "/api/v1/session", + response_model=dto.Response[dto.Session], + operation_id="session", + ) + def current_session(): + _, digest = load_assets() + return { + "data": { + "capabilities": ["read_local"], + "expires_in_seconds": max(0, int(session.expires - time.monotonic())), + "frontend_digest": digest, + }, + "meta": { + "request_id": uuid.uuid4().hex, + "generated_at": datetime.now(UTC).isoformat(), + }, + } + + @app.get( + "/api/v1/projects", + response_model=dto.Response[dto.Projects], + operation_id="projects", + ) + def projects(): + return read("projects") + + @app.get( + "/api/v1/overview", + response_model=dto.Response[dto.Overview], + operation_id="overview", + ) + def overview(): + return read("overview") + + @app.get( + "/api/v1/workspace", + response_model=dto.Response[dto.Workspace], + operation_id="workspace", + ) + def workspace( + project_id: ID, + binding_id: ID, + focus: Annotated[str, Query(max_length=2000)] = "", + ): + return read( + "workspace", project_id=project_id, binding_id=binding_id, focus=focus + ) + + @app.get("/api/v1/code", response_model=dto.Response[dto.Code], operation_id="code") + def code( + project_id: ID, + binding_id: ID, + evidence_id: Annotated[str, Query(pattern=r"^code:[0-9a-f]{64}$")], + start_line: Annotated[str, Query(pattern=r"^[1-9][0-9]{0,6}$")] = "1", + ): + return read( + "code", + project_id=project_id, + binding_id=binding_id, + evidence_id=evidence_id, + start_line=start_line, + ) + + @app.get( + "/api/v1/tasks", response_model=dto.Response[dto.Tasks], operation_id="tasks" + ) + def tasks(project_id: ID): + return read("tasks", project_id=project_id) + + @app.get("/api/v1/task", response_model=dto.Response[dto.Task], operation_id="task") + def task(project_id: ID, run_id: ID): + return read("task", project_id=project_id, run_id=run_id) + + @app.get( + "/api/v1/review", response_model=dto.Response[dto.Review], operation_id="review" + ) + def review(project_id: ID, run_id: ID): + return read("review", project_id=project_id, run_id=run_id) + + @app.get( + "/api/v1/settings", + response_model=dto.Response[dto.Settings], + operation_id="settings", + ) + def settings(): + return read("settings") + + @app.get("/api/v1/openapi.json", include_in_schema=False) + def schema(): + return app.openapi() + + @app.get("/{path:path}", include_in_schema=False) + async def fallback(request: Request, path: str): + route = "/" + path + if route in LEGACY: + method, required, optional = LEGACY[route] + pairs = list(request.query_params.multi_items()) + params = dict(pairs) + if ( + len(params) != len(pairs) + or not set(required).issubset(params) + or not set(params).issubset((*required, *optional)) + or any(len(v) > 2000 for v in params.values()) + ): + raise ValueError("invalid query") + result = await run_in_threadpool(read, method, **params) + return JSONResponse(result["data"]) + assets, _ = load_assets() + if not request.url.query and route in assets: + body, mime = assets[route] + return Response(body, media_type=mime) + if UI_ROUTE.fullmatch(route): + return Response(assets["/"][0], media_type="text/html") + raise HTTPException(404) + + # FastAPI otherwise silently ignores unknown / repeated query parameters. + @app.middleware("http") + async def strict_queries(request: Request, call_next): + path = request.url.path + if path.startswith("/api/v1/"): + legacy = LEGACY.get(path.replace("/api/v1/", "/api/", 1)) + allowed = (*legacy[1], *legacy[2]) if legacy else () + pairs = list(request.query_params.multi_items()) + if len(dict(pairs)) != len(pairs) or any( + k not in allowed for k, _ in pairs + ): + return error(400, "invalid_request", "请求包含未知或重复参数。") + return await call_next(request) + + app.add_middleware(Boundary, session=session) + return app + + +if __name__ == "__main__": + print( + json.dumps(create_app().openapi(), ensure_ascii=False, indent=2, sort_keys=True) + ) diff --git a/src/reposteward/web_api/assets.py b/src/reposteward/web_api/assets.py new file mode 100644 index 0000000..f9b1c1b --- /dev/null +++ b/src/reposteward/web_api/assets.py @@ -0,0 +1,44 @@ +"""Load only build-manifest assets from an installed wheel or source checkout.""" + +from __future__ import annotations + +import hashlib +import json +import re +from functools import lru_cache +from importlib.resources import files +from pathlib import Path + + +@lru_cache(maxsize=1) +def load_assets() -> tuple[dict[str, tuple[bytes, str]], str]: + root = files("reposteward").joinpath("web_dist") + if not root.joinpath("assets.json").is_file(): + root = Path(__file__).resolve().parents[3] / "frontend/dist" + try: + manifest = json.loads(root.joinpath("assets.json").read_text()) + if manifest["version"] != 1 or len(manifest["files"]) > 100: + raise ValueError("invalid manifest") + assets = {} + for entry in manifest["files"]: + path = entry["path"] + if path != "index.html" and not re.fullmatch( + r"assets/[A-Za-z0-9_.-]+", path + ): + raise ValueError("invalid asset path") + body = root.joinpath(path).read_bytes() + if ( + len(body) > 2_000_000 + or hashlib.sha256(body).hexdigest() != entry["sha256"] + ): + raise ValueError("asset digest mismatch") + assets["/" if path == "index.html" else "/" + path] = (body, entry["mime"]) + assets["/app.js"] = assets["/" + manifest["entry_js"]] + assets["/app.css"] = assets["/" + manifest["entry_css"]] + if "/" not in assets: + raise ValueError("missing index") + return assets, manifest["inputs_digest"] + except (OSError, KeyError, TypeError, ValueError) as exc: + raise RuntimeError( + "Workbench assets missing or invalid; build or reinstall RepoSteward." + ) from exc diff --git a/src/reposteward/web_api/schemas.py b/src/reposteward/web_api/schemas.py new file mode 100644 index 0000000..bb02864 --- /dev/null +++ b/src/reposteward/web_api/schemas.py @@ -0,0 +1,205 @@ +"""Public workbench DTOs; historical protocol payloads remain versioned JSON.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, JsonValue + + +class Record(BaseModel): + # Workbench already curates these domain records. Preserve provenance and + # coverage extensions instead of silently discarding historical evidence. + model_config = ConfigDict(extra="allow") + __pydantic_extra__: dict[str, JsonValue] = Field(init=False) + + +class Meta(BaseModel): + request_id: str + generated_at: str + api_version: Literal["1"] = "1" + + +class Response[T](BaseModel): + data: T + meta: Meta + + +class ErrorDetail(BaseModel): + code: str + message: str + request_id: str = "" + retryable: bool = False + + +class ErrorResponse(BaseModel): + error: ErrorDetail + + +class Policy(BaseModel): + mode: str + enabled: bool + task_access: bool + + +class WorkspaceRef(BaseModel): + id: str + root: str + + +class Project(Record): + id: str + identity: str + host: str + repository: str + name: str + created_at: str + workspace_count: int = 0 + missing_workspaces: int = 0 + workspace_check_omitted: int = 0 + workspaces: list[WorkspaceRef] = Field(default_factory=list) + policy: Policy + + +class Projects(Record): + projects: list[Project] + omitted: int + public_write: Literal[False] = False + + +class Attention(Record): + id: str + priority: int + summary: str + reason_code: str + run_id: str = "" + pull_number: int = 0 + issue_number: int = 0 + next_command: str = "" + source_updated_at: str = "" + + +class SourceStatus(Record): + status: str + fetched_at: str = "" + error: str = "" + + +class OverviewProject(Record): + project_id: str + repository: str + complete: bool + items: list[Attention] + omitted: int + sources: SourceStatus + + +class Overview(Record): + projects: list[OverviewProject] + complete: bool + omitted_projects: int + excluded_projects: int + observed_at: str + overview_digest: str + + +class Source(Record): + evidence_id: str + path: str + line: int + end_line: int + digest: str + trust: str + url: str = "" + + +class ReadingStep(Record): + step: int + path: str + reason: str + summary: str = "" + language: str + source: Source + + +class Guide(Record): + status: str + repository: str + reading_path: list[ReadingStep] = Field(default_factory=list) + scanned_at: str = "" + limitations: list[str] = Field(default_factory=list) + + +class WorkspaceState(BaseModel): + head: str + branch: str + dirty: bool + + +class Commands(BaseModel): + scan: str + mcp_clients: dict[str, str] + + +class Workspace(Record): + project: dict[str, JsonValue] + binding: dict[str, JsonValue] + workspace: WorkspaceState + policy: Policy + guide: Guide + commands: Commands + command_shell: str + + +class Code(Record): + evidence_id: str + status: str + text: str + source: Source | None = None + truncated: bool = False + + +class TaskAttempt(BaseModel): + id: str + issue_number: int + stage: str + status: str + updated_at: str + title: str | None + + +class Tasks(Record): + tasks: list[TaskAttempt] + omitted: int + status: str + + +class Task(Record): + run_id: str + kind: Literal["external", "managed"] + context: dict[str, JsonValue] | None + context_status: str + current_applicability: str + command: str + + +class Review(Record): + run_id: str + trace: dict[str, JsonValue] + verification: dict[str, JsonValue] | None + decision_freshness: Literal["historical_only"] + publication_eligible: Literal[False] + + +class Settings(Record): + installation: dict[str, JsonValue] + configuration: dict[str, JsonValue] + databases: dict[str, JsonValue] + next_actions: list[str] + + +class Session(BaseModel): + capabilities: list[Literal["read_local"]] + expires_in_seconds: int + api_version: Literal["1"] = "1" + frontend_digest: str diff --git a/src/reposteward/web_server.py b/src/reposteward/web_server.py index 0213bac..a5a7e72 100644 --- a/src/reposteward/web_server.py +++ b/src/reposteward/web_server.py @@ -1,226 +1,96 @@ -"""A bounded loopback-only HTTP adapter; no file server or mutation routes.""" +"""CLI-compatible lifecycle for the FastAPI loopback workbench.""" from __future__ import annotations -import hmac -import json -import secrets import socket -import sqlite3 -import time -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from importlib.resources import files -from threading import BoundedSemaphore -from urllib.parse import parse_qs, urlsplit +from threading import Event + +import uvicorn from .config import AppConfig -from .external_tasks import TaskConflict -from .projects import ProjectError +from .web_api.app import LocalSession, create_app +from .web_api.assets import load_assets from .workbench import Workbench -MAX_RESPONSE_BYTES = 400_000 -SESSION_SECONDS = 12 * 60 * 60 -ASSETS = { - "/": ("index.html", "text/html; charset=utf-8"), - "/app.js": ("app.js", "text/javascript; charset=utf-8"), - "/app.css": ("app.css", "text/css; charset=utf-8"), -} -ROUTES = { - "/api/overview": ("overview", (), ()), - "/api/projects": ("projects", (), ()), - "/api/workspace": ("workspace", ("project_id", "binding_id"), ("focus",)), - "/api/code": ("code", ("project_id", "binding_id", "evidence_id"), ("start_line",)), - "/api/tasks": ("tasks", ("project_id",), ()), - "/api/task": ("task", ("project_id", "run_id"), ()), - "/api/review": ("review", ("project_id", "run_id"), ()), - "/api/settings": ("settings", (), ()), -} - - -class LocalServer(ThreadingHTTPServer): - daemon_threads = True - allow_reuse_address = False - request_queue_size = 8 + +class LocalServer: + """Own the bound socket and session; retain the existing local lifecycle API.""" def __init__(self, app: Workbench, *, port: int = 0): if type(port) is not int or not 0 <= port <= 65535: raise ValueError("port must be between 0 and 65535") self.app = app - self.session = secrets.token_urlsafe(32) - self.expires = time.monotonic() + SESSION_SECONDS - self.slots = BoundedSemaphore(4) - self.assets = { - route: (files("reposteward").joinpath("web", name).read_bytes(), mime) - for route, (name, mime) in ASSETS.items() - } - super().__init__(("127.0.0.1", port), Handler) + self.assets, _ = load_assets() + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + self.socket.bind(("127.0.0.1", port)) + self.socket.listen(8) + except BaseException: + self.socket.close() + raise + self.server_address = self.socket.getsockname() + self.server_port = self.server_address[1] self.authority = f"127.0.0.1:{self.server_port}" - self.origin = "http://" + self.authority + self.capability = LocalSession(self.authority) + self.origin = self.capability.origin + self.session = self.capability.token + self.stopped = Event() + self.stopped.set() + self.asgi = create_app(app, session=self.capability) + self.server = uvicorn.Server( + uvicorn.Config( + self.asgi, + host="127.0.0.1", + port=self.server_port, + workers=1, + proxy_headers=False, + access_log=False, + log_config=None, + log_level="critical", + server_header=False, + timeout_keep_alive=1, + timeout_graceful_shutdown=5, + h11_max_incomplete_event_size=16_384, + ) + ) + + @property + def expires(self) -> float: + return self.capability.expires + + @expires.setter + def expires(self, value: float) -> None: + self.capability.expires = value @property def url(self) -> str: return self.origin + "/#session=" + self.session - def get_request(self) -> tuple[socket.socket, tuple]: - request, address = super().get_request() - request.settimeout(5) - return request, address - def verify_request(self, request, client_address) -> bool: return client_address[0] == "127.0.0.1" - def process_request(self, request, client_address) -> None: - if not self.slots.acquire(blocking=False): - self.shutdown_request(request) - return + def serve_forever(self, *, poll_interval: float = 0.2) -> None: + self.stopped.clear() try: - super().process_request(request, client_address) - except BaseException: - self.slots.release() - raise - - def process_request_thread(self, request, client_address) -> None: - try: - super().process_request_thread(request, client_address) + self.server.run(sockets=[self.socket]) finally: - self.slots.release() - - def handle_error(self, request, client_address) -> None: - # Neither arbitrary requests, repository data nor local session keys go to logs. - pass - - -class Handler(BaseHTTPRequestHandler): - server: LocalServer - server_version = "RepoSteward" - sys_version = "" - - def log_message(self, format, *args) -> None: - pass - - def _send(self, status: int, body: bytes, mime: str = "application/json") -> None: - self.close_connection = True - self.send_response(status) - self.send_header("Content-Type", mime) - self.send_header("Content-Length", str(len(body))) - self.send_header("Connection", "close") - self.send_header("Cache-Control", "no-store") - self.send_header("Referrer-Policy", "no-referrer") - self.send_header("X-Content-Type-Options", "nosniff") - self.send_header("X-Frame-Options", "DENY") - self.send_header("Cross-Origin-Resource-Policy", "same-origin") - self.send_header( - "Permissions-Policy", "camera=(), microphone=(), geolocation=()" - ) - self.send_header( - "Content-Security-Policy", - "default-src 'none'; script-src 'self'; style-src 'self'; " - "connect-src 'self'; img-src 'self'; base-uri 'none'; " - "frame-ancestors 'none'; form-action 'none'", - ) - self.end_headers() - if self.command != "HEAD": - self.wfile.write(body) + self.stopped.set() - def _error(self, status: int, code: str, message: str) -> None: - self._send( - status, json.dumps({"error": {"code": code, "message": message}}).encode() - ) + def shutdown(self) -> None: + self.server.should_exit = True + if not self.stopped.wait(timeout=10): + raise RuntimeError("local server did not complete shutdown") - def send_error(self, code, message=None, explain=None) -> None: - self._error(code, "invalid_request", "请求不可用。请使用工作台提供的入口。") + def server_close(self) -> None: + self.shutdown() + self.socket.close() - def do_GET(self) -> None: - try: - self._get() - except TaskConflict: - self._error(409, "changed", "配置或工作区已变化,请核对后重新打开工作台。") - except ProjectError: - self._error( - 409, - "binding_or_policy", - "项目绑定或策略不可用,请在 CLI 检查关联与配置。", - ) - except KeyError: - self._error( - 404, "not_found", "所选项目、工作区或任务不存在,或不属于当前范围。" - ) - except ValueError: - self._error( - 400, - "invalid_request", - "请求超出读取边界,请检查选择或使用 CLI 查看完整内容。", - ) - except (OSError, RuntimeError, sqlite3.Error): - self._error( - 503, - "unavailable", - "本地数据暂不可读。请检查设置诊断;其他页面仍可使用。", - ) + def __enter__(self): + return self - def _get(self) -> None: - if ( - self.headers.get_all("Host") != [self.server.authority] - or self.headers.get_all("Origin") not in (None, [self.server.origin]) - or self.headers.get("Sec-Fetch-Site", "none") not in {"none", "same-origin"} - ): - self._error(403, "origin", "仅接受当前本地工作台的请求。") - return - if ( - len(self.path) > 4096 - or len(str(self.headers)) > 16_000 - or self.headers.get_all("Transfer-Encoding") - or self.headers.get_all("Content-Length") not in (None, ["0"]) - ): - self._error(400, "invalid_request", "请求超出读取边界。") - return - parsed = urlsplit(self.path) - if parsed.scheme or parsed.netloc or parsed.fragment or "%" in parsed.path: - raise ValueError("invalid route") - if parsed.path in self.server.assets and not parsed.query: - body, mime = self.server.assets[parsed.path] - self._send(200, body, mime) - return - authorization = self.headers.get_all("Authorization") or [] - if ( - len(authorization) != 1 - or not hmac.compare_digest( - authorization[0].encode(), ("Bearer " + self.server.session).encode() - ) - or time.monotonic() >= self.server.expires - ): - self._error(401, "session", "请用终端打印的本地会话链接重新打开工作台。") - return - route = ROUTES.get(parsed.path) - if route is None: - self._error(404, "not_found", "没有这个读取入口。") - return - method, required, optional = route - query = parse_qs( - parsed.query, strict_parsing=True, keep_blank_values=True, max_num_fields=6 - ) - if ( - not set(required).issubset(query) - or not set(query).issubset((*required, *optional)) - or any(len(value) != 1 or len(value[0]) > 2000 for value in query.values()) - ): - raise ValueError("invalid query") - self.server.app.check_configuration() - result = getattr(self.server.app, method)(**{k: v[0] for k, v in query.items()}) - self.server.app.check_configuration() - body = json.dumps(result, ensure_ascii=False).encode() - if len(body) > MAX_RESPONSE_BYTES: - raise ValueError("read result exceeds response limit") - self._send(200, body) - - def do_POST(self) -> None: - self._error(405, "read_only", "工作台仅提供读取;请通过已有 CLI 执行明确操作。") - - do_PUT = do_POST - do_PATCH = do_POST - do_DELETE = do_POST - do_OPTIONS = do_POST - do_HEAD = do_POST + def __exit__(self, *args): + self.shutdown() + self.server_close() def serve(config: AppConfig, *, port: int = 0) -> None: @@ -228,6 +98,6 @@ def serve(config: AppConfig, *, port: int = 0) -> None: print(f"RepoSteward 本地工作台:{server.url}", flush=True) print("仅本机、只读。链接在本次进程中有效;按 Ctrl+C 停止。", flush=True) try: - server.serve_forever(poll_interval=0.2) + server.serve_forever() except KeyboardInterrupt: pass diff --git a/tests/test_frontend_build.py b/tests/test_frontend_build.py new file mode 100644 index 0000000..055e122 --- /dev/null +++ b/tests/test_frontend_build.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import json +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +from reposteward.web_api.assets import load_assets +from reposteward.workspace import sanitized_environment + + +class FrontendBuildTests(unittest.TestCase): + def test_packaged_manifest_verifies_every_asset(self): + assets, digest = load_assets() + self.assertEqual(len(digest), 64) + self.assertIn("/", assets) + self.assertTrue(any(path.startswith("/assets/") for path in assets)) + self.assertIn(b"/assets/", assets["/"][0]) + self.assertNotIn(b"/src/", assets["/"][0]) + + @unittest.skipUnless( + shutil.which("npm"), "Frontend checks require the verifier Node toolchain" + ) + def test_frontend_types_and_user_interactions(self): + frontend = Path(__file__).resolve().parents[1] / "frontend" + lock = json.loads((frontend / "package-lock.json").read_text()) + package = json.loads((frontend / "package.json").read_text()) + self.assertEqual(lock["packages"][""]["dependencies"], package["dependencies"]) + self.assertEqual( + lock["packages"][""]["devDependencies"], package["devDependencies"] + ) + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / "schema.ts" + generated = subprocess.run( + [ + str(frontend / "node_modules/.bin/openapi-typescript"), + "openapi.json", + "-o", + str(output), + ], + cwd=frontend, + capture_output=True, + text=True, + timeout=30, + env=sanitized_environment(keep_codex_credentials=False), + check=False, + ) + self.assertEqual( + generated.returncode, 0, generated.stdout + generated.stderr + ) + self.assertEqual( + output.read_bytes(), + (frontend / "src/api/generated/schema.ts").read_bytes(), + ) + result = subprocess.run( + ["npm", "run", "check"], + cwd=frontend, + capture_output=True, + text=True, + timeout=120, + env=sanitized_environment(keep_codex_credentials=False), + check=False, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) diff --git a/tests/test_workbench_asgi.py b/tests/test_workbench_asgi.py new file mode 100644 index 0000000..040860f --- /dev/null +++ b/tests/test_workbench_asgi.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import json +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +import test_workbench +from fastapi.testclient import TestClient + +from reposteward.web_api.app import LocalSession, create_app + + +class WorkbenchASGITests(unittest.TestCase): + def setUp(self): + self.service = Mock() + self.service.projects.return_value = { + "schema_version": 1, + "projects": [], + "omitted": 0, + "public_write": False, + } + self.session = LocalSession("127.0.0.1:8765") + self.app = create_app(self.service, session=self.session) + self.client = TestClient( + self.app, base_url="http://127.0.0.1:8765", client=("127.0.0.1", 40000) + ) + self.headers = {"Authorization": "Bearer " + self.session.token} + + def test_typed_response_preserves_readonly_facts(self): + result = self.client.get("/api/v1/projects", headers=self.headers) + self.assertEqual(result.status_code, 200) + self.assertEqual(result.json()["data"], self.service.projects.return_value) + self.assertEqual(result.json()["meta"]["api_version"], "1") + self.service.check_configuration.assert_called() + + def test_unknown_duplicate_and_unbound_parameters_do_not_reach_service(self): + for path in ( + "/api/v1/projects?path=/etc/passwd", + "/api/v1/projects?x=1&x=2", + "/api/v1/workspace?project_id=bad&binding_id=bad", + ): + with self.subTest(path=path): + self.assertEqual( + self.client.get(path, headers=self.headers).status_code, 400 + ) + self.service.projects.assert_not_called() + + self.service.workspace.assert_not_called() + + def test_authentication_precedes_query_validation(self): + response = self.client.get( + "/api/v1/projects?path=/etc/passwd", + headers={**self.headers, "Origin": "http://foreign.example"}, + ) + self.assertEqual(response.status_code, 403) + response = self.client.get("/api/v1/projects?path=/etc/passwd") + self.assertEqual(response.status_code, 401) + + def test_schema_can_be_exported_without_runtime_or_authentication(self): + with ( + patch( + "reposteward.github.resolve_token", side_effect=AssertionError("auth") + ), + patch("reposteward.store.Store", side_effect=AssertionError("store")), + ): + schema = create_app().openapi() + self.assertEqual(schema["openapi"], "3.1.0") + self.assertIn("Project", schema["components"]["schemas"]) + self.assertEqual( + schema["paths"]["/api/v1/projects"]["get"]["operationId"], "projects" + ) + saved = Path(__file__).resolve().parents[1] / "frontend/openapi.json" + self.assertEqual(schema, json.loads(saved.read_text())) + self.assertEqual(self.client.get("/api/v1/openapi.json").status_code, 401) + + def test_response_validation_does_not_leak_uncurated_objects(self): + self.service.projects.return_value = {"projects": "private-invalid-object"} + response = self.client.get("/api/v1/projects", headers=self.headers) + self.assertEqual(response.status_code, 503) + self.assertNotIn("private-invalid-object", response.text) + + def test_deep_link_and_missing_asset_have_distinct_results(self): + for route in ("/projects", "/projects/" + "a" * 32 + "?focus=cli", "/settings"): + response = self.client.get(route) + self.assertEqual(response.status_code, 200) + self.assertIn('id="root"', response.text) + for route in ("/assets/missing.js", "/api/v1/not-found", "/projects/invalid"): + response = self.client.get(route, headers=self.headers) + self.assertEqual(response.status_code, 404) + self.assertIn("application/json", response.headers["content-type"]) + + def test_no_transport_method_can_trigger_a_write(self): + for method in ("POST", "PATCH", "DELETE", "PUT"): + response = self.client.request( + method, + "/api/v1/projects", + headers=self.headers, + json={"repository": "owner/repo"}, + ) + self.assertEqual(response.status_code, 405) + self.service.projects.assert_not_called() + + +class WorkbenchResponseIntegrationTests(unittest.TestCase): + def test_real_project_guide_and_task_records_match_the_public_contract(self): + fixture = test_workbench.WorkbenchTests() + fixture.setUp() + self.addCleanup(fixture.doCleanups) + session = LocalSession("127.0.0.1:8765") + project = fixture.project_id + binding = fixture.binding_id + run = fixture.task["run_id"] + source = fixture.app.workspace(project, binding)["guide"]["reading_path"][0][ + "source" + ] + routes = { + "projects": {}, + "overview": {}, + "settings": {}, + "workspace": {"project_id": project, "binding_id": binding}, + "code": { + "project_id": project, + "binding_id": binding, + "evidence_id": source["evidence_id"], + }, + "tasks": {"project_id": project}, + "task": {"project_id": project, "run_id": run}, + "review": {"project_id": project, "run_id": run}, + } + with TestClient( + create_app(fixture.app, session=session), + base_url=session.origin, + client=("127.0.0.1", 40000), + ) as client: + for route, params in routes.items(): + with self.subTest(route=route): + response = client.get( + "/api/v1/" + route, + params=params, + headers={"Authorization": "Bearer " + session.token}, + ) + self.assertEqual(response.status_code, 200, response.text) + self.assertIn("data", response.json()) diff --git a/tests/test_workbench_http.py b/tests/test_workbench_http.py index 3ef7f02..7c9a427 100644 --- a/tests/test_workbench_http.py +++ b/tests/test_workbench_http.py @@ -22,9 +22,16 @@ def setUp(self): self.app = Mock() self.app.settings.return_value = {"status": "local", "public_write": False} self.server = LocalServer(self.app) + self.thread_errors = [] + + def serve(): + try: + self.server.serve_forever(poll_interval=0.01) + except BaseException as exc: # noqa: BLE001 - asserted by close_server + self.thread_errors.append(exc) + self.thread = Thread( - target=self.server.serve_forever, - kwargs={"poll_interval": 0.01}, + target=serve, daemon=True, ) self.thread.start() @@ -35,6 +42,7 @@ def close_server(self): self.server.server_close() self.thread.join(timeout=5) self.assertFalse(self.thread.is_alive()) + self.assertEqual(self.thread_errors, []) def request(self, path="/api/settings", *, method="GET", headers=None, token=True): connection = http.client.HTTPConnection( @@ -44,7 +52,11 @@ def request(self, path="/api/settings", *, method="GET", headers=None, token=Tru try: connection.request(method, path, headers={**default, **(headers or {})}) response = connection.getresponse() - return response.status, dict(response.getheaders()), response.read() + return ( + response.status, + {k.title(): v for k, v in response.getheaders()}, + response.read(), + ) finally: connection.close() @@ -106,7 +118,11 @@ def test_duplicate_host_or_authorization_header_is_rejected(self): connection.putheader(header, value) connection.endheaders() response = connection.getresponse() - self.assertEqual(response.status, expected) + # h11 may reject duplicate Host before the ASGI boundary. + if header == "Host": + self.assertIn(response.status, {400, expected}) + else: + self.assertEqual(response.status, expected) response.read() finally: connection.close() diff --git a/uv.lock b/uv.lock index 16a84ec..ff14619 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,15 @@ resolution-markers = [ "python_full_version < '3.14'", ] +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + [[package]] name = "annotated-types" version = "0.8.0" @@ -37,6 +46,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + [[package]] name = "cffi" version = "2.1.1" @@ -181,6 +199,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, ] +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -190,6 +224,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + [[package]] name = "httpcore2" version = "2.12.0" @@ -203,6 +250,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, ] +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "httpx2" version = "2.12.0" @@ -503,7 +565,10 @@ name = "reposteward" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "fastapi" }, { name = "jsonschema" }, + { name = "pydantic" }, + { name = "uvicorn" }, ] [package.optional-dependencies] @@ -516,19 +581,26 @@ mcp = [ [package.dev-dependencies] dev = [ + { name = "httpx" }, { name = "mcp" }, ] [package.metadata] requires-dist = [ + { name = "fastapi", specifier = ">=0.141.1,<0.142" }, { name = "jsonschema", specifier = ">=4.23,<5" }, { name = "mcp", marker = "extra == 'mcp'", specifier = ">=2.1.1,<3" }, { name = "openai-codex", marker = "extra == 'codex-sdk'", specifier = ">=0.147,<0.148" }, + { name = "pydantic", specifier = ">=2.10,<3" }, + { name = "uvicorn", specifier = ">=0.34,<1" }, ] provides-extras = ["codex-sdk", "mcp"] [package.metadata.requires-dev] -dev = [{ name = "mcp", specifier = ">=2.1.1,<3" }] +dev = [ + { name = "httpx", specifier = ">=0.28,<1" }, + { name = "mcp", specifier = ">=2.1.1,<3" }, +] [[package]] name = "rpds-py"