diff --git a/docs/adr/ADR-004-deno-runtime.md b/docs/adr/ADR-004-deno-runtime.md new file mode 100644 index 00000000..5d25d6fd --- /dev/null +++ b/docs/adr/ADR-004-deno-runtime.md @@ -0,0 +1,175 @@ +# ADR-004:运行时切换为 Deno 2.9.7(锁定)+ TypeScript 7.0.2 + +- 状态:提议(2026-09-27,Deno 准入 spike 完成后)。采纳后替代 ADR-001,ADR-001 的状态措辞由协调者修改。 +- 关联:REQ §9、§10.2;plan §1;验证记录 `docs/spikes/deno-2.9.7.md`;代码 `spikes/deno/` + +## 背景 + +用户要求运行时改成 Deno 最新稳定版并锁定版本,同时语言版本用 TypeScript 7.0.2;前端继续用 Vite 7 + React Router 7。ADR-001 当初选 Node 有三条理由:本机没有 Deno、依赖以 Node 为主要目标、工具链沿用 uhd_web。这次 spike 逐项核对了这三条:Deno 2.9.7 已装在 `/usr/local/bin/deno`;OpenWorkflow、Pi、Hono、Kysely/pg、pino、vitest、Vite/React Router 在 Deno 下的语义和 Node 一致;真实仓库 7 个包加 server 的测试和类型检查也都跑过。 + +## 决定 + +1. **运行时:Deno 2.9.7**,本机、CI、镜像三处锁同一版本。镜像用 `denoland/deno:2.9.7`,已确认有 amd64 和 arm64。依赖统一用 `deno install` + `deno.lock` 管理,彻底去掉 pnpm。 +2. **语言版本:TypeScript 7.0.2**,由 `npm:typescript@7.0.2/tsc` 提供。它是 Go 原生实现,入口名就叫 `tsc`,包里没有 `tsgo`。它是 CI 的**类型门禁**。另外加一道 `deno check`,覆盖 Deno 专有的解析:import map、`jsr:`、`Deno.*` 全局。这一道同时也是 `deno compile` 实际使用的检查,它内置的 TS 是 6.0.3。 +3. **依赖目录 `nodeModulesDir: "auto"`。** Vite、React Router、vitest、tsc 7 都要从 node_modules 解析依赖,所以必须保留这个目录。 +4. **workspace 成员同时放 `deno.json` 和精简的 `package.json`。** deno.json 负责 name/exports/tasks;package.json 只写 name/type/exports,作用是让 `deno install` 把成员软链到 `node_modules/@umt/*`,这样 Vite、vitest、tsc 7 不需要配置 alias 或 paths 就能解析。npm 依赖的版本**只在根 `deno.json` 的 `imports` 里登记一次**,相当于统一目录。 +5. **不改动的部分:** Hono 4(入口换成 `Deno.serve`)、Kysely 0.29 + pg 8(业务库)、postgres.js(只给 OpenWorkflow 用)、pino 10、vitest 5(通过 `deno run -A npm:vitest` 运行)、biome(通过 `npm:@biomejs/biome` 运行)、Vite 7 + React Router 7。 +6. **生产形态:单容器,一个 `deno compile` 二进制。** 它按 `--role api|worker|all` 运行 API 和 worker,并托管 web 静态产物。镜像基础是 `debian:trixie-slim`,spike 实测 260 MB。 + +## 理由 + +- spike 的结论是通过或有变通,没有不通过的项。和 Node 的差异都能在工程层面处理:不带扩展名的 import、Kysely 迁移导入路径、TS7 移除了 `baseUrl`、compile 打包体积。 +- 能拿回 Deno 的运行时权限模型,正好落实 ADR-001 列为代价的 REQ §10.2 要求。生产二进制在编译时就固定 `--allow-net/--allow-read=<白名单>/--allow-env/--allow-sys`,不给 `--allow-run`,除非后续确实需要 ffprobe 这类外部程序,那时按程序名列白名单。 +- 测试和工具链继续沿用现有资产(vitest 用例、biome 规则、Vite 配置),迁移的改动主要是机械替换。 + +## 代价 / 风险 + +- 类型检查分两套:TS 7.0.2 和 Deno 内置的 6.0.3。只要不用 TS7 独有语法就不会有分歧,spike 里两套结果一致。 +- `jsr:` 包 tsc 7 解析不了。规则是:业务代码只用 npm 发布的包;`@std/*` 只能出现在 `deno check` 覆盖、tsc 排除的文件里,目前没有这类文件。 +- compile 默认会把整个 node_modules 嵌进二进制(440 MB),必须加 `--node-modules-dir=none --exclude-unused-npm`。代码里如果有动态 `import()`,要用 `--include` 显式带上。 +- 有安装脚本的依赖默认不执行脚本。`protobufjs`、`@google/genai`、`msw` 被跳过后都不影响使用,和原来 pnpm 的 `allowBuilds` 结果一致。以后新增这类依赖时,要用 `deno approve-scripts` 显式批准。 + +## 迁移清单 + +### 1. 根目录 + +新增 `deno.json`,示意如下,版本照搬现有锁定: + +```jsonc +{ + "workspace": ["./packages/*", "./apps/server", "./apps/web"], + "nodeModulesDir": "auto", + "imports": { + "hono": "npm:hono@4.13.9", "hono/": "npm:/hono@4.13.9/", + "@hono/zod-openapi": "npm:@hono/zod-openapi@1.6.3", "@hono/swagger-ui": "npm:@hono/swagger-ui@0.6.1", + "kysely": "npm:kysely@0.29.6", "kysely/": "npm:/kysely@0.29.6/", + "pg": "npm:pg@8.23.0", "@types/pg": "npm:@types/pg@8.23.1", "postgres": "npm:postgres@3.4.9", + "openworkflow": "npm:openworkflow@0.10.1", "openworkflow/": "npm:/openworkflow@0.10.1/", + "@earendil-works/pi-agent-core": "npm:@earendil-works/pi-agent-core@0.87.1", + "@earendil-works/pi-agent-core/": "npm:/@earendil-works/pi-agent-core@0.87.1/", + "@earendil-works/pi-ai": "npm:@earendil-works/pi-ai@0.87.1", "typebox": "npm:typebox@1.3.27", + "zod": "npm:zod@4.3.6", "pino": "npm:pino@10.3.1", "pino-pretty": "npm:pino-pretty@13.1.3", + "cheerio": "npm:cheerio@…", "fast-xml-parser": "npm:fast-xml-parser@…", "nunjucks": "npm:nunjucks@…", + "@xmldom/xmldom": "npm:@xmldom/xmldom@…", "xpath": "npm:xpath@…", "@types/nunjucks": "npm:@types/nunjucks@…", + "vitest": "npm:vitest@5.0.2", "typescript": "npm:typescript@7.0.2", "@types/node": "npm:@types/node@24", + "@biomejs/biome": "npm:@biomejs/biome@2.5.14" + }, + "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, "verbatimModuleSyntax": true }, + "tasks": { + "dev": { "dependencies": ["dev:server", "dev:web"] }, + "dev:server": "cd apps/server && deno task dev", + "dev:web": "cd apps/web && deno task dev", + "db:up": "docker compose -f docker-compose.dev.yml up -d --wait", + "db:migrate": "deno run -A --env-file=.env packages/db/scripts/migrate.ts latest", + "db:reset": "deno run -A --env-file=.env packages/db/scripts/migrate.ts reset", + "types": "deno types > types/deno.d.ts", + "typecheck": "deno task types && deno task typecheck:tsc && deno task typecheck:deno", + "typecheck:tsc": "for p in packages/* apps/server; do deno run -A npm:typescript@7.0.2/tsc --noEmit -p $p/tsconfig.json || exit 1; done && cd apps/web && deno run -A npm:@react-router/dev@7.12.0/react-router typegen && deno run -A npm:typescript@7.0.2/tsc -p tsconfig.json", + "typecheck:deno": "deno check apps/server/src/main.ts packages/*/src/index.ts", + "lint": "deno run -A npm:@biomejs/biome@2.5.14 check .", + "format": "deno run -A npm:@biomejs/biome@2.5.14 check --write .", + "test": "for p in packages/* apps/server apps/web; do (cd $p && deno run -A npm:vitest@5.0.2 run) || exit 1; done", + "check:tokens": "cd apps/web && deno task check:tokens", + "build": "cd apps/web && deno task build && cd ../server && deno task compile", + "ci": "deno task typecheck && deno task lint && deno task test && deno task check:tokens && deno task build" + } +} +``` + +- lint/fmt:**保留 biome**(规则已调好,Deno 下验证可用),打开 `correctness/useImportExtensions` 来约束 import 必须带扩展名。`deno lint` 和 `deno fmt` 不纳入 CI,避免两套规则并存。 +- 删除:`pnpm-workspace.yaml`、`pnpm-lock.yaml`、根 `package.json`、`tsx` 依赖、`apps/server/scripts/build.mjs`(esbuild,由 `deno compile` 取代)、`@hono/node-server`、`esbuild` devDependency。`.gitignore` 增加 `types/deno.d.ts`。 +- `tsconfig.base.json`:保留(tsc 7 用),`types` 加一项 `"./types/deno.d.ts"`,或者只在 server 的配置里 include 它;其他选项在 TS7 下都已验证 0 错误。**不要**在根目录放 `tsconfig.json`,否则 `deno check` 会读它的 `lib`,导致 `Deno` 名字丢失。 +- `docker-compose.dev.yml` 不变。 + +### 2. 每个成员 + +`packages//deno.json`: + +```json +{ "name": "@umt/", "version": "0.0.0", "exports": { ".": "./src/index.ts" } } +``` + +`packages//package.json` 精简为以下内容,去掉 dependencies、scripts、`workspace:*`: + +```json +{ "name": "@umt/", "private": true, "type": "module", "exports": { ".": "./src/index.ts" } } +``` + +- 各包的 `tsconfig.json`(extends base)保留,用于 tsc 7。`vitest.config.ts` 保留,不用改。 +- `apps/server/deno.json`: + - `tasks.dev`:`deno run -A --watch --env-file=../../.env src/main.ts` + - `tasks.start`:`deno run --allow-net --allow-env --allow-sys --allow-read --env-file=../../.env src/main.ts` + - `tasks.compile`:`deno compile --frozen --node-modules-dir=none --exclude-unused-npm --allow-net --allow-env --allow-sys --allow-read=/app --output ../../dist/umt-server src/main.ts` +- `apps/web`:**保留 package.json**。它的 scripts 由 `deno task` 直接执行,会用 Deno 运行 `node_modules/.bin`,不依赖 node。改动两处:`gen:tokens` / `check:tokens` 里的 `node` 换成 `deno run -A`;`tsconfig.json` 删掉 `"baseUrl": "."`(TS5102),`paths` 保持 `"~/*": ["./app/*"]` 不变。`@umt/contracts` 的依赖写成 `"workspace:*"`,Deno 认这个写法。 + +### 3. 代码改动 + +| 点 | 改法 | +|---|---| +| 不带扩展名的相对 import(src 和 test 共约 400 行) | 一次性执行 `deno lint --sloppy-imports --fix --rules-include=no-sloppy-imports packages apps/server`(已验证能自动改成 `./x.ts` / `./dir/index.ts`),之后由 biome `useImportExtensions` 守住 | +| `apps/server/src/main.ts` | `@hono/node-server` 的 `serve` 换成 `Deno.serve({ port, hostname, signal: ac.signal }, app.fetch)`。关服时执行 `ac.abort(); await server.finished`,`closeAllConnections` 可以删掉。实测挂着 SSE 长连接也能在 0.2 秒内退出。`package.json with { type: "json" }` 读版本号的写法改成读 `deno.json` | +| 静态托管(单容器) | `import { serveStatic } from "hono/deno"`。写法是 `app.use("/*", serveStatic({ root }))`,再加 `app.get("*", serveStatic({ root, path: "index.html" }))`。path 不能写绝对路径,否则 404 | +| Kysely 迁移 | 已经在用 `kysely/migration`,不变。import map 需要有 `"kysely/"` 前缀映射 | +| `packages/tasks/test/crash-recovery.test.ts` 和 agent 的崩溃恢复测试 | 原来是 `spawn(process.execPath, ["--import","tsx", WORKER])`,改成 `spawn(process.execPath, ["run","-A", WORKER])`,并去掉 tsx。现状在 Deno 下 tasks 已经 15/15;agent 失败的根因是不带扩展名的 import,做完上一行的修复就好 | +| `packages/db/scripts/*.ts` | 由 `tsx --env-file-if-exists` 改成 `deno run -A --env-file=...`(Deno 的 `--env-file` 在文件不存在时只告警) | +| logger | pino 保留。顺带修两个 Node 下也存在的脱敏漏洞:加 `formatters.bindings` 脱敏 child bindings;`redact()` 遇到 `Error` 先按 message/stack 序列化,否则 `err` 会变成 `{}` | + +### 4. Node 专有 API 风险点(spike 中实测) + +| API | Deno 2.9.7 现状 | 建议 | +|---|---|---| +| `worker_threads`(pino transport、`new Worker(url,{type:"module"})`) | 可用 | 新代码可以直接用 Web Worker(`new Worker(url, { type: "module" })`) | +| `process.on("SIGTERM")` / `process.exit` / `process.env` / `process.pid` | 可用,Deno 2 下 `process` 是全局对象 | 入口层统一改成 `Deno.addSignalListener`、`Deno.exit`、`Deno.env`,库代码保留 `node:process` 也行 | +| `fs.statfs`(磁盘余量) | 可用,bsize/bavail 正确 | 不变 | +| `child_process.spawn` / `kill(SIGKILL/SIGTERM)` | 可用 | 生产要 `--allow-run=<程序名>`。新代码可以用 `Deno.Command` | +| `process.execPath` | 是 deno 本身 | 凡是"用当前运行时起子进程"的地方,参数要改成 `run -A `(见 §3) | +| `AsyncLocalStorage`、`node:crypto`、`node:util parseArgs`、`os.*` | 可用 | 不变 | +| `require` / `__dirname` | 仓库里只有 2 处 `__dirname`(脚本) | 改成 `import.meta.dirname` | + +### 5. 测试 + +- **继续用 vitest 5**,运行方式是 `deno run -A npm:vitest@5.0.2 run`,用例不用改。成员有精简 package.json 之后,`@umt/*` 走 node_modules 链接就能解析,不需要 alias。 +- 暂不切 `deno test`。如果以后要切,最小改动是:import 换成 `@std/testing/bdd` 的 describe/it/beforeAll/afterAll 加 `@std/expect` 的 expect;`vi.fn/vi.spyOn` 换成 `@std/testing/mock` 的 `spy/stub`;`describe.skipIf(c)` 换成 `describe({ name, ignore: c, fn })`;同时要接受资源泄漏检查(sanitizer)。 + +### 6. Dockerfile(单容器,spike 已构建并验证) + +```dockerfile +FROM denoland/deno:2.9.7 AS build +WORKDIR /app +COPY . . +RUN deno install --frozen +RUN deno task check:tokens && cd apps/web && deno task build +RUN cd apps/server && deno task compile # → /app/dist/umt-server + +FROM debian:trixie-slim +RUN useradd -r -u 1000 umt +WORKDIR /app +COPY --from=build /app/dist/umt-server /app/umt-server +COPY --from=build /app/apps/web/build/client /app/public +ENV WEB_ROOT=/app/public HOST=0.0.0.0 PORT=4380 +USER umt +EXPOSE 4380 +ENTRYPOINT ["/app/umt-server"] +CMD ["--role", "all"] +``` + +- 不要用 `deno run` + `nodeModulesDir: auto` 当运行时入口。spike 实测运行时会尝试重建 node_modules 链接,报 EEXIST,镜像还有 813 MB。如果非要用 `deno run`,必须加 `--node-modules-dir=manual --cached-only`。 +- 迁移作为同一个二进制的子命令(`--role migrate`),或者单独 compile 一个 `umt-migrate`,在部署时先执行。 + +### 7. CI + +```bash +curl -fsSL https://deno.land/install.sh | sh -s v2.9.7 # 或 denoland/setup-deno,deno-version: 2.9.7 +deno install --frozen +deno task ci # typecheck(tsc 7.0.2 + deno check)→ biome → vitest → tokens → web build + compile +``` + +需要 PG 的测试(tasks/db)在 CI 里单独起一个一次性 PG 服务,通过 `TASKS_TEST_DATABASE_URL` 注入。spike 显示,和开发者共用的 55433 库会互相干扰。 + +## 预估改动面 + +- 新增 9 个 `deno.json`,改 8 个 `package.json`,新增根 `deno.json`,删除 pnpm 相关的 3 个文件和 `build.mjs`。 +- 约 400 行相对 import 补扩展名,可自动修复。server `main.ts` 约 30 行。2 个崩溃恢复测试的 spawn 参数。db scripts 3 个。logger 约 15 行。web 的 tsconfig 和 package.json 各 1 到 2 行。 +- 文档:README、plan §1、ADR-001 标记 superseded。 +- 业务逻辑(domain/contracts/adapters/agent/tasks 的实现)**不用改**。 diff --git a/docs/spikes/deno-2.9.7.md b/docs/spikes/deno-2.9.7.md new file mode 100644 index 00000000..d3291856 --- /dev/null +++ b/docs/spikes/deno-2.9.7.md @@ -0,0 +1,196 @@ +# Deno 2.9.7 + TypeScript 7.0.2 切换准入验证记录 + +- 日期:2026-09-27;宿主:Linux x86_64,Docker 可用;Node v24.15.0 仍在机器上,用来做对照。 +- 代码:`spikes/deno/`,是独立的 Deno workspace,不属于 pnpm workspace。原始输出在 `spikes/deno/logs/*.txt`。 +- PG:`docker run -d --name umt-deno-pg -p 55435:5432 -e POSTGRES_PASSWORD=spike postgres:16-alpine`,验证结束后已删除。 +- 结论见 `docs/adr/ADR-004-deno-runtime.md`。 + +## 0. 安装与版本 + +```text +$ curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s v2.9.7 +Deno was installed successfully to /usr/local/bin/deno +$ deno --version +deno 2.9.7 (stable, release, x86_64-unknown-linux-gnu) +v8 15.0.245.2-rusty +typescript 6.0.3 +``` + +**注意:Deno 2.9.7 内置的 TypeScript 是 6.0.3,不是 7。** `deno check`、`deno test`、`deno compile` 做类型检查时都用 6.0.3。TS 7.0.2 只能从 npm 装(`npm:typescript@7.0.2`),见 §g。 + +## 1. 结论表 + +| 项 | 结论 | 要点 | +|---|---|---| +| workspace + `nodeModulesDir: "auto"` + `npm:` imports + `deno install` | 通过 | 12 秒装完;生成的 `deno.lock` 是 v5 格式。有安装脚本的包(protobufjs、@google/genai、msw)默认被跳过,和 pnpm `allowBuilds` 的效果一样 | +| a. openworkflow 0.10.1 + postgres.js | 通过 | 迁移建表、step.run/sleep/waitForSignal、幂等键、kill -9 后被另一个 worker 接管、双 worker 40 个 run 无重复,语义和 Node 下一致 | +| b. pi-agent-core / pi-ai 0.87.1 | 通过 | fake provider 事件流、白名单(`bash` 被拒)、abort、SIGKILL 后 JSONL 恢复("结果未知",不重复提交)都跑通。`NodeExecutionEnv` 在 Deno 下可以用 | +| c1. Hono 4 + `Deno.serve` + SSE | 通过 | SSE 头和分帧正确;SIGTERM 触发 `AbortController` 后,即使有 SSE 长连接挂着,服务也在约 0.2 秒内退出 | +| c2. Kysely 0.29 + pg 8 / postgres.js | 两种都通过,**推荐 pg** | 项目真实的 12 个迁移在 Deno 下全部成功。postgres.js 方言对 `JSON.stringify` 过的 jsonb 会双重编码,所以业务库继续用 pg;OpenWorkflow 自己带 postgres.js | +| d. pino 10 | 通过 | 自定义 destination、formatters/hooks 脱敏、`pino.transport`(worker_threads)、pino-pretty 都能用,**不用换**。LogTape 作为备选也验证通过 | +| e. vitest 5 | 通过(需两处调整) | `deno run -A npm:vitest run` 能跑 node:http、child_process kill、fs 临时目录、statfs。真实仓库各包:server 15/15,contracts、db、domain、testkit 和 Node 一样;tasks 用干净 PG 跑 15/15;agent 有 1 例失败,原因是 import 没写扩展名 | +| e'. `deno test` + `@std/expect` | 通过 | 只改 2 行 import 就能跑同一套用例 | +| f. 前端 Vite 7 + React Router 7 | 通过 | `deno task build`(执行 package.json 里的 script)能构建,`vite dev` / `react-router dev` 在 5181 端口 curl 200;token 脚本 `deno run -A` 能生成也能检查。全程没有调用 node(用假 node 放在 PATH 最前面验证过) | +| g. TypeScript 7.0.2 | 通过(需变通) | 可执行入口是 `npm:typescript@7.0.2/tsc`,**包里没有 `tsgo`**。真实仓库 8 个 tsconfig 在 TS7 下都是 0 错误;web 要删掉 `baseUrl`(TS7 已移除这个选项)。tsc 解析不了 `jsr:`,Deno 全局类型要靠 `deno types` 提供 | +| h. `deno compile` / 镜像 | 通过(需变通) | 默认打包会把整个 node_modules 塞进去,二进制 440 MB;加 `--node-modules-dir=none --exclude-unused-npm` 后是 107 MB。`denoland/deno:2.9.7` 有 amd64 和 arm64 两个平台。单容器镜像 260 MB,API、SSE、SPA 回退和 SIGTERM 退出都验证过 | + +## 2. 分项记录 + +### workspace 结构 + +```text +spikes/deno/ + deno.json workspace: packages/x, packages/contracts, apps/server, web;nodeModulesDir: auto;imports 用 npm:/jsr: + packages/x/ deno.json 成员(name/exports),模拟 packages/* + packages/contracts/ 仓库 contracts 的快照;同时有 deno.json 和精简的 package.json + apps/server/ deno.json 成员:ow/、pi/、http.ts、db.ts、log-*.ts、node-compat.ts、test/ + web/ apps/web 的只读复制;package.json 成员 + tsconfig.tsc.json 只给 TS 7 用(不能叫 tsconfig.json,见 §g) + Dockerfile 单容器验证 +``` + +**成员被 Vite/vitest/tsc 解析的前提:** `package.json` 成员会被 `deno install` 软链到 `node_modules/@umt/*`,所以 Vite、vitest、tsc 这些走 node 解析的工具都能找到它。只有 `deno.json` 的成员(比如 `@spike/x`)**不会**被链接,这些工具找不到。已验证同一个目录可以同时放 `deno.json` 和 `package.json`:Deno 的 name/exports 以 deno.json 为准,链接照样生成。 + +### a. OpenWorkflow + +```text +$ deno run -A src/ow/cli.ts tables +biz.side_effect_ledger / openworkflow.openworkflow_migrations / step_attempts / workflow_runs / workflow_signals +# 正常路径:signal 发出后 run 完成,步骤依次是 submit→signal-wait→copy→register→sleep→cleanup;用同一个 idempotencyKey 再投递,返回的是同一个 run +# kill -9:copy-file(6s)执行中 kill -9 worker A,启动 worker B,约 36 秒后完成(租约过期接管) +1fdbe2ec copy-file A start +1fdbe2ec copy-file B start / end → register B → cleanup B (attempts=4) +# 双 worker:40 个 burst run +{"total":"40","runs":"40","a":"24","b":"16"} +# SIGTERM:Deno.addSignalListener → worker.stop() → "[A] worker stopped gracefully" +``` + +改动只有三处:`process.on("SIGTERM")` 换成 `Deno.addSignalListener`,`node:fs` 写 pidfile 换成 `Deno.writeTextFileSync`,其余文件在顶部补 `import process from "node:process"`。其实不改也能跑,因为 Deno 2 对 npm/Node 代码提供了全局 `process`,`process.on` 也验证可用,见 §h。 + +### b. Pi + +```text +$ deno run -A src/pi/fake-run.ts +[tool_end] inventory__query isError=false → {"inLibrary":false} +[tool_end] download__submit isError=false → {"accepted":true,"idempotencyKey":"dl:Dune:1080p"} +[tool_end] bash isError=true -> "Tool \"bash\" is unavailable" +[run_end] status=completed fake calls: 3 +$ SUBMIT_DELAY_MS=2000 deno run -A src/pi/abort-resume.ts crash # 进程自己 SIGKILL +$ deno run -A src/pi/abort-resume.ts resume +open operations at attach: [{"lane":"main","kind":"run",...}] +[tool_end] (recovery) download__submit "[Tool execution was interrupted ... external outcome is unknown.]" +SUBMITTED in this process [] ← 没有重复提交 +``` + +### c. Hono + Deno.serve + SSE,Kysely + +- `Deno.serve({ port, signal })` 直接接 `app.fetch`;`streamSSE` 输出 `event/data/id`。SIGTERM 时 abort signal,`server.finished` 会 resolve。 +- 挂着 SSE 长连接时关服:不管应用层有没有主动关 SSE,进程都在约 0.21 秒内退出(`logs/hono-shutdown.txt`)。Node 那边原来需要 `closeAllConnections()`,这里不需要。 +- 静态托管:用 `hono/deno` 的 `serveStatic({ root })`,SPA 回退要写成 `serveStatic({ root, path: "index.html" })`。**写绝对路径 `path: "/app/public/index.html"` 会 404**。 +- Kysely 0.29 的迁移 API 要从 `kysely/migration` 导入,import map 里需要加 `"kysely/": "npm:/kysely@^0.29.6/"`。 + +```text +pg migrate: ["0001_init:Success","0002_index:Success"] rows: string:1/Dune/{"q":"1080p"}/true 50 并发/池 4: 158ms +postgresjs migrate: 同上 rows: string:1/Dune/"{\"q\":\"1080p\"}"/true ← jsonb 被双重编码 170ms +# 真实迁移(只读引用 packages/db/migrations): +pg migrate: ["0001_init:Success", … ,"0012_organization:Success"] error: null +``` + +### d. 日志 + +```text +{"level":30,...,"token":"[REDACTED]","url":"https://x/rss?passkey=[REDACTED]&a=1","msg":"fetch https://x?api_key=[REDACTED]"} +{"level":40,...,"component":"worker","password":"p","err":{},"msg":"child"} ← 见下方"顺带发现" +{"level":30,...,"secret":"[REDACTED]","msg":"via pino.transport (worker_threads)"} +[18:04:42] INFO (987284): via pino-pretty transport +``` + +**顺带发现(和运行时无关,Node 下复现结果相同):** 现有 `apps/server/src/logger.ts` 的脱敏有两个漏洞:① `logger.child({ password })` 的 bindings 不经过 `formatters.log`,需要再加一个 `formatters.bindings`;② `redact()` 把 `Error` 当普通对象处理,结果 `err` 变成 `{}`,应该先按 `err.message/stack` 序列化,再做脱敏。 + +LogTape(npm 版 `@logtape/logtape`)写一个 redacting sink 包装约 5 行,输出 JSON Lines,也可用。**结论:保留 pino。** + +### e. 测试 + +```text +$ deno run -A npm:vitest@5.0.2 run # spike 自带用例 +FAIL: Cannot find package '@spike/x' ← deno.json 成员不在 node_modules 里,Vite 解析不到 +# vitest.config.ts 加上 resolve.alias 之后,连跑 3 次都是 5/5 通过(约 0.7s) + +$ cd apps/server && deno run -A --no-lock npm:vitest run # 真实仓库,只读 +Test Files 4 passed (4) Tests 15 passed (15) +``` + +真实各包在 Deno 和 Node 下的对照(`logs/vitest-real-packages.txt`): + +| 包 | Deno | Node | 说明 | +|---|---|---|---| +| contracts / db / domain / testkit | 361 / 11 / 122 / 1 全过 | 同 | | +| adapters | 108 过 1 败 | 108 过 1 败 | 两边一样,是在改代码导致的,和运行时无关 | +| tasks | 在共享的开发 PG 上不稳 | 在共享的开发 PG 上也不稳 | 换成干净的 PG(55435)后 Deno 下 **15/15**,包括子进程 kill -9 的恢复用例 | +| agent | 26 过 1 败 | 27 全过 | 子进程用 `process.execPath` 启动,在 Deno 下这个路径就是 deno,原生加载 `contracts/src/index.ts` 时碰到 `export * from "./agent"` 这种不带扩展名的 import | + +`deno test` 的写法(`logs/deno-test.txt`):只需把 + +```ts +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +``` + +换成 + +```ts +import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +``` + +`ok | 1 passed (6 steps)`。资源泄漏检查(sanitizer)也没有报错。要注意的差异:没有 `vi.fn/vi.mock`(改用 `@std/testing/mock` 的 `spy/stub`);没有 `describe.skipIf`(改成 `it({ ignore: cond })`);`toMatchObject`、`toEqual` 等常用 matcher 在 `@std/expect` 里都有。 + +### f. 前端 + +```text +$ deno run -A npm:react-router build +error: ... react-router/package.json did not have a bin property ← CLI 在 @react-router/dev 包里 +$ deno run -A npm:@react-router/dev@7.12.0/react-router build → ✓ built in 690ms,SPA Mode: Generated build/client/index.html +$ deno task build (执行 package.json 的 "react-router build";PATH 最前面放了会 exit 99 的假 node,构建仍然成功) +$ deno run -A npm:vite@7 dev --port 5181 --strictPort +GET / -> 200 228202B UHD 媒体自动化控制台;GET /app/root.tsx -> 200 text/javascript +$ deno run -A scripts/gen-design-tokens.mjs [--check] → 生成 / "登记表是最新的",exit 0;check-design-tokens.sh 通过 +``` + +`deno task` 运行 `node_modules/.bin/*` 时用的是 Deno,不会去调系统的 node。 + +### g. TypeScript 7.0.2 + +- 包结构:`typescript@7.0.2` 的 `bin` 只有 `{ tsc }`,`lib/tsc.js` 只是个壳,真正执行的是平台包 `@typescript/typescript-linux-x64/lib/tsc`(Go 原生二进制)。Deno 下没有 `process.execve`,会走 `execFileSync` 分支,exit code 能正确透传。 +- 命令:`deno run -A npm:typescript@7.0.2/tsc --noEmit -p tsconfig.tsc.json`。 +- 结果对照(`logs/typecheck-compare.txt`、`logs/tsc7-real-repo.txt`、`logs/tsc7-web.txt`): + +| | tsc 7.0.2 | deno check(TS 6.0.3) | +|---|---|---| +| spike 代码 | 0 错误,1.5s(约 12s CPU,多核并行) | 0 错误,2.6s | +| 同一批严格错误(noUncheckedIndexedAccess 等) | 报出,结果一致 | 报出,结果一致 | +| `jsr:` 包(如 `@std/expect`) | **解析不了**(TS2307) | 能解析 | +| `Deno.*` 全局 | 需要 `deno types > types/deno.d.ts` 加进 include | 内置 | +| 真实仓库 7 个包 + server(现有 tsconfig) | **全部 0 错误** | 不带扩展名的 import 未解决前:contracts 16、domain 7、db 3、tasks 9、agent 11、adapters 6、server 8 个解析错误;加 `--sloppy-imports` 后全部 0 错误 | +| web(React Router typegen 之后) | 删掉 `baseUrl` 后 0 错误(TS5102: Option 'baseUrl' has been removed) | 不适用 | + +- **坑:** 只要 workspace 根目录有 `tsconfig.json`,`deno check` 就会读它的 `compilerOptions.lib`。如果里面写的是 `["ES2023","DOM"]`,Deno 会报找不到 `Deno` 名字,也不认识 `import.meta.main`。所以给 TS7 用的配置要换个文件名(`tsconfig.tsc.json`),或者写到各包目录里,不要放在根目录叫 `tsconfig.json`。 +- 不带扩展名的 import 可以自动修:`deno lint --sloppy-imports --fix --rules-include=no-sloppy-imports `,验证过能改成 `./a.ts` 和 `./dir/index.ts`(`logs/sloppy-fix.txt`)。仓库 src+test 里大约有 400 行这样的 import。 + +### h. 容器与 Node 专有 API + +```text +deno compile -A worker.ts → 440 MB(把整个 node_modules 443M 都嵌进去了,包括前端依赖和 TS 原生二进制) +deno compile --bundle → 141 MB +deno compile --node-modules-dir=none --exclude-unused-npm → 107 MB 三种都能正常消费 OW 任务并优雅停止 +$ docker manifest inspect denoland/deno:2.9.7 → OCI index,linux/amd64 + linux/arm64(Debian 13 trixie,默认 root 用户) +$ docker build(spikes/deno/Dockerfile:deno install → web build → deno compile → debian:trixie-slim) +image size 260MB;health 200;/ 200 text/html;/settings(SPA 回退)200;assets js 200;SSE 3 个事件;docker stop(SIGTERM)0.31s +``` + +第一版 Dockerfile 用的是 `deno run --cached-only` 加 `USER deno`,结果失败:`File exists (os error 17), symlink '../../packages/contracts' -> '/app/node_modules/@umt/contracts'`。原因是 `nodeModulesDir: auto` 在运行时还会尝试重建 node_modules 链接,而且镜像有 813 MB。**生产镜像应改用 compile 出的二进制;如果坚持用 `deno run`,就加 `--node-modules-dir=manual`,并在构建阶段用 root 装好依赖。** + +Node API 探测(`src/node-compat.ts` → `logs/node-compat.txt`):`node:util parseArgs`、`fs.statfs`、`os.*`、`process.memoryUsage`、`AsyncLocalStorage`、`node:crypto`、`worker_threads`(`new Worker(url, {type:"module"})`)、`child_process.spawn` 加 SIGTERM、`process.on("SIGTERM")` 全部正常;`process.execPath` 是 `/usr/local/bin/deno`。 + +## 3. 清理 + +`umt-deno-pg`、`umt-deno-app` 两个容器和 `umt-deno-spike` 镜像已删除;compile 产物、`web/build`、会话目录已删除;5181、5199、58080、55435 端口都没有监听进程。`spikes/deno/node_modules`(443 MB)被 .gitignore 忽略,需要时执行 `deno install` 重建。 diff --git a/spikes/deno/.dockerignore b/spikes/deno/.dockerignore new file mode 100644 index 00000000..23411835 --- /dev/null +++ b/spikes/deno/.dockerignore @@ -0,0 +1,6 @@ +**/node_modules +dist +logs +web/build +web/.react-router +apps/server/src/pi/.sessions diff --git a/spikes/deno/.gitignore b/spikes/deno/.gitignore new file mode 100644 index 00000000..48dd88ad --- /dev/null +++ b/spikes/deno/.gitignore @@ -0,0 +1,2 @@ +types/deno.d.ts +apps/server/src/pi/.sessions/ diff --git a/spikes/deno/Dockerfile b/spikes/deno/Dockerfile new file mode 100644 index 00000000..ec1c546b --- /dev/null +++ b/spikes/deno/Dockerfile @@ -0,0 +1,20 @@ +# Single container: one `deno compile` binary runs API (+ worker role) and serves the prebuilt SPA. +FROM denoland/deno:2.9.7 AS build +WORKDIR /app +COPY . . +RUN deno install --frozen +RUN cd web && deno task build +# --node-modules-dir=none + --exclude-unused-npm: embed only npm packages reachable from the server graph +RUN deno compile --frozen --node-modules-dir=none --exclude-unused-npm \ + --allow-net --allow-env --allow-sys --allow-read=/app \ + --output /out/umt-server apps/server/src/http.ts + +FROM debian:trixie-slim +RUN useradd -r -u 1000 umt +WORKDIR /app +COPY --from=build /out/umt-server /app/umt-server +COPY --from=build /app/web/build/client /app/public +ENV WEB_ROOT=/app/public HOST=0.0.0.0 PORT=8080 +USER umt +EXPOSE 8080 +ENTRYPOINT ["/app/umt-server"] diff --git a/spikes/deno/apps/server/deno.json b/spikes/deno/apps/server/deno.json new file mode 100644 index 00000000..09af2876 --- /dev/null +++ b/spikes/deno/apps/server/deno.json @@ -0,0 +1,9 @@ +{ + "name": "@spike/server", + "version": "0.0.0", + "exports": { ".": "./src/main.ts" }, + "tasks": { + "dev": "deno run -A --watch src/main.ts", + "test": "deno test -A test/node_apis_test.ts" + } +} diff --git a/spikes/deno/apps/server/src/db.ts b/spikes/deno/apps/server/src/db.ts new file mode 100644 index 00000000..96f2dc38 --- /dev/null +++ b/spikes/deno/apps/server/src/db.ts @@ -0,0 +1,50 @@ +// Kysely 0.29 on Deno: DIALECT=pg (node-postgres Pool) or DIALECT=postgresjs (kysely-postgres-js). +import { Kysely, PostgresDialect, sql } from "kysely"; +import { FileMigrationProvider, Migrator } from "kysely/migration"; +import { PostgresJSDialect } from "kysely-postgres-js"; +import pg from "pg"; +import postgres from "postgres"; +import { promises as fs } from "node:fs"; +import * as path from "node:path"; + +const url = Deno.env.get("PG_URL") ?? "postgresql://postgres:spike@127.0.0.1:55435/postgres"; +const dialectName = Deno.env.get("DIALECT") ?? "pg"; +const schema = Deno.env.get("SCHEMA") ?? `kysely_${dialectName}`; + +export function createDb(): Kysely { + const dialect = dialectName === "postgresjs" + ? new PostgresJSDialect({ postgres: postgres(url, { max: 4, onnotice: () => {} }) }) + : new PostgresDialect({ pool: new pg.Pool({ connectionString: url, max: 4 }) }); + return new Kysely({ dialect }); +} + +if (import.meta.main) { + const root = createDb(); + await sql`DROP SCHEMA IF EXISTS ${sql.id(schema)} CASCADE`.execute(root); + await sql`CREATE SCHEMA ${sql.id(schema)}`.execute(root); + const db = root.withSchema(schema); + const migrator = new Migrator({ + db: root, + migrationTableSchema: schema, + provider: new FileMigrationProvider({ fs, path, migrationFolder: Deno.env.get("MIGRATIONS_DIR") ?? new URL("./migrations", import.meta.url).pathname }), + }); + // Migrations use unqualified names: run them with search_path pointing at the dialect's schema. + await sql`SET search_path TO ${sql.id(schema)}`.execute(root); + const r = await migrator.migrateToLatest(); + console.log(dialectName, "migrate:", JSON.stringify(r.results?.map((x) => `${x.migrationName}:${x.status}`)), "error:", r.error ?? null); + if (Deno.env.get("MIGRATIONS_DIR")) { await root.destroy(); Deno.exit(r.error ? 1 : 0); } + await db.insertInto("items").values([{ title: "Dune", meta: JSON.stringify({ q: "1080p" }) }, { title: "Arrival", meta: null }]).execute(); + const rows = await db.selectFrom("items").select(["id", "title", "meta", "created_at"]).orderBy("id").execute(); + console.log(dialectName, "rows:", rows.map((r) => `${typeof r.id}:${r.id}/${r.title}/${JSON.stringify(r.meta)}/${r.created_at instanceof Date}`).join(" ")); + // transaction + rollback + await db.transaction().execute(async (tx) => { await tx.insertInto("items").values({ title: "tx-ok" }).execute(); }).catch(() => {}); + await db.transaction().execute(async (tx) => { await tx.insertInto("items").values({ title: "tx-rollback" }).execute(); throw new Error("rollback"); }).catch((e) => console.log(dialectName, "tx error ->", e.message)); + const titles = (await db.selectFrom("items").select("title").execute()).map((r) => r.title); + console.log(dialectName, "titles after tx:", titles.join(",")); + // concurrency: 50 parallel queries on pool max=4 + const t0 = performance.now(); + await Promise.all(Array.from({ length: 50 }, (_, i) => sql`SELECT pg_sleep(0.01), ${i}::int AS i`.execute(root))); + console.log(dialectName, `50 parallel queries on pool=4: ${(performance.now() - t0).toFixed(0)}ms`); + await root.destroy(); + console.log(dialectName, "destroyed cleanly"); +} diff --git a/spikes/deno/apps/server/src/http.ts b/spikes/deno/apps/server/src/http.ts new file mode 100644 index 00000000..54424a68 --- /dev/null +++ b/spikes/deno/apps/server/src/http.ts @@ -0,0 +1,52 @@ +// Hono 4 on Deno.serve with an SSE stream + graceful shutdown. +import { Hono } from "hono"; +import { streamSSE } from "hono/streaming"; +import { serveStatic } from "hono/deno"; +import { redact } from "@spike/x"; + +export function createApp() { + const app = new Hono(); + app.get("/api/v1/health", (c) => c.json({ ok: true, runtime: `deno ${Deno.version.deno}`, ts: Deno.version.typescript })); + app.get("/api/v1/redact", (c) => c.json(redact({ url: c.req.url, apiKey: "x" }) as Record)); + app.get("/api/v1/events", (c) => + streamSSE(c, async (stream) => { + const after = Number(c.req.query("after") ?? 0); + let aborted = false; + stream.onAbort(() => { aborted = true; }); + for (let i = after + 1; i <= after + 3 && !aborted; i++) { + await stream.writeSSE({ id: String(i), event: "tick", data: JSON.stringify({ i }) }); + await stream.sleep(100); + } + })); + // Endless stream: used to check how Deno.serve shutdown treats open SSE connections. + app.get("/api/v1/forever", (c) => + streamSSE(c, async (stream) => { + let i = 0; + while (!stream.aborted && !shutdownSignal.aborted) { + await stream.writeSSE({ id: String(++i), data: "ping" }); + await stream.sleep(200); + } + })); + // Single-container mode: serve the built SPA (react-router ssr:false) from WEB_ROOT with index.html fallback. + const webRoot = Deno.env.get("WEB_ROOT"); + if (webRoot) { + app.use("/*", serveStatic({ root: webRoot })); + app.get("*", serveStatic({ root: webRoot, path: "index.html" })); + } + return app; +} + +/** App-level shutdown signal: SSE loops must observe it, otherwise graceful shutdown waits forever. */ +export const shutdownCtl = new AbortController(); +const shutdownSignal = shutdownCtl.signal; + +if (import.meta.main) { + const port = Number(Deno.env.get("PORT") ?? 5199); + const ac = new AbortController(); + const server = Deno.serve({ port, hostname: Deno.env.get("HOST") ?? "127.0.0.1", signal: ac.signal, onListen: ({ port }) => console.log(`listening ${port}`) }, createApp().fetch); + const stop = () => { console.log("shutdown"); if (Deno.env.get("CLOSE_SSE") !== "0") shutdownCtl.abort(); ac.abort(); }; + Deno.addSignalListener("SIGTERM", stop); + Deno.addSignalListener("SIGINT", stop); + await server.finished; + console.log("server finished"); +} diff --git a/spikes/deno/apps/server/src/log-logtape.ts b/spikes/deno/apps/server/src/log-logtape.ts new file mode 100644 index 00000000..732dcfdd --- /dev/null +++ b/spikes/deno/apps/server/src/log-logtape.ts @@ -0,0 +1,12 @@ +// LogTape alternative: JSON lines + redaction sink wrapper. +import { configure, getJsonLinesFormatter, getLogger, getStreamSink, type LogRecord, type Sink } from "@logtape/logtape"; +import { redact, redactString } from "@spike/x"; + +const base = getStreamSink(Deno.stdout.writable, { formatter: getJsonLinesFormatter() }); +const redacting = (inner: Sink): Sink => (r: LogRecord) => + inner({ ...r, message: r.message.map((m: unknown) => (typeof m === "string" ? redactString(m) : redact(m))), rawMessage: typeof r.rawMessage === "string" ? redactString(r.rawMessage) : r.rawMessage, properties: redact(r.properties) as Record }); +await configure({ sinks: { out: redacting(base) }, loggers: [{ category: ["umt"], lowestLevel: "info", sinks: ["out"] }, { category: ["logtape", "meta"], lowestLevel: "warning", sinks: ["out"] }] }); +const log = getLogger(["umt", "worker"]); +log.info("fetch {url}", { url: "https://x/rss?passkey=SECRET", token: "abc", n: 3 }); +log.with({ runId: "r1", apiKey: "k" }).warn("child context"); +await new Promise((r) => setTimeout(r, 100)); diff --git a/spikes/deno/apps/server/src/log-pino.ts b/spikes/deno/apps/server/src/log-pino.ts new file mode 100644 index 00000000..6912a57b --- /dev/null +++ b/spikes/deno/apps/server/src/log-pino.ts @@ -0,0 +1,32 @@ +// pino 10 on Deno: sync stdout, custom destination, formatters/hooks redaction, child loggers, pino.transport (worker_threads). +import pino from "pino"; +import { redact, redactString } from "@spike/x"; +import { Writable } from "node:stream"; + +const opts: pino.LoggerOptions = { + level: "info", + formatters: { log: (obj) => redact(obj) as Record }, + hooks: { + logMethod(args, method) { + return method.apply(this, args.map((a) => (typeof a === "string" ? redactString(a) : a)) as typeof args); + }, + }, +}; +const lines: string[] = []; +const dest = new Writable({ write(chunk, _enc, cb) { lines.push(String(chunk)); cb(); } }); +const log = pino(opts, dest); +log.info({ token: "abc", url: "https://x/rss?passkey=SECRET&a=1", n: 1 }, "fetch https://x?api_key=K"); +log.child({ component: "worker", password: "p" }).warn({ err: new Error("boom token=zzz") }, "child"); +console.log("custom-dest:", lines.join("").trim()); + +pino(opts).info({ cookie: "c=1" }, "stdout default destination"); +const t = pino.transport({ target: "pino/file", options: { destination: 1 } }); +const lt = pino(opts, t); +lt.info({ secret: "s" }, "via pino.transport (worker_threads)"); +await new Promise((r) => setTimeout(r, 300)); +t.end(); +if (Deno.env.get("PRETTY")) { + const pp = pino({ ...opts, transport: { target: "pino-pretty", options: { translateTime: "SYS:HH:MM:ss" } } }); + pp.info({ token: "x" }, "via pino-pretty transport"); + await new Promise((r) => setTimeout(r, 500)); +} diff --git a/spikes/deno/apps/server/src/migrations/0001_init.ts b/spikes/deno/apps/server/src/migrations/0001_init.ts new file mode 100644 index 00000000..443ab9b7 --- /dev/null +++ b/spikes/deno/apps/server/src/migrations/0001_init.ts @@ -0,0 +1,10 @@ +import { type Kysely, sql } from "kysely"; +export async function up(db: Kysely) { + await db.schema.createTable("items").ifNotExists() + .addColumn("id", "bigserial", (c) => c.primaryKey()) + .addColumn("title", "text", (c) => c.notNull()) + .addColumn("meta", "jsonb") + .addColumn("created_at", "timestamptz", (c) => c.notNull().defaultTo(sql`now()`)) + .execute(); +} +export async function down(db: Kysely) { await db.schema.dropTable("items").execute(); } diff --git a/spikes/deno/apps/server/src/migrations/0002_index.ts b/spikes/deno/apps/server/src/migrations/0002_index.ts new file mode 100644 index 00000000..7fdb8a22 --- /dev/null +++ b/spikes/deno/apps/server/src/migrations/0002_index.ts @@ -0,0 +1,5 @@ +import type { Kysely } from "kysely"; +export async function up(db: Kysely) { + await db.schema.createIndex("items_title_idx").ifNotExists().on("items").column("title").execute(); +} +export async function down(db: Kysely) { await db.schema.dropIndex("items_title_idx").execute(); } diff --git a/spikes/deno/apps/server/src/node-compat.ts b/spikes/deno/apps/server/src/node-compat.ts new file mode 100644 index 00000000..7ce4f491 --- /dev/null +++ b/spikes/deno/apps/server/src/node-compat.ts @@ -0,0 +1,32 @@ +// Node-only API probe under Deno (things the current server/tasks code uses or is likely to use). +import process from "node:process"; +import { Worker, isMainThread, parentPort } from "node:worker_threads"; +import { statfs } from "node:fs/promises"; +import { spawn } from "node:child_process"; +import { parseArgs } from "node:util"; +import os from "node:os"; +import { AsyncLocalStorage } from "node:async_hooks"; +import { createHash, randomUUID } from "node:crypto"; + +if (!isMainThread) { parentPort!.postMessage({ echo: "from worker", pid: process.pid }); } else { + const r: Record = {}; + r.parseArgs = parseArgs({ args: ["--role", "api"], options: { role: { type: "string" } } }).values; + const s = await statfs("/"); + r.statfs = { bsize: s.bsize, bavailGB: Math.round((s.bavail * s.bsize) / 1e9) }; + r.os = { cpus: os.cpus().length, freememMB: Math.round(os.freemem() / 1e6), loadavg: os.loadavg().length }; + r.memoryUsage = Object.keys(process.memoryUsage()).join(","); + const als = new AsyncLocalStorage(); + r.als = await als.run("req-1", async () => { await new Promise((x) => setTimeout(x, 5)); return als.getStore(); }); + r.crypto = createHash("sha256").update("x").digest("hex").slice(0, 8) + " " + randomUUID().length; + r.worker = await new Promise((res) => { const w = new Worker(new URL(import.meta.url), { type: "module" } as any); w.once("message", (m) => { res(m); w.terminate(); }); }); + const child = spawn("sh", ["-c", "trap 'echo got-TERM; exit 0' TERM; echo ready; sleep 5 & wait"]); + let out = ""; child.stdout.on("data", (d) => { out += d; }); + await new Promise((x) => setTimeout(x, 200)); + child.kill("SIGTERM"); + r.childKill = await new Promise((res) => child.on("exit", (code, sig) => res({ code, sig, out: out.trim().replace(/\n/g, "|") }))); + r.execPath = process.execPath; + console.log(JSON.stringify(r, null, 1)); + process.on("SIGTERM", () => { console.log("process.on(SIGTERM) handler fired"); process.exit(0); }); + setTimeout(() => process.kill(process.pid, "SIGTERM"), 100); + setTimeout(() => { console.log("handler did NOT fire"); process.exit(1); }, 2000); +} diff --git a/spikes/deno/apps/server/src/ow/cli.ts b/spikes/deno/apps/server/src/ow/cli.ts new file mode 100644 index 00000000..1d07796f --- /dev/null +++ b/spikes/deno/apps/server/src/ow/cli.ts @@ -0,0 +1,65 @@ +import process from "node:process"; +import { makeClient, initBiz, db } from "./common.ts"; +import { sql } from "kysely"; +await initBiz(); +const { ow, backend, acquire, flaky, slow, burst } = await makeClient(); +const [cmd, ...args] = process.argv.slice(2) as [string, ...string[]]; +const out = (x: unknown) => console.log(JSON.stringify(x, null, 0)); +switch (cmd) { + case "start-acquire": { + const [downloadId, key, copyMs, retention] = args; + const h = await acquire.run({ downloadId, copyMs: Number(copyMs ?? 0), retention: retention ?? "3s" }, + key ? { idempotencyKey: key } : undefined); + out({ runId: h.workflowRun.id, status: h.workflowRun.status, version: h.workflowRun.version }); + break; + } + case "start-flaky": { + const h = await flaky.run({ failTimes: Number(args[0]), maxAttempts: Number(args[1]) }); + out({ runId: h.workflowRun.id }); + break; + } + case "start-slow": { const h = await slow.run({}); out({ runId: h.workflowRun.id }); break; } + case "start-burst": { + const ids = []; + for (let i = 0; i < Number(args[0]); i++) ids.push((await burst.run({ i })).workflowRun.id); + out({ count: ids.length }); + break; + } + case "signal": { + const r = await ow.sendSignal({ signal: `download-done:${args[0]}`, data: { ok: true }, idempotencyKey: args[1] }); + out(r); + break; + } + case "cancel": { await ow.cancelWorkflowRun(args[0]!); out({ canceled: args[0] }); break; } + case "get": { + const r = await backend.getWorkflowRun({ workflowRunId: args[0]! }); + out({ status: r?.status, version: r?.version, attempts: r?.attempts, error: r?.error, output: r?.output }); + break; + } + case "steps": { + const r = await backend.listStepAttempts({ workflowRunId: args[0], limit: 100 } as any); + for (const s of [...r.data].sort((a,b)=>+a.createdAt-+b.createdAt)) console.log(s.stepName.padEnd(18), s.kind.padEnd(12), s.status, s.error ? JSON.stringify((s.error as any).message) : ""); + break; + } + case "ledger": { + const rows = await db.selectFrom("biz.side_effect_ledger").selectAll() + .$if(!!args[0], (q) => q.where("run_id", "=", args[0])).orderBy("id").execute(); + for (const r of rows) console.log(r.run_id.slice(0, 8), r.step.padEnd(16), r.worker.padEnd(6), r.note); + break; + } + case "burst-dups": { + const rows = await sql`SELECT count(*) total, count(DISTINCT run_id) runs, + count(*) FILTER (WHERE worker='A') a, count(*) FILTER (WHERE worker='B') b + FROM biz.side_effect_ledger WHERE step='work'`.execute(db); + out(rows.rows[0]); + break; + } + case "tables": { + const r = await sql`SELECT table_schema, table_name FROM information_schema.tables + WHERE table_schema IN ('openworkflow','biz') ORDER BY 1,2`.execute(db); + for (const t of r.rows) console.log(`${t.table_schema}.${t.table_name}`); + break; + } +} +await backend.stop(); +await db.destroy(); diff --git a/spikes/deno/apps/server/src/ow/common.ts b/spikes/deno/apps/server/src/ow/common.ts new file mode 100644 index 00000000..2b166441 --- /dev/null +++ b/spikes/deno/apps/server/src/ow/common.ts @@ -0,0 +1,111 @@ +// Shared: DB URLs, OpenWorkflow client, Kysely business DB (schema "biz"), workflow definitions. +import { OpenWorkflow } from "openworkflow"; +import { BackendPostgres } from "openworkflow/postgres"; +import { Kysely, PostgresDialect, sql } from "kysely"; +import pg from "pg"; +import process from "node:process"; + +export const PG_URL = process.env.PG_URL ?? "postgresql://postgres:spike@127.0.0.1:55435/postgres"; +export const WF_VERSION = process.env.WF_VERSION ?? "v1"; +export const TAG = process.env.TAG ?? `pid${process.pid}`; + +export const db = new Kysely({ + dialect: new PostgresDialect({ pool: new pg.Pool({ connectionString: PG_URL, max: 4 }) }), +}); + +export async function initBiz() { + await sql`CREATE SCHEMA IF NOT EXISTS biz`.execute(db); + await sql`CREATE TABLE IF NOT EXISTS biz.side_effect_ledger ( + id bigserial PRIMARY KEY, run_id text, step text, worker text, pid int, note text, + at timestamptz DEFAULT now())`.execute(db); +} + +export async function ledger(runId: string, step: string, note = "") { + await db.insertInto("biz.side_effect_ledger") + .values({ run_id: runId, step, worker: TAG, pid: process.pid, note }).execute(); + console.log(`[${TAG}] run=${runId.slice(0, 8)} step=${step} ${note}`); +} + +const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export async function makeClient() { + // connect() runs migrations by default (runMigrations: true), schema default "openworkflow". + const backend = await BackendPostgres.connect(PG_URL, { schema: "openworkflow" }); + const ow = new OpenWorkflow({ backend }); + + // Main vertical slice: submit download -> wait signal -> copy -> register -> retention sleep -> cleanup + const acquire = ow.defineWorkflow( + { name: "acquire", version: WF_VERSION }, + async ({ input, step, run, version }) => { + const { downloadId, copyMs = 0, retention = "3s" } = input as any; + const sub = await step.run({ name: "submit-download" }, async () => { + await ledger(run.id, "submit-download", `version=${version}`); + return { torrentHash: `hash-${downloadId}` }; + }); + const done = await step.waitForSignal<{ ok: boolean }>({ + name: "wait-download", signal: `download-done:${downloadId}`, timeout: "10m", + }); + if (!done) throw new Error("download wait timed out"); + const copied = await step.run({ name: "copy-file" }, async () => { + await ledger(run.id, "copy-file", "start"); + await wait(copyMs); + await ledger(run.id, "copy-file", "end"); + return { dest: `/lib/${downloadId}.mkv` }; + }); + if (process.env.EXTRA_STEP === "1") { + // EXTRA_STEP=1 simulates a code change between copy and register (tests code upgrade semantics) + await step.run({ name: "v2-extra-verify" }, async () => { await ledger(run.id, "v2-extra-verify"); return 1; }); + } + await step.run({ name: "register" }, async () => { await ledger(run.id, "register", `version=${version}`); return 1; }); + await step.sleep("retention", retention); + await step.run({ name: "cleanup" }, async () => { await ledger(run.id, "cleanup"); return 1; }); + return { sub, copied, signal: done.data }; + }, + ); + + // Retry test: step fails `failTimes` times then succeeds; step maximumAttempts = maxAttempts + const flaky = ow.defineWorkflow( + { name: "flaky", version: WF_VERSION }, + async ({ input, step, run }) => { + const { failTimes, maxAttempts } = input as any; + await step.run({ name: "before" }, async () => { await ledger(run.id, "before"); return 1; }); + return await step.run( + { name: "flaky-step", retryPolicy: { maximumAttempts: maxAttempts, initialInterval: "200ms", maximumInterval: "1s" } }, + async () => { + const n = await db.selectFrom("biz.side_effect_ledger").select(db.fn.countAll().as("c")) + .where("run_id", "=", run.id).where("step", "=", "flaky-step").executeTakeFirst(); + const attempt = Number(n?.c ?? 0) + 1; + await ledger(run.id, "flaky-step", `attempt=${attempt}`); + if (attempt <= failTimes) throw new Error(`boom attempt ${attempt}`); + return { attempt }; + }, + ); + }, + ); + + // Cancel test: long step, then a follow-up step + const slow = ow.defineWorkflow( + { name: "slow", version: WF_VERSION }, + async ({ step, run }) => { + await step.run({ name: "long" }, async () => { + await ledger(run.id, "long", "start"); + await wait(8000); + await ledger(run.id, "long", "end (still ran after cancel?)"); + return 1; + }); + await step.run({ name: "after-long" }, async () => { await ledger(run.id, "after-long"); return 1; }); + return "done"; + }, + ); + + // Concurrency test: many short runs + const burst = ow.defineWorkflow( + { name: "burst", version: WF_VERSION }, + async ({ step, run }) => { + await step.run({ name: "work" }, async () => { await wait(300); await ledger(run.id, "work"); return 1; }); + return 1; + }, + ); + + return { ow, backend, acquire, flaky, slow, burst }; +} diff --git a/spikes/deno/apps/server/src/ow/worker.ts b/spikes/deno/apps/server/src/ow/worker.ts new file mode 100644 index 00000000..1f6b2650 --- /dev/null +++ b/spikes/deno/apps/server/src/ow/worker.ts @@ -0,0 +1,14 @@ +// Deno port of spikes/openworkflow/worker.ts: node:fs/process.on -> Deno APIs +import { initBiz, makeClient, TAG, WF_VERSION } from "./common.ts"; +await initBiz(); +const { ow } = await makeClient(); +const worker = ow.newWorker({ concurrency: Number(Deno.env.get("CONCURRENCY") ?? 4) }); +await worker.start(); +const pidfile = Deno.env.get("PIDFILE"); +if (pidfile) Deno.writeTextFileSync(pidfile, String(Deno.pid)); +console.log(`[${TAG}] worker started pid=${Deno.pid} version=${WF_VERSION} runtime=deno ${Deno.version.deno}`); +Deno.addSignalListener("SIGTERM", async () => { + await worker.stop(); + console.log(`[${TAG}] worker stopped gracefully`); + Deno.exit(0); +}); diff --git a/spikes/deno/apps/server/src/pi/abort-resume.ts b/spikes/deno/apps/server/src/pi/abort-resume.ts new file mode 100644 index 00000000..e7db5c40 --- /dev/null +++ b/spikes/deno/apps/server/src/pi/abort-resume.ts @@ -0,0 +1,61 @@ +import process from "node:process"; +// Modes: abort | crash | resume. Uses a file-backed JSONL session so a second process can recover. +import { createModels, fauxProvider } from "@earendil-works/pi-ai"; +import { AgentHarness, BACKGROUND_CONTEXT, JsonlSessionRepo } from "@earendil-works/pi-agent-core"; +import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/harness/env/nodejs"; +import { downloadSubmit, inventoryQuery, script, submitted } from "./common.ts"; + +const mode = process.argv[2]; +const ctx = BACKGROUND_CONTEXT; +const faux = fauxProvider({ provider: "umt-fake" }); +const models = createModels(); +models.setProvider(faux.provider); +faux.setResponses(mode === "resume" ? [script[2]!] : script); + +const root = new URL("./.sessions", import.meta.url).pathname; +const fs = new NodeExecutionEnv({ cwd: root }); // used only as FileSystem for session storage; no shell tool is exposed +const repo = new JsonlSessionRepo({ fileSystem: fs, sessionsRoot: root }); +let session; +if (mode === "resume") { + const list = await repo.list({ cwd: root }, ctx); + const meta = list.find((m) => m.id === "crash-demo"); + console.log("found session", meta?.id, meta?.path); + session = await repo.open(meta!, ctx); +} else { + session = await repo.create({ id: mode === "crash" ? "crash-demo" : `abort-${Date.now()}`, cwd: root }, ctx); +} + +const { harness, open } = await AgentHarness.create({ + session, models, model: faux.getModel(), systemPrompt: "媒体库助手", + tools: [inventoryQuery, downloadSubmit], toolContext: { actorId: "user:1", grants: ["download.submit"] }, + toolExecution: "sequential", +}, ctx); +console.log("open operations at attach:", JSON.stringify(open)); + +let lane = await harness.lane("main", ctx); +for (const t of ["tool_start", "tool_end", "run_end", "operation_abort", "run_resume"] as const) + harness.events.on(t as any, async (e: any) => { + console.log(`[${e.type}]${e.recovery ? " (recovery)" : ""} ${e.toolName ?? ""} ${e.status ?? ""} ${e.result ? JSON.stringify(e.result.content[0]?.text).slice(0, 220) : ""}`); + if (e.type === "tool_start" && e.toolName === "download__submit") { + if (mode === "abort") setTimeout(async () => { console.log(">> lane.abort()"); console.log("abort result:", JSON.stringify(await lane.abort(ctx))); }, 300); + if (mode === "crash") setTimeout(() => { console.log(">> SIGKILL self during download.submit"); process.kill(process.pid, "SIGKILL"); }, 300); + } + }); + +if (mode === "resume") { + const r = await lane.resume(ctx); + console.log("RESUME RESULT", JSON.stringify(r)); +} else { + const r = await lane.prompt("把《Dune》1080p 收进来", undefined, ctx); + console.log("PROMPT RESULT", JSON.stringify(r)); +} +console.log("SUBMITTED in this process", JSON.stringify(submitted), "fake calls:", faux.state.callCount); +const entries = (await lane.findEntries(undefined, ctx)).reverse(); +for (const e of entries as any[]) { + if (e.type !== "message") { console.log(" entry", e.type); continue; } + const m = e.message; + const text = m.role === "assistant" ? m.content.map((c: any) => c.type === "toolCall" ? `toolCall(${c.name})` : c.type === "text" ? c.text : `<${c.type}>`).join(" | ") + : Array.isArray(m.content) ? m.content.map((c: any) => c.text).join("") : m.content; + console.log(` ${m.role.padEnd(10)} ${m.toolName ?? ""} ${m.isError ? "[isError]" : ""} ${String(text).slice(0, 200)}`); +} +await harness.close(ctx); diff --git a/spikes/deno/apps/server/src/pi/common.ts b/spikes/deno/apps/server/src/pi/common.ts new file mode 100644 index 00000000..457126a1 --- /dev/null +++ b/spikes/deno/apps/server/src/pi/common.ts @@ -0,0 +1,84 @@ +import process from "node:process"; +// Shared pieces for the Pi spike: whitelisted business tools + scripted fake model. +import { Type } from "typebox"; +import { + fauxAssistantMessage, fauxText, fauxThinking, fauxToolCall, + type FauxResponseFactory, +} from "@earendil-works/pi-ai"; +import type { AgentHarnessTool } from "@earendil-works/pi-agent-core"; + +// Capability id -> provider-safe tool name. Anthropic/OpenAI tool names must match ^[a-zA-Z0-9_-]{1,64}$, +// so "inventory.query" cannot be sent verbatim; the port maps ids <-> names. +export const toolName = (capabilityId: string) => capabilityId.replace(/\./g, "__"); + +export const submitted: Array<{ key: string; title: string }> = []; + +type Ctx = { actorId: string; grants: string[] }; + +export const inventoryQuery: AgentHarnessTool = { + name: toolName("inventory.query"), + label: "inventory.query", + description: "Query local library inventory for a title. Returns whether it exists and which versions.", + parameters: Type.Object({ title: Type.String(), year: Type.Optional(Type.Number()) }), + replay: "safe", // read-only: harness may re-execute after a crash + async execute(_id, params: any, onUpdate, toolCtx, invocation, context) { + onUpdate({ content: [{ type: "text", text: "searching…" }], details: {} }); + const found = params.title === "Arrival"; + return { + content: [{ type: "text", text: JSON.stringify({ title: params.title, inLibrary: found, versions: found ? ["2160p"] : [] }) }], + details: { actor: toolCtx.actorId, invocationId: invocation.invocationId, aborted: context.abortSignal?.aborted ?? false }, + }; + }, +}; + +export const downloadSubmit: AgentHarnessTool = { + name: toolName("download.submit"), + label: "download.submit", + description: "Submit a download plan. Server validates authorization and generates the idempotency key.", + parameters: Type.Object({ title: Type.String(), quality: Type.String() }), + replay: "never", // external side effect: after crash the harness synthesizes an 'interrupted' result instead of re-running + executionMode: "sequential", + async execute(_id, params: any, _onUpdate, toolCtx, invocation, context) { + if (!toolCtx.grants.includes("download.submit")) throw new Error("not authorized: download.submit"); + // Server-side idempotency key derived from operation semantics, not from the model. + const key = `dl:${params.title}:${params.quality}`; + const delayMs = Number(process.env.SUBMIT_DELAY_MS ?? 0); + if (delayMs) { + await new Promise((resolve, reject) => { + const t = setTimeout(resolve, delayMs); + context.abortSignal?.addEventListener("abort", () => { clearTimeout(t); reject(new Error("aborted by signal")); }); + }); + } + if (!submitted.find((s) => s.key === key)) submitted.push({ key, title: params.title }); + await invocation.setMemo("submitted", key); + return { content: [{ type: "text", text: JSON.stringify({ accepted: true, idempotencyKey: key, status: "submitted" }) }], details: { key } }; + }, +}; + +const lastToolResult = (ctx: any, name: string) => + [...ctx.messages].reverse().find((m: any) => m.role === "toolResult" && m.toolName === name); + +// Scripted fake model: each step is a factory that looks at the transcript (tool results) and decides. +export const script: FauxResponseFactory[] = [ + () => fauxAssistantMessage([ + fauxThinking("先查本地库存"), + fauxText("我先查一下库里有没有《Dune》。"), + fauxToolCall(toolName("inventory.query"), { title: "Dune", year: 2021 }), + ], { stopReason: "toolUse" }), + (ctx) => { + const inv = JSON.parse(lastToolResult(ctx, toolName("inventory.query")).content[0].text); + if (inv.inLibrary) return fauxAssistantMessage("库里已有,无需下载。"); + return fauxAssistantMessage([ + fauxText("库里没有,提交 1080p 下载;顺便试一下未授权的 bash。"), + fauxToolCall(toolName("download.submit"), { title: "Dune", quality: "1080p" }), + fauxToolCall("bash", { command: "rm -rf /" }), + ], { stopReason: "toolUse" }); + }, + (ctx) => { + const dl = lastToolResult(ctx, toolName("download.submit")); + const bash = lastToolResult(ctx, "bash"); + return fauxAssistantMessage( + `下载已提交(仅代表已提交,不代表已入库):${dl.content[0].text};bash 结果 isError=${bash?.isError}。`, + ); + }, +]; diff --git a/spikes/deno/apps/server/src/pi/fake-run.ts b/spikes/deno/apps/server/src/pi/fake-run.ts new file mode 100644 index 00000000..d0b320ae --- /dev/null +++ b/spikes/deno/apps/server/src/pi/fake-run.ts @@ -0,0 +1,51 @@ +import process from "node:process"; +// Fake model drives AgentHarness through 2 whitelisted tools; streams events. +import { createModels, fauxProvider } from "@earendil-works/pi-ai"; +import { AgentHarness, BACKGROUND_CONTEXT, MemorySessionRepo } from "@earendil-works/pi-agent-core"; +import { downloadSubmit, inventoryQuery, script, submitted } from "./common.ts"; + +const faux = fauxProvider({ provider: "umt-fake" }); +const models = createModels(); +models.setProvider(faux.provider); +faux.setResponses(script); + +const repo = new MemorySessionRepo(); +const session = await repo.create({ id: "spike-1" }, BACKGROUND_CONTEXT); +const { harness } = await AgentHarness.create({ + session, models, model: faux.getModel(), + systemPrompt: "你是媒体库自动化助手,只能使用提供的工具。", + tools: [inventoryQuery, downloadSubmit], // explicit whitelist; no discovery + toolContext: { actorId: "user:1", grants: ["download.submit"] }, + thinkingLevel: "off", + compaction: { enabled: true, reserveTokens: 4096, keepRecentTokens: 8000 }, + retry: { enabled: true, maxRetries: 2, baseDelayMs: 100 }, + toolExecution: "sequential", +}, BACKGROUND_CONTEXT); + +// Server-side authorization gate in addition to the whitelist. +harness.hooks.on("before_tool", (ev) => { + console.log(` [hook before_tool] ${ev.toolName} args=${JSON.stringify(ev.args)}`); + return undefined; +}); + +const types = ["run_start", "turn_start", "message_update", "tool_start", "tool_update", "tool_end", "turn_end", "run_end", "usage", "fault", "handler_error"] as const; +for (const t of types) harness.events.on(t as any, (e: any) => { + if (e.type === "message_update") { + if (e.event.type === "text_delta") process.stdout.write(` [text_delta] ${JSON.stringify(e.event.delta)}\n`); + else if (e.event.type === "toolcall_end") console.log(` [toolcall_end] ${e.event.toolCall.name}`); + return; + } + if (e.type === "tool_end") return console.log(`[tool_end] ${e.toolName} isError=${e.isError} -> ${JSON.stringify(e.result.content[0]?.text)}`); + if (e.type === "tool_start") return console.log(`[tool_start] ${e.toolName} ${JSON.stringify(e.args)}`); + if (e.type === "usage") return console.log(`[usage] in=${e.row?.usage?.input ?? "?"} totals.input=${e.totals.input} output=${e.totals.output}`); + if (e.type === "run_end") return console.log(`[run_end] status=${e.status}${e.error ? " error=" + JSON.stringify(e.error) : ""}`); + console.log(`[${e.type}]${e.lane ? " lane=" + e.lane : ""}`); +}); + +const lane = await harness.lane("main", BACKGROUND_CONTEXT); +const result = await lane.prompt("把《Dune》1080p 收进来", undefined, BACKGROUND_CONTEXT); +console.log("RESULT", JSON.stringify(result)); +console.log("SUBMITTED", JSON.stringify(submitted), "fake calls:", faux.state.callCount); +const entries = await lane.findEntries(undefined, BACKGROUND_CONTEXT); +console.log("TRANSCRIPT roles:", entries.map((e: any) => e.type === "message" ? e.message.role : e.type).join(",")); +await harness.close(BACKGROUND_CONTEXT); diff --git a/spikes/deno/apps/server/test/node-apis.test.ts b/spikes/deno/apps/server/test/node-apis.test.ts new file mode 100644 index 00000000..700a23d3 --- /dev/null +++ b/spikes/deno/apps/server/test/node-apis.test.ts @@ -0,0 +1,54 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createServer, type Server } from "node:http"; +import { spawn } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile, statfs } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createApp } from "../src/http.ts"; +import { redact } from "@spike/x"; + +let server: Server; +let base = ""; +beforeAll(async () => { + server = createServer((req, res) => { + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify({ path: req.url, method: req.method })); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); + const addr = server.address(); + base = `http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : 0}`; +}); +afterAll(async () => { await new Promise((r) => server.close(r)); }); + +describe("node compat under test runner", () => { + it("node:http mock server + fetch", async () => { + const r = await fetch(`${base}/api/v2/torrents/info?x=1`); + expect(await r.json()).toEqual({ path: "/api/v2/torrents/info?x=1", method: "GET" }); + }); + it("node:child_process spawn + kill", async () => { + const child = spawn("sleep", ["30"]); + await new Promise((r) => setTimeout(r, 100)); + const exited = new Promise<{ code: number | null; signal: string | null }>((r) => child.on("exit", (code, signal) => r({ code, signal }))); + expect(child.kill("SIGKILL")).toBe(true); + expect(await exited).toEqual({ code: null, signal: "SIGKILL" }); + }); + it("fs temp dir + statfs", async () => { + const dir = await mkdtemp(join(tmpdir(), "umt-")); + await writeFile(join(dir, "a.txt"), "hi"); + expect(await readFile(join(dir, "a.txt"), "utf8")).toBe("hi"); + const s = await statfs(dir); + expect(s.bsize).toBeGreaterThan(0); + expect(typeof s.bavail).toBe("number"); + await rm(dir, { recursive: true }); + }); + it("hono app.request SSE", async () => { + const res = await createApp().request("/api/v1/events?after=1"); + expect(res.headers.get("content-type")).toContain("text/event-stream"); + const text = await res.text(); + expect(text).toContain("id: 2"); + expect(text).toContain("event: tick"); + }); + it("workspace package import", () => { + expect(redact({ token: "x" })).toEqual({ token: "[REDACTED]" }); + }); +}); diff --git a/spikes/deno/apps/server/test/node_apis_test.ts b/spikes/deno/apps/server/test/node_apis_test.ts new file mode 100644 index 00000000..ef04214d --- /dev/null +++ b/spikes/deno/apps/server/test/node_apis_test.ts @@ -0,0 +1,55 @@ +import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { createServer, type Server } from "node:http"; +import { spawn } from "node:child_process"; +import { mkdtemp, readFile, rm, writeFile, statfs } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createApp } from "../src/http.ts"; +import { redact } from "@spike/x"; + +let server: Server; +let base = ""; +beforeAll(async () => { + server = createServer((req, res) => { + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify({ path: req.url, method: req.method })); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); + const addr = server.address(); + base = `http://127.0.0.1:${typeof addr === "object" && addr ? addr.port : 0}`; +}); +afterAll(async () => { await new Promise((r) => server.close(r)); }); + +describe("node compat under test runner", () => { + it("node:http mock server + fetch", async () => { + const r = await fetch(`${base}/api/v2/torrents/info?x=1`); + expect(await r.json()).toEqual({ path: "/api/v2/torrents/info?x=1", method: "GET" }); + }); + it("node:child_process spawn + kill", async () => { + const child = spawn("sleep", ["30"]); + await new Promise((r) => setTimeout(r, 100)); + const exited = new Promise<{ code: number | null; signal: string | null }>((r) => child.on("exit", (code, signal) => r({ code, signal }))); + expect(child.kill("SIGKILL")).toBe(true); + expect(await exited).toEqual({ code: null, signal: "SIGKILL" }); + }); + it("fs temp dir + statfs", async () => { + const dir = await mkdtemp(join(tmpdir(), "umt-")); + await writeFile(join(dir, "a.txt"), "hi"); + expect(await readFile(join(dir, "a.txt"), "utf8")).toBe("hi"); + const s = await statfs(dir); + expect(s.bsize).toBeGreaterThan(0); + expect(typeof s.bavail).toBe("number"); + await rm(dir, { recursive: true }); + }); + it("hono app.request SSE", async () => { + const res = await createApp().request("/api/v1/events?after=1"); + expect(res.headers.get("content-type")).toContain("text/event-stream"); + const text = await res.text(); + expect(text).toContain("id: 2"); + expect(text).toContain("event: tick"); + }); + it("workspace package import", () => { + expect(redact({ token: "x" })).toEqual({ token: "[REDACTED]" }); + }); +}); diff --git a/spikes/deno/apps/server/vitest.config.ts b/spikes/deno/apps/server/vitest.config.ts new file mode 100644 index 00000000..c07b2caf --- /dev/null +++ b/spikes/deno/apps/server/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vitest/config"; +// Vite's resolver does not read deno.json workspace/imports: map workspace members manually. +export default defineConfig({ + resolve: { alias: { "@spike/x": new URL("../../packages/x/src/mod.ts", import.meta.url).pathname } }, + test: { include: ["test/**/*.test.ts"] }, +}); diff --git a/spikes/deno/deno.json b/spikes/deno/deno.json new file mode 100644 index 00000000..603e7ddd --- /dev/null +++ b/spikes/deno/deno.json @@ -0,0 +1,55 @@ +{ + "workspace": [ + "./packages/x", + "./packages/contracts", + "./apps/server", + "./web" + ], + "nodeModulesDir": "auto", + "imports": { + "openworkflow": "npm:openworkflow@0.10.1", + "openworkflow/postgres": "npm:openworkflow@0.10.1/postgres", + "postgres": "npm:postgres@^3.4.9", + "kysely": "npm:kysely@^0.29.6", + "kysely/": "npm:/kysely@^0.29.6/", + "kysely-postgres-js": "npm:kysely-postgres-js@^5.0.1", + "pg": "npm:pg@^8.23.0", + "@types/pg": "npm:@types/pg@^8", + "@earendil-works/pi-agent-core": "npm:@earendil-works/pi-agent-core@0.87.1", + "@earendil-works/pi-agent-core/": "npm:/@earendil-works/pi-agent-core@0.87.1/", + "@earendil-works/pi-ai": "npm:@earendil-works/pi-ai@0.87.1", + "typebox": "npm:typebox@^1", + "hono": "npm:hono@^4.13.9", + "hono/": "npm:/hono@^4.13.9/", + "pino": "npm:pino@^10.3.1", + "pino-pretty": "npm:pino-pretty@^13.1.3", + "@logtape/logtape": "npm:@logtape/logtape@^2", + "@std/expect": "jsr:@std/expect@^1", + "@std/testing": "jsr:@std/testing@^1", + "vitest": "npm:vitest@5.0.2", + "typescript": "npm:typescript@7.0.2", + "@types/node": "npm:@types/node@^24" + }, + "tasks": { + "typecheck": "deno check", + "tsgo": "deno task types && deno run -A npm:typescript@7.0.2/tsc --noEmit -p tsconfig.tsc.json", + "types": "deno types > types/deno.d.ts" + }, + "lint": { + "rules": { + "exclude": [ + "no-explicit-any" + ] + } + }, + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "lib": [ + "deno.window", + "deno.unstable", + "dom" + ] + } +} diff --git a/spikes/deno/deno.lock b/spikes/deno/deno.lock new file mode 100644 index 00000000..4fe101c1 --- /dev/null +++ b/spikes/deno/deno.lock @@ -0,0 +1,5200 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@^1.0.19": "1.0.19", + "jsr:@std/async@^1.5.1": "1.5.1", + "jsr:@std/data-structures@^1.1.3": "1.1.3", + "jsr:@std/expect@1": "1.0.20", + "jsr:@std/fs@^1.0.24": "1.0.24", + "jsr:@std/internal@^1.0.12": "1.0.14", + "jsr:@std/internal@^1.0.14": "1.0.14", + "jsr:@std/path@^1.1.5": "1.1.6", + "jsr:@std/path@^1.1.6": "1.1.6", + "jsr:@std/testing@1": "1.0.21", + "npm:@earendil-works/pi-agent-core@0.87.1": "0.87.1_zod@4.6.5", + "npm:@earendil-works/pi-ai@0.87.1": "0.87.1_zod@4.6.5", + "npm:@fontsource-variable/inter-tight@^5.2.7": "5.3.0", + "npm:@fontsource-variable/space-grotesk@^5.2.10": "5.3.0", + "npm:@logtape/logtape@2": "2.3.8", + "npm:@react-router/dev@7.12.0": "7.12.0_react-router@7.12.0_typescript@7.0.2_vite@7.3.6_@types+node@24.19.0", + "npm:@react-router/node@7.12.0": "7.12.0_react-router@7.12.0_typescript@7.0.2_react@19.3.0_react-dom@19.3.0", + "npm:@tailwindcss/vite@^4.1.13": "4.3.3_vite@7.3.6_@types+node@24.19.0", + "npm:@tanstack/react-query@^5.90.21": "5.104.0_react@19.3.0", + "npm:@types/node@24": "24.19.0", + "npm:@types/pg@8": "8.23.1", + "npm:@types/react-dom@^19.2.3": "19.3.0_@types+react@19.3.0", + "npm:@types/react@^19.2.7": "19.3.0", + "npm:class-variance-authority@~0.7.1": "0.7.1", + "npm:clsx@^2.1.1": "2.1.1", + "npm:date-fns@^4.1.0": "4.4.0", + "npm:hono@^4.13.9": "4.13.9", + "npm:isbot@^5.1.31": "5.2.2", + "npm:kysely-postgres-js@^5.0.1": "5.0.1_kysely@0.29.6_postgres@3.4.9", + "npm:kysely@~0.29.6": "0.29.6", + "npm:lucide-react@0.576": "0.576.0_react@19.3.0", + "npm:next-themes@~0.4.6": "0.4.6_react@19.3.0_react-dom@19.3.0", + "npm:openworkflow@0.10.1": "0.10.1_postgres@3.4.9", + "npm:pg@^8.23.0": "8.23.0", + "npm:pino-pretty@^13.1.3": "13.1.3", + "npm:pino@^10.3.1": "10.3.1", + "npm:postgres@^3.4.9": "3.4.9", + "npm:radix-ui@^1.4.3": "1.6.7_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0", + "npm:react-day-picker@^10.0.1": "10.0.1_@types+react@19.3.0_react@19.3.0", + "npm:react-dom@^19.2.4": "19.3.0_react@19.3.0", + "npm:react-router@7.12.0": "7.12.0_react@19.3.0_react-dom@19.3.0", + "npm:react@^19.2.4": "19.3.0", + "npm:shadcn@^3.8.5": "3.8.5_@types+node@24.19.0", + "npm:sonner@^2.0.7": "2.0.8_@types+react@19.3.0_react@19.3.0_react-dom@19.3.0", + "npm:tailwind-merge@^3.5.0": "3.7.0", + "npm:tailwindcss@^4.1.13": "4.3.3", + "npm:tw-animate-css@^1.4.0": "1.4.0", + "npm:typebox@1": "1.3.27", + "npm:typescript@7.0.2": "7.0.2", + "npm:vite-tsconfig-paths@^5.1.4": "5.1.4_vite@7.3.6_@types+node@24.19.0", + "npm:vite@7": "7.3.6_@types+node@24.19.0", + "npm:vite@^7.1.7": "7.3.6_@types+node@24.19.0", + "npm:vitest@5.0.2": "5.0.2_@types+node@24.19.0_vite@7.3.6", + "npm:zod@^4.3.6": "4.6.5" + }, + "jsr": { + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", + "dependencies": [ + "jsr:@std/internal@^1.0.12" + ] + }, + "@std/async@1.5.1": { + "integrity": "c82455dc70cab29a0adf3ac8075db934785bffdd3902af5afcf25d21a74cfd86" + }, + "@std/data-structures@1.1.3": { + "integrity": "a5ec67199e79ee5b29123474d93fa9a47701ec08a4ef3bd1b623a36476306c5b" + }, + "@std/expect@1.0.20": { + "integrity": "ffc4f3c33f732417e3e9acf3d995818c89984313c37d933b9ebbb4571d2887e9", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/internal@^1.0.14", + "jsr:@std/path@^1.1.6" + ] + }, + "@std/fs@1.0.24": { + "integrity": "f3061b45b81673a2bece689da041df32d174be064c89eb6397fb5718d3fb7877", + "dependencies": [ + "jsr:@std/path@^1.1.5" + ] + }, + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" + }, + "@std/path@1.1.6": { + "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe", + "dependencies": [ + "jsr:@std/internal@^1.0.14" + ] + }, + "@std/testing@1.0.21": { + "integrity": "a0d14b2519005242821565091127c7d6fa96ca1b330e84e9249afcd602ee764f", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/async", + "jsr:@std/data-structures", + "jsr:@std/fs", + "jsr:@std/internal@^1.0.14", + "jsr:@std/path@^1.1.6" + ] + } + }, + "npm": { + "@antfu/ni@25.0.0": { + "integrity": "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA==", + "dependencies": [ + "ansis", + "fzf", + "package-manager-detector", + "tinyexec" + ], + "bin": true + }, + "@anthropic-ai/sdk@0.124.0_zod@4.6.5": { + "integrity": "sha512-cN5O8i9UVxHeOQAzj/XjshWXG8KiibJDw9OGpH2Z/eR3n/RBxdoLxDJOcfqAJWvjaMDFfHTBADU04hWRJVkDyA==", + "dependencies": [ + "json-schema-to-ts", + "standardwebhooks", + "zod@4.6.5" + ], + "optionalPeers": [ + "zod@4.6.5" + ], + "bin": true + }, + "@aws-sdk/client-bedrock-runtime@3.1127.0": { + "integrity": "sha512-IDl/lrPb90aH+pZFHGNDmgH9nAUQj5PlZH1sJ3w7RikctyjHSnY3oNjZhrLoaBoQn/rNK0zsP6OHEqEhj2tdLA==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/credential-provider-node", + "@aws-sdk/eventstream-handler-node", + "@aws-sdk/middleware-eventstream", + "@aws-sdk/middleware-websocket", + "@aws-sdk/token-providers@3.1127.0", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/core@3.978.1": { + "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==", + "dependencies": [ + "@aws-sdk/types", + "@aws-sdk/xml-builder", + "@aws/lambda-invoke-store", + "@smithy/core", + "@smithy/signature-v4", + "@smithy/types", + "bowser", + "tslib" + ] + }, + "@aws-sdk/credential-provider-env@3.972.72": { + "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-http@3.972.74": { + "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-ini@3.973.17": { + "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-login", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/credential-provider-imds", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-login@3.972.79": { + "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-node@3.972.84": { + "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==", + "dependencies": [ + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-ini", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@aws-sdk/types", + "@smithy/core", + "@smithy/credential-provider-imds", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-process@3.972.72": { + "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-sso@3.973.16": { + "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/token-providers@3.1138.0", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-web-identity@3.972.78": { + "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/eventstream-handler-node@3.972.35": { + "integrity": "sha512-a8xilRoRaalvSPZdfrs0VY3/BPc0uYhXW56Z/k6nmZ5Vk430LHeowf9APIPq8utM3tMJSNGSSuJMlB9YrPsN+A==", + "dependencies": [ + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/middleware-eventstream@3.972.30": { + "integrity": "sha512-B6gvZlcnRBNWraKgyEjKsbhv+VjtbyHmpU7hmtTHzXpDSxHPpCJnYLpglEOOsF+SFne20DbZC5IVC1aNr2Pb4A==", + "dependencies": [ + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/middleware-websocket@3.972.54": { + "integrity": "sha512-bhESdru8u8KosziH8jcVta/NM/dNreeRz2+2aU86si5bfBYOiJ6IiqD+9U4URUcU5WJO3Yw+jO3GLDIHlscXMQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/signature-v4", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/nested-clients@3.997.46": { + "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/signature-v4-multi-region", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/signature-v4-multi-region@3.996.47": { + "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==", + "dependencies": [ + "@aws-sdk/types", + "@smithy/signature-v4", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/token-providers@3.1127.0": { + "integrity": "sha512-Dv2TMWBshJ+tF6ahs2Sy5bh4Iabsd4GAQqVvE9XZmYmnoaVbpS2QKIKE/HRacc7bTtbjEEvP+laGzHvHlf1CiQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/token-providers@3.1138.0": { + "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/types@3.974.6": { + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/xml-builder@3.972.41": { + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@aws/lambda-invoke-store@0.3.0": { + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==" + }, + "@babel/code-frame@7.29.7": { + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dependencies": [ + "@babel/helper-validator-identifier", + "js-tokens", + "picocolors" + ] + }, + "@babel/compat-data@7.29.7": { + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==" + }, + "@babel/core@7.29.7": { + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dependencies": [ + "@babel/code-frame", + "@babel/generator", + "@babel/helper-compilation-targets", + "@babel/helper-module-transforms", + "@babel/helpers", + "@babel/parser", + "@babel/template", + "@babel/traverse", + "@babel/types", + "@jridgewell/remapping", + "convert-source-map", + "debug", + "gensync", + "json5", + "semver@6.3.1" + ] + }, + "@babel/generator@7.29.8": { + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dependencies": [ + "@babel/parser", + "@babel/types", + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping", + "jsesc" + ] + }, + "@babel/helper-annotate-as-pure@7.29.7": { + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dependencies": [ + "@babel/types" + ] + }, + "@babel/helper-compilation-targets@7.29.7": { + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dependencies": [ + "@babel/compat-data", + "@babel/helper-validator-option", + "browserslist", + "lru-cache", + "semver@6.3.1" + ] + }, + "@babel/helper-create-class-features-plugin@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dependencies": [ + "@babel/core", + "@babel/helper-annotate-as-pure", + "@babel/helper-member-expression-to-functions", + "@babel/helper-optimise-call-expression", + "@babel/helper-replace-supers", + "@babel/helper-skip-transparent-expression-wrappers", + "@babel/traverse", + "semver@6.3.1" + ] + }, + "@babel/helper-globals@7.29.7": { + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==" + }, + "@babel/helper-member-expression-to-functions@7.29.7": { + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dependencies": [ + "@babel/traverse", + "@babel/types" + ] + }, + "@babel/helper-module-imports@7.29.7": { + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dependencies": [ + "@babel/traverse", + "@babel/types" + ] + }, + "@babel/helper-module-transforms@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dependencies": [ + "@babel/core", + "@babel/helper-module-imports", + "@babel/helper-validator-identifier", + "@babel/traverse" + ] + }, + "@babel/helper-optimise-call-expression@7.29.7": { + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dependencies": [ + "@babel/types" + ] + }, + "@babel/helper-plugin-utils@7.29.7": { + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==" + }, + "@babel/helper-replace-supers@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dependencies": [ + "@babel/core", + "@babel/helper-member-expression-to-functions", + "@babel/helper-optimise-call-expression", + "@babel/traverse" + ] + }, + "@babel/helper-skip-transparent-expression-wrappers@7.29.7": { + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dependencies": [ + "@babel/traverse", + "@babel/types" + ] + }, + "@babel/helper-string-parser@7.29.7": { + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==" + }, + "@babel/helper-validator-identifier@7.29.7": { + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" + }, + "@babel/helper-validator-option@7.29.7": { + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==" + }, + "@babel/helpers@7.29.7": { + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dependencies": [ + "@babel/template", + "@babel/types" + ] + }, + "@babel/parser@7.29.9": { + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dependencies": [ + "@babel/types" + ], + "bin": true + }, + "@babel/plugin-syntax-jsx@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dependencies": [ + "@babel/core", + "@babel/helper-plugin-utils" + ] + }, + "@babel/plugin-syntax-typescript@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dependencies": [ + "@babel/core", + "@babel/helper-plugin-utils" + ] + }, + "@babel/plugin-transform-modules-commonjs@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dependencies": [ + "@babel/core", + "@babel/helper-module-transforms", + "@babel/helper-plugin-utils" + ] + }, + "@babel/plugin-transform-typescript@7.29.9_@babel+core@7.29.7": { + "integrity": "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg==", + "dependencies": [ + "@babel/core", + "@babel/helper-annotate-as-pure", + "@babel/helper-create-class-features-plugin", + "@babel/helper-plugin-utils", + "@babel/helper-skip-transparent-expression-wrappers", + "@babel/plugin-syntax-typescript" + ] + }, + "@babel/preset-typescript@7.29.7_@babel+core@7.29.7": { + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dependencies": [ + "@babel/core", + "@babel/helper-plugin-utils", + "@babel/helper-validator-option", + "@babel/plugin-syntax-jsx", + "@babel/plugin-transform-modules-commonjs", + "@babel/plugin-transform-typescript" + ] + }, + "@babel/runtime@7.29.7": { + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==" + }, + "@babel/template@7.29.7": { + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dependencies": [ + "@babel/code-frame", + "@babel/parser", + "@babel/types" + ] + }, + "@babel/traverse@7.29.8": { + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dependencies": [ + "@babel/code-frame", + "@babel/generator", + "@babel/helper-globals", + "@babel/parser", + "@babel/template", + "@babel/types", + "debug" + ] + }, + "@babel/types@7.29.8": { + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dependencies": [ + "@babel/helper-string-parser", + "@babel/helper-validator-identifier" + ] + }, + "@date-fns/tz@1.5.0": { + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==" + }, + "@dotenvx/dotenvx@1.75.1": { + "integrity": "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==", + "dependencies": [ + "@dotenvx/primitives", + "commander@11.1.0", + "conf", + "dotenv", + "enquirer", + "env-paths", + "execa@5.1.1", + "fdir", + "ignore@5.3.2", + "object-treeify", + "open@8.4.2", + "picomatch@4.0.7", + "systeminformation", + "undici", + "which@4.0.0", + "yocto-spinner" + ], + "bin": true + }, + "@dotenvx/primitives@0.8.0": { + "integrity": "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==" + }, + "@earendil-works/chord@0.87.1": { + "integrity": "sha512-bg7IkJGFcEaMqqYgOGUiq5Ky9RghpRfrlZ8I/v/1b4bBZ02A7t3E+6uhPRbadwWb/kWsnVFbZsqOKRN4a3LLCg==", + "dependencies": [ + "esbuild" + ] + }, + "@earendil-works/pi-agent-core@0.87.1_zod@4.6.5": { + "integrity": "sha512-Zev3B0HK7YS5A4EZQ2XnEqiJuirx6QBiltJ+LpmjV5a/+2IU0cfKtIfnkNkORK707XOvKBY2WRtk7cAwHpbh2Q==", + "dependencies": [ + "@earendil-works/chord", + "@earendil-works/pi-ai", + "@earendil-works/pi-telemetry", + "diff", + "ignore@7.0.8", + "typebox", + "yaml" + ] + }, + "@earendil-works/pi-ai@0.87.1_zod@4.6.5": { + "integrity": "sha512-X/3PfQBnnoeVdO9Cv8zHghUMglzlgNZYGNzoPnbRoGnHl3Rw3TlA2UKSUB7BRHUOxMryHXYa8dnjWZlbRheDZA==", + "dependencies": [ + "@anthropic-ai/sdk", + "@aws-sdk/client-bedrock-runtime", + "@earendil-works/pi-telemetry", + "@google/genai", + "@smithy/node-http-handler", + "http-proxy-agent", + "https-proxy-agent@9.1.0", + "openai", + "partial-json", + "typebox" + ], + "bin": true + }, + "@earendil-works/pi-telemetry@0.87.1": { + "integrity": "sha512-MC6TRQH5lgMXpcN+Vku2WMI2T8BsiUPzMQHGo81uqFZD3/9O79WWJAysEDGuzduP6R4tvtgwMLwmqIxynM10JQ==" + }, + "@esbuild/aix-ppc64@0.28.2": { + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/android-arm64@0.28.2": { + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm@0.28.2": { + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-x64@0.28.2": { + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/darwin-arm64@0.28.2": { + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-x64@0.28.2": { + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-arm64@0.28.2": { + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-x64@0.28.2": { + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/linux-arm64@0.28.2": { + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm@0.28.2": { + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-ia32@0.28.2": { + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-loong64@0.28.2": { + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-mips64el@0.28.2": { + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-ppc64@0.28.2": { + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-riscv64@0.28.2": { + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-s390x@0.28.2": { + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-x64@0.28.2": { + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-arm64@0.28.2": { + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-x64@0.28.2": { + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-arm64@0.28.2": { + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-x64@0.28.2": { + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/openharmony-arm64@0.28.2": { + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@esbuild/sunos-x64@0.28.2": { + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/win32-arm64@0.28.2": { + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-ia32@0.28.2": { + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-x64@0.28.2": { + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@floating-ui/core@1.8.0": { + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "dependencies": [ + "@floating-ui/utils" + ] + }, + "@floating-ui/dom@1.8.0": { + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "dependencies": [ + "@floating-ui/core", + "@floating-ui/utils" + ] + }, + "@floating-ui/react-dom@2.1.9_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "dependencies": [ + "@floating-ui/dom", + "react", + "react-dom" + ] + }, + "@floating-ui/utils@0.2.12": { + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==" + }, + "@fontsource-variable/inter-tight@5.3.0": { + "integrity": "sha512-ZhZZ29sNZ15P1K+cUt13GeBRYON0akgmNiftshUeDLos7X7zGcRgPzt434879Xhq1QbwlW3M/apBMk7jWabwvA==" + }, + "@fontsource-variable/space-grotesk@5.3.0": { + "integrity": "sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw==" + }, + "@google/genai@2.21.0": { + "integrity": "sha512-+PDtco2/Z0ONdzCGekCoCT+O1VJS9xJQNN4XzQpXG/t3El/SWWMkCWlFRO1KmivOHPa4Q0VjUYu1HBKCZ/v33Q==", + "dependencies": [ + "google-auth-library", + "p-retry", + "protobufjs", + "ws" + ], + "scripts": true + }, + "@hono/node-server@2.1.1_hono@4.13.9": { + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "dependencies": [ + "hono" + ] + }, + "@inquirer/ansi@2.0.8": { + "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==" + }, + "@inquirer/confirm@6.3.2_@types+node@24.19.0": { + "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", + "dependencies": [ + "@inquirer/core", + "@inquirer/type", + "@types/node" + ], + "optionalPeers": [ + "@types/node" + ] + }, + "@inquirer/core@12.0.3_@types+node@24.19.0": { + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dependencies": [ + "@inquirer/ansi", + "@inquirer/figures", + "@inquirer/type", + "@types/node", + "cli-width", + "fast-wrap-ansi", + "mute-stream", + "signal-exit@4.1.0" + ], + "optionalPeers": [ + "@types/node" + ] + }, + "@inquirer/figures@2.0.9": { + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==" + }, + "@inquirer/type@4.1.1_@types+node@24.19.0": { + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", + "dependencies": [ + "@types/node" + ], + "optionalPeers": [ + "@types/node" + ] + }, + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/remapping@2.3.5": { + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec@1.6.0": { + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==" + }, + "@jridgewell/trace-mapping@0.3.31": { + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "@logtape/logtape@2.3.8": { + "integrity": "sha512-MufJjxJF9frfmRuDBmpTYxhMgENkNGqSaDw59DnSjVeZ+ptxkDgfEoaJkLcpxtF8is9TAptRsCYXfw+j+3yX3A==" + }, + "@mjackson/node-fetch-server@0.2.0": { + "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==" + }, + "@modelcontextprotocol/sdk@1.30.1_zod@3.25.76": { + "integrity": "sha512-H2HxLvC3HDNybePJaLdSrU1hhUK5iQw+WvV1b01myFyI7sdVGe1u/IPTE5D9fGCiJDVtgMV/lmFkQXLmQyIFYA==", + "dependencies": [ + "@hono/node-server", + "ajv", + "ajv-formats@3.0.1_ajv@8.20.0", + "content-type@1.0.5", + "cors", + "cross-spawn", + "eventsource", + "eventsource-parser", + "express", + "express-rate-limit", + "hono", + "jose", + "json-schema-typed@8.0.2", + "pkce-challenge", + "raw-body", + "zod@3.25.76", + "zod-to-json-schema" + ] + }, + "@mswjs/interceptors@0.41.9": { + "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==", + "dependencies": [ + "@open-draft/deferred-promise@2.2.0", + "@open-draft/logger", + "@open-draft/until", + "is-node-process", + "outvariant", + "strict-event-emitter" + ] + }, + "@napi-rs/lzma-linux-x64-gnu@1.5.1": { + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@nodelib/fs.scandir@2.1.5": { + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": [ + "@nodelib/fs.stat", + "run-parallel" + ] + }, + "@nodelib/fs.stat@2.0.5": { + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" + }, + "@nodelib/fs.walk@1.2.8": { + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": [ + "@nodelib/fs.scandir", + "fastq" + ] + }, + "@open-draft/deferred-promise@2.2.0": { + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==" + }, + "@open-draft/deferred-promise@3.0.0": { + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==" + }, + "@open-draft/logger@0.3.0": { + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dependencies": [ + "is-node-process", + "outvariant" + ] + }, + "@open-draft/until@2.1.0": { + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" + }, + "@pinojs/redact@0.4.0": { + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" + }, + "@protobufjs/aspromise@1.1.2": { + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64@1.1.2": { + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen@2.0.5": { + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==" + }, + "@protobufjs/eventemitter@1.1.1": { + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==" + }, + "@protobufjs/fetch@1.1.1": { + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dependencies": [ + "@protobufjs/aspromise" + ] + }, + "@protobufjs/float@1.0.2": { + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/path@1.1.2": { + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool@1.1.0": { + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8@1.1.2": { + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==" + }, + "@radix-ui/number@1.1.3": { + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==" + }, + "@radix-ui/primitive@1.1.7": { + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==" + }, + "@radix-ui/react-accessible-icon@1.1.15_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-WTQwcAvQf5sOcuUyi90lKPbhwcvQ+j55cjrSmeaN+L2vKU3DooOvlKw2MDeiJ5IkV5N905KW0/fGojKOBhD11A==", + "dependencies": [ + "@radix-ui/react-visually-hidden", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-accordion@1.2.20_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-collapsible", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-id", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-alert-dialog@1.1.23_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-dialog", + "@radix-ui/react-primitive", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-arrow@1.1.15_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "dependencies": [ + "@radix-ui/react-primitive", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-aspect-ratio@1.1.15_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ==", + "dependencies": [ + "@radix-ui/react-primitive", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-avatar@1.2.6_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-context", + "@radix-ui/react-primitive", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-is-hydrated", + "@radix-ui/react-use-layout-effect", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-checkbox@1.3.11_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-size", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-collapsible@1.1.20_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-id", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-layout-effect", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-collection@1.1.15_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "dependencies": [ + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-compose-refs@1.1.5_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "dependencies": [ + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-context-menu@2.3.7_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-context", + "@radix-ui/react-menu", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-context@1.2.2_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "dependencies": [ + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-dialog@1.1.23_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-focus-guards", + "@radix-ui/react-focus-scope", + "@radix-ui/react-id", + "@radix-ui/react-portal", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-layout-effect", + "@types/react", + "@types/react-dom", + "aria-hidden", + "react", + "react-dom", + "react-remove-scroll" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-direction@1.1.4_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "dependencies": [ + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-dismissable-layer@1.1.19_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-primitive", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-effect-event", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-dropdown-menu@2.1.24_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-id", + "@radix-ui/react-menu", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-focus-guards@1.1.6_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "dependencies": [ + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-focus-scope@1.1.16_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "dependencies": [ + "@radix-ui/react-compose-refs", + "@radix-ui/react-primitive", + "@radix-ui/react-use-callback-ref", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-form@0.1.16_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-Q4TLEn2A7TAypxwmd6R9EwrlXDvkfYSDMrq9/887AXAGh+G1rH+kYJKSTv+Si9Y0JPKTwKYv6PviAJosysNimA==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-id", + "@radix-ui/react-label", + "@radix-ui/react-primitive", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-hover-card@1.1.23_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-popper", + "@radix-ui/react-portal", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-id@1.1.4_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "dependencies": [ + "@radix-ui/react-use-layout-effect", + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-label@2.1.15_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "dependencies": [ + "@radix-ui/react-primitive", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-menu@2.1.24_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-focus-guards", + "@radix-ui/react-focus-scope", + "@radix-ui/react-id", + "@radix-ui/react-popper", + "@radix-ui/react-portal", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", + "@radix-ui/react-slot", + "@radix-ui/react-use-callback-ref", + "@types/react", + "@types/react-dom", + "aria-hidden", + "react", + "react-dom", + "react-remove-scroll" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-menubar@1.1.24_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-id", + "@radix-ui/react-menu", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-navigation-menu@1.2.22_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-id", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-layout-effect", + "@radix-ui/react-use-previous", + "@radix-ui/react-visually-hidden", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-one-time-password-field@0.1.16_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-Tj9P6ntAJEw52oq/F0AGknXR4XncxEt7XU47O3xJQOiWfLzEy3d9gtgKfvjSzGxzHkfL+VzvxGu2KTFsloJqXw==", + "dependencies": [ + "@radix-ui/number", + "@radix-ui/primitive", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-effect-event", + "@radix-ui/react-use-is-hydrated", + "@radix-ui/react-use-layout-effect", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-password-toggle-field@0.1.11_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-4gvFnmDXu3dgj21CqsufzIameRvlRd4SBqaWhcrlrNhRo0Y5i/49AmRJYe1fdAM3G2VNBbmin4b0D6cdQocwgw==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-id", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-effect-event", + "@radix-ui/react-use-is-hydrated", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-popover@1.1.23_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-focus-guards", + "@radix-ui/react-focus-scope", + "@radix-ui/react-id", + "@radix-ui/react-popper", + "@radix-ui/react-portal", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "aria-hidden", + "react", + "react-dom", + "react-remove-scroll" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-popper@1.3.7_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "dependencies": [ + "@floating-ui/react-dom", + "@radix-ui/react-arrow", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-primitive", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-layout-effect", + "@radix-ui/react-use-rect", + "@radix-ui/react-use-size", + "@radix-ui/rect", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-portal@1.1.17_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "dependencies": [ + "@radix-ui/react-primitive", + "@radix-ui/react-use-layout-effect", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-presence@1.1.10_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "dependencies": [ + "@radix-ui/react-use-layout-effect", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-primitive@2.1.10_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "dependencies": [ + "@radix-ui/react-slot", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-progress@1.1.16_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", + "dependencies": [ + "@radix-ui/react-context", + "@radix-ui/react-primitive", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-radio-group@1.4.7_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-size", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-roving-focus@1.1.19_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-id", + "@radix-ui/react-primitive", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-is-hydrated", + "@radix-ui/react-use-layout-effect", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-scroll-area@1.2.18_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "dependencies": [ + "@radix-ui/number", + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-layout-effect", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-select@2.3.7_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "dependencies": [ + "@radix-ui/number", + "@radix-ui/primitive", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-focus-guards", + "@radix-ui/react-focus-scope", + "@radix-ui/react-id", + "@radix-ui/react-popper", + "@radix-ui/react-portal", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-layout-effect", + "@radix-ui/react-use-previous", + "@radix-ui/react-visually-hidden", + "@types/react", + "@types/react-dom", + "aria-hidden", + "react", + "react-dom", + "react-remove-scroll" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-separator@1.1.15_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "dependencies": [ + "@radix-ui/react-primitive", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-slider@1.4.7_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==", + "dependencies": [ + "@radix-ui/number", + "@radix-ui/primitive", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-layout-effect", + "@radix-ui/react-use-previous", + "@radix-ui/react-use-size", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-slot@1.3.3_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "dependencies": [ + "@radix-ui/react-compose-refs", + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-switch@1.3.7_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-size", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-tabs@1.1.21_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-id", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-toast@1.2.23_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-ofhyAsYaocRGOs/n0XWdUOSVzEAG6BfrMVM8z0c0kLEWY38w/0WuMFPTJP/HVaZPYkMvHZoKIIhNcjbTCBILPg==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-portal", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-layout-effect", + "@radix-ui/react-visually-hidden", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-toggle-group@1.1.19_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", + "@radix-ui/react-toggle", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-toggle@1.1.18_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-primitive", + "@radix-ui/react-use-controllable-state", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-toolbar@1.1.19_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-context", + "@radix-ui/react-direction", + "@radix-ui/react-primitive", + "@radix-ui/react-roving-focus", + "@radix-ui/react-separator", + "@radix-ui/react-toggle-group", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-tooltip@1.2.16_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-id", + "@radix-ui/react-popper", + "@radix-ui/react-portal", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-slot", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-layout-effect", + "@radix-ui/react-visually-hidden", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/react-use-callback-ref@1.1.4_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "dependencies": [ + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-use-controllable-state@1.2.6_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-use-effect-event", + "@radix-ui/react-use-layout-effect", + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-use-effect-event@0.0.5_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "dependencies": [ + "@radix-ui/react-use-layout-effect", + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-use-escape-keydown@1.1.5_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-ge3ipobwSXTj4JyVtswQ7qZj0ZHdtbGuOno/LrgAAeSxtsJ6Vs4Gz5IkPH2bmqpjcLUFoqGhA/mueuIf63UXlA==", + "dependencies": [ + "@radix-ui/react-use-callback-ref", + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-use-is-hydrated@0.1.3_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "dependencies": [ + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-use-layout-effect@1.1.4_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "dependencies": [ + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-use-previous@1.1.4_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "dependencies": [ + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-use-rect@1.1.4_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "dependencies": [ + "@radix-ui/rect", + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-use-size@1.1.4_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "dependencies": [ + "@radix-ui/react-use-layout-effect", + "@types/react", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "@radix-ui/react-visually-hidden@1.2.11_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "dependencies": [ + "@radix-ui/react-primitive", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "@radix-ui/rect@1.1.3": { + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==" + }, + "@react-router/dev@7.12.0_react-router@7.12.0_typescript@7.0.2_vite@7.3.6_@types+node@24.19.0": { + "integrity": "sha512-5GpwXgq4pnOVeG7l6ADkCHA1rthJus1q/A3NRYJAIypclUQDYAzg1/fDNjvaKuTSrq+Nr3u6aj2v+oC+47MX6g==", + "dependencies": [ + "@babel/core", + "@babel/generator", + "@babel/parser", + "@babel/plugin-syntax-jsx", + "@babel/preset-typescript", + "@babel/traverse", + "@babel/types", + "@react-router/node", + "@remix-run/node-fetch-server", + "arg", + "babel-dead-code-elimination", + "chokidar", + "dedent", + "es-module-lexer@1.7.0", + "exit-hook", + "isbot", + "jsesc", + "lodash", + "p-map", + "pathe@1.1.2", + "picocolors", + "pkg-types", + "prettier", + "react-refresh", + "react-router", + "semver@7.8.5", + "tinyglobby", + "typescript", + "valibot", + "vite", + "vite-node" + ], + "optionalPeers": [ + "typescript" + ], + "bin": true + }, + "@react-router/node@7.12.0_react-router@7.12.0_typescript@7.0.2_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-o/t10Cse4LK8kFefqJ8JjC6Ng6YuKD2I87S2AiJs17YAYtXU5W731ZqB73AWyCDd2G14R0dSuqXiASRNK/xLjg==", + "dependencies": [ + "@mjackson/node-fetch-server", + "react-router", + "typescript" + ], + "optionalPeers": [ + "typescript" + ] + }, + "@remix-run/node-fetch-server@0.9.0": { + "integrity": "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA==" + }, + "@rollup/rollup-android-arm-eabi@4.63.5": { + "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==", + "os": ["android"], + "cpu": ["arm"] + }, + "@rollup/rollup-android-arm64@4.63.5": { + "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-arm64@4.63.5": { + "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-x64@4.63.5": { + "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@rollup/rollup-freebsd-arm64@4.63.5": { + "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@rollup/rollup-freebsd-x64@4.63.5": { + "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-arm-gnueabihf@4.63.5": { + "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm-musleabihf@4.63.5": { + "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm64-gnu@4.63.5": { + "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-arm64-musl@4.63.5": { + "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-loong64-gnu@4.63.5": { + "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-loong64-musl@4.63.5": { + "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-ppc64-gnu@4.63.5": { + "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-ppc64-musl@4.63.5": { + "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-riscv64-gnu@4.63.5": { + "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-riscv64-musl@4.63.5": { + "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-s390x-gnu@4.63.5": { + "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@rollup/rollup-linux-x64-gnu@4.63.5": { + "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-x64-musl@4.63.5": { + "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-openbsd-x64@4.63.5": { + "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-openharmony-arm64@4.63.5": { + "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-arm64-msvc@4.63.5": { + "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-ia32-msvc@4.63.5": { + "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@rollup/rollup-win32-x64-gnu@4.63.5": { + "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@rollup/rollup-win32-x64-msvc@4.63.5": { + "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@sec-ant/readable-stream@0.4.1": { + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==" + }, + "@sindresorhus/merge-streams@4.0.0": { + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==" + }, + "@smithy/core@3.35.0": { + "integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@smithy/credential-provider-imds@4.5.2": { + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/fetch-http-handler@5.8.0": { + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/node-http-handler@4.12.1": { + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/signature-v4@5.7.4": { + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/types@4.19.0": { + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "dependencies": [ + "tslib" + ] + }, + "@stablelib/base64@1.0.1": { + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==" + }, + "@tailwindcss/node@4.3.3": { + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dependencies": [ + "@jridgewell/remapping", + "enhanced-resolve", + "jiti", + "lightningcss", + "magic-string@0.30.21", + "source-map-js", + "tailwindcss" + ] + }, + "@tailwindcss/oxide-android-arm64@4.3.3": { + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-darwin-arm64@4.3.3": { + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-darwin-x64@4.3.3": { + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide-freebsd-x64@4.3.3": { + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": { + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": { + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": { + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": { + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide-linux-x64-musl@4.3.3": { + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide-wasm32-wasi@4.3.3": { + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "cpu": ["wasm32"] + }, + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": { + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": { + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@tailwindcss/oxide@4.3.3": { + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "optionalDependencies": [ + "@tailwindcss/oxide-android-arm64", + "@tailwindcss/oxide-darwin-arm64", + "@tailwindcss/oxide-darwin-x64", + "@tailwindcss/oxide-freebsd-x64", + "@tailwindcss/oxide-linux-arm-gnueabihf", + "@tailwindcss/oxide-linux-arm64-gnu", + "@tailwindcss/oxide-linux-arm64-musl", + "@tailwindcss/oxide-linux-x64-gnu", + "@tailwindcss/oxide-linux-x64-musl", + "@tailwindcss/oxide-wasm32-wasi", + "@tailwindcss/oxide-win32-arm64-msvc", + "@tailwindcss/oxide-win32-x64-msvc" + ] + }, + "@tailwindcss/vite@4.3.3_vite@7.3.6_@types+node@24.19.0": { + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dependencies": [ + "@tailwindcss/node", + "@tailwindcss/oxide", + "tailwindcss", + "vite" + ] + }, + "@tanstack/query-core@5.104.0": { + "integrity": "sha512-JrC2r/JQlXt7khBSdUpsgxNvybzOg+aITa+ARRMlP2AFo93Y8vIqz077rp+e61mJetSZ0T6AJbwW1JHer1vrPQ==" + }, + "@tanstack/react-query@5.104.0_react@19.3.0": { + "integrity": "sha512-e1TZmDCQnWIfiDVryIHeA6Idj+Lfx1hORLOhXS/l5wcgvtVD4yYqbTDl378sGQKIi2cSgb0TEMC9cKK8GGoJEw==", + "dependencies": [ + "@tanstack/query-core", + "react" + ] + }, + "@ts-morph/common@0.27.0": { + "integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==", + "dependencies": [ + "fast-glob", + "minimatch", + "path-browserify" + ] + }, + "@types/chai@5.2.3": { + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dependencies": [ + "@types/deep-eql", + "assertion-error" + ] + }, + "@types/deep-eql@4.0.2": { + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==" + }, + "@types/estree@1.0.9": { + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" + }, + "@types/node@24.19.0": { + "integrity": "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==", + "dependencies": [ + "undici-types" + ] + }, + "@types/pg@8.23.1": { + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dependencies": [ + "@types/node", + "pg-protocol", + "pg-types" + ] + }, + "@types/react-dom@19.3.0_@types+react@19.3.0": { + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dependencies": [ + "@types/react" + ] + }, + "@types/react@19.3.0": { + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dependencies": [ + "csstype" + ] + }, + "@types/retry@0.12.0": { + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "@types/set-cookie-parser@2.4.10": { + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dependencies": [ + "@types/node" + ] + }, + "@types/statuses@2.0.6": { + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==" + }, + "@types/validate-npm-package-name@4.0.2": { + "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==" + }, + "@typescript/typescript-aix-ppc64@7.0.2": { + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@typescript/typescript-darwin-arm64@7.0.2": { + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@typescript/typescript-darwin-x64@7.0.2": { + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@typescript/typescript-freebsd-arm64@7.0.2": { + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-freebsd-x64@7.0.2": { + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-linux-arm64@7.0.2": { + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@typescript/typescript-linux-arm@7.0.2": { + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@typescript/typescript-linux-loong64@7.0.2": { + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@typescript/typescript-linux-mips64el@7.0.2": { + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@typescript/typescript-linux-ppc64@7.0.2": { + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@typescript/typescript-linux-riscv64@7.0.2": { + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@typescript/typescript-linux-s390x@7.0.2": { + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@typescript/typescript-linux-x64@7.0.2": { + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@typescript/typescript-netbsd-arm64@7.0.2": { + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-netbsd-x64@7.0.2": { + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-openbsd-arm64@7.0.2": { + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-openbsd-x64@7.0.2": { + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-sunos-x64@7.0.2": { + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@typescript/typescript-win32-arm64@7.0.2": { + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@typescript/typescript-win32-x64@7.0.2": { + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@vitest/mocker@5.0.2_vite@7.3.6_@types+node@24.19.0": { + "integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==", + "dependencies": [ + "@jridgewell/trace-mapping", + "@vitest/spy", + "estree-walker", + "magic-string@1.4.2", + "vite" + ], + "optionalPeers": [ + "vite" + ] + }, + "@vitest/spy@5.0.2": { + "integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==" + }, + "accepts@2.0.0": { + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": [ + "mime-types", + "negotiator" + ] + }, + "agent-base@7.1.4": { + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==" + }, + "agent-base@9.0.0": { + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==" + }, + "ajv-formats@2.1.1_ajv@8.20.0": { + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": [ + "ajv" + ], + "optionalPeers": [ + "ajv" + ] + }, + "ajv-formats@3.0.1_ajv@8.20.0": { + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": [ + "ajv" + ], + "optionalPeers": [ + "ajv" + ] + }, + "ajv@8.20.0": { + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dependencies": [ + "fast-deep-equal", + "fast-uri", + "json-schema-traverse", + "require-from-string" + ] + }, + "ansi-colors@4.1.3": { + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" + }, + "ansi-regex@5.0.1": { + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-regex@6.3.0": { + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==" + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert" + ] + }, + "ansis@4.4.0": { + "integrity": "sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==" + }, + "arg@5.0.2": { + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "aria-hidden@1.2.6": { + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dependencies": [ + "tslib" + ] + }, + "assertion-error@2.0.1": { + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==" + }, + "ast-types@0.16.3": { + "integrity": "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==", + "dependencies": [ + "tslib" + ] + }, + "atomic-sleep@1.0.0": { + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, + "atomically@1.7.0": { + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==" + }, + "babel-dead-code-elimination@1.0.12": { + "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "dependencies": [ + "@babel/core", + "@babel/parser", + "@babel/traverse", + "@babel/types" + ] + }, + "balanced-match@4.0.4": { + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "baseline-browser-mapping@2.11.26": { + "integrity": "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==", + "bin": true + }, + "bignumber.js@9.3.1": { + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==" + }, + "body-parser@2.3.0": { + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dependencies": [ + "bytes", + "content-type@2.1.0", + "debug", + "http-errors", + "iconv-lite", + "on-finished", + "qs", + "raw-body", + "type-is" + ] + }, + "bowser@2.14.1": { + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + }, + "brace-expansion@5.0.12": { + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dependencies": [ + "balanced-match" + ] + }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": [ + "fill-range" + ] + }, + "browserslist@4.29.1": { + "integrity": "sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw==", + "dependencies": [ + "baseline-browser-mapping", + "caniuse-lite", + "electron-to-chromium", + "node-releases", + "update-browserslist-db" + ], + "bin": true + }, + "buffer-equal-constant-time@1.0.1": { + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "bundle-name@4.1.1": { + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "dependencies": [ + "run-applescript" + ] + }, + "bytes@3.1.2": { + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cac@6.7.14": { + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" + }, + "call-bind-apply-helpers@1.0.2": { + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": [ + "es-errors", + "function-bind" + ] + }, + "call-bound@1.0.4": { + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": [ + "call-bind-apply-helpers", + "get-intrinsic" + ] + }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "caniuse-lite@1.0.30001812": { + "integrity": "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng==" + }, + "chai@6.2.2": { + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==" + }, + "chalk@5.6.2": { + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" + }, + "chokidar@4.0.3": { + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": [ + "readdirp" + ] + }, + "class-variance-authority@0.7.1": { + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dependencies": [ + "clsx" + ] + }, + "cli-cursor@5.0.0": { + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dependencies": [ + "restore-cursor" + ] + }, + "cli-spinners@2.9.2": { + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==" + }, + "cli-width@4.1.0": { + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==" + }, + "cliui@8.0.1": { + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": [ + "string-width@4.2.3", + "strip-ansi@6.0.1", + "wrap-ansi" + ] + }, + "clsx@2.1.1": { + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "code-block-writer@13.0.3": { + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==" + }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "colorette@2.0.20": { + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, + "commander@11.1.0": { + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + }, + "commander@14.0.3": { + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==" + }, + "conf@10.2.0": { + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "dependencies": [ + "ajv", + "ajv-formats@2.1.1_ajv@8.20.0", + "atomically", + "debounce-fn", + "dot-prop", + "env-paths", + "json-schema-typed@7.0.3", + "onetime@5.1.2", + "pkg-up", + "semver@7.8.5" + ] + }, + "confbox@0.3.1": { + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==" + }, + "content-disposition@1.1.0": { + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==" + }, + "content-type@1.0.5": { + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "content-type@2.1.0": { + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==" + }, + "convert-source-map@2.0.0": { + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "cookie-signature@1.2.2": { + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, + "cookie@0.7.2": { + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie@1.1.1": { + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==" + }, + "cors@2.8.6": { + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": [ + "object-assign", + "vary" + ] + }, + "cosmiconfig@9.0.2_typescript@7.0.2": { + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dependencies": [ + "env-paths", + "import-fresh", + "js-yaml", + "parse-json", + "typescript" + ], + "optionalPeers": [ + "typescript" + ] + }, + "cross-spawn@7.0.6": { + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": [ + "path-key@3.1.1", + "shebang-command", + "which@2.0.2" + ] + }, + "cssesc@3.0.0": { + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": true + }, + "csstype@3.2.3": { + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "data-uri-to-buffer@4.0.1": { + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, + "date-fns@4.4.0": { + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==" + }, + "dateformat@4.6.3": { + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==" + }, + "debounce-fn@4.0.0": { + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dependencies": [ + "mimic-fn@3.1.0" + ] + }, + "debug@4.4.3": { + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": [ + "ms" + ] + }, + "dedent@1.7.2": { + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==" + }, + "deepmerge@4.3.1": { + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, + "default-browser-id@5.0.1": { + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==" + }, + "default-browser@5.5.1": { + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dependencies": [ + "bundle-name", + "default-browser-id" + ] + }, + "define-lazy-prop@2.0.0": { + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" + }, + "define-lazy-prop@3.0.0": { + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==" + }, + "depd@2.0.0": { + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "detect-libc@2.1.2": { + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + }, + "detect-node-es@1.1.0": { + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "diff@8.0.4": { + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==" + }, + "dot-prop@6.0.1": { + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dependencies": [ + "is-obj@2.0.0" + ] + }, + "dotenv@17.4.2": { + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==" + }, + "dunder-proto@1.0.1": { + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": [ + "call-bind-apply-helpers", + "es-errors", + "gopd" + ] + }, + "ecdsa-sig-formatter@1.0.11": { + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": [ + "safe-buffer" + ] + }, + "ee-first@1.1.1": { + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "electron-to-chromium@1.5.439": { + "integrity": "sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==" + }, + "emoji-regex@10.6.0": { + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" + }, + "emoji-regex@8.0.0": { + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl@2.0.0": { + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "end-of-stream@1.4.5": { + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": [ + "once" + ] + }, + "enhanced-resolve@5.25.1": { + "integrity": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "dependencies": [ + "graceful-fs", + "tapable" + ] + }, + "enquirer@2.4.1": { + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dependencies": [ + "ansi-colors", + "strip-ansi@6.0.1" + ] + }, + "env-paths@2.2.1": { + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, + "error-ex@1.3.4": { + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dependencies": [ + "is-arrayish" + ] + }, + "es-define-property@1.0.1": { + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors@1.3.0": { + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-module-lexer@1.7.0": { + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==" + }, + "es-module-lexer@2.3.2": { + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==" + }, + "es-object-atoms@1.1.2": { + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": [ + "es-errors" + ] + }, + "esbuild@0.28.2": { + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "optionalDependencies": [ + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/openharmony-arm64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" + ], + "scripts": true, + "bin": true + }, + "escalade@3.2.0": { + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "escape-html@1.0.3": { + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "esprima@4.0.1": { + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": true + }, + "estree-walker@3.0.3": { + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": [ + "@types/estree" + ] + }, + "etag@1.8.1": { + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventsource-parser@3.1.1": { + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==" + }, + "eventsource@3.0.7": { + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dependencies": [ + "eventsource-parser" + ] + }, + "execa@5.1.1": { + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": [ + "cross-spawn", + "get-stream@6.0.1", + "human-signals@2.1.0", + "is-stream@2.0.1", + "merge-stream", + "npm-run-path@4.0.1", + "onetime@5.1.2", + "signal-exit@3.0.7", + "strip-final-newline@2.0.0" + ] + }, + "execa@9.6.1": { + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dependencies": [ + "@sindresorhus/merge-streams", + "cross-spawn", + "figures", + "get-stream@9.0.1", + "human-signals@8.0.1", + "is-plain-obj", + "is-stream@4.0.1", + "npm-run-path@6.0.0", + "pretty-ms", + "signal-exit@4.1.0", + "strip-final-newline@4.0.0", + "yoctocolors" + ] + }, + "exit-hook@2.2.1": { + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==" + }, + "expect-type@1.4.0": { + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==" + }, + "express-rate-limit@8.7.0_express@5.2.1": { + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", + "dependencies": [ + "debug", + "express", + "ip-address" + ] + }, + "express@5.2.1": { + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": [ + "accepts", + "body-parser", + "content-disposition", + "content-type@1.0.5", + "cookie@0.7.2", + "cookie-signature", + "debug", + "depd", + "encodeurl", + "escape-html", + "etag", + "finalhandler", + "fresh", + "http-errors", + "merge-descriptors", + "mime-types", + "on-finished", + "once", + "parseurl", + "proxy-addr", + "qs", + "range-parser", + "router", + "send", + "serve-static", + "statuses", + "type-is", + "vary" + ] + }, + "exsolve@1.1.1": { + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==" + }, + "extend@3.0.2": { + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-copy@4.1.1": { + "integrity": "sha512-A4QTJmuiztpGtr6AMeJts9R4hbj2ZBUwtOaKrG6rw2y7t6+IaJKjz5M3XDs8BUznxDH43FVc6A0y/gWlMl4UtA==" + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob@3.3.3": { + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": [ + "@nodelib/fs.stat", + "@nodelib/fs.walk", + "glob-parent", + "merge2", + "micromatch" + ] + }, + "fast-safe-stringify@2.1.1": { + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "fast-sha256@1.3.0": { + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==" + }, + "fast-string-truncated-width@3.0.3": { + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==" + }, + "fast-string-width@3.0.2": { + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dependencies": [ + "fast-string-truncated-width" + ] + }, + "fast-uri@3.1.8": { + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==" + }, + "fast-wrap-ansi@0.2.2": { + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dependencies": [ + "fast-string-width" + ] + }, + "fastq@1.20.3": { + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dependencies": [ + "reusify" + ] + }, + "fdir@6.5.0_picomatch@4.0.7": { + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dependencies": [ + "picomatch@4.0.7" + ], + "optionalPeers": [ + "picomatch@4.0.7" + ] + }, + "fetch-blob@3.2.0": { + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dependencies": [ + "node-domexception", + "web-streams-polyfill" + ] + }, + "figures@6.1.0": { + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dependencies": [ + "is-unicode-supported@2.1.0" + ] + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": [ + "to-regex-range" + ] + }, + "finalhandler@2.1.1": { + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": [ + "debug", + "encodeurl", + "escape-html", + "on-finished", + "parseurl", + "statuses" + ] + }, + "find-up@3.0.0": { + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": [ + "locate-path" + ] + }, + "formdata-polyfill@4.0.10": { + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": [ + "fetch-blob" + ] + }, + "forwarded@0.2.0": { + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh@2.0.0": { + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, + "fs-extra@11.4.1": { + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dependencies": [ + "graceful-fs", + "jsonfile", + "universalify" + ] + }, + "fsevents@2.3.3": { + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "os": ["darwin"], + "scripts": true + }, + "function-bind@1.1.2": { + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "fuzzysort@3.1.0": { + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==" + }, + "fzf@0.5.2": { + "integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==" + }, + "gaxios@7.3.1": { + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "dependencies": [ + "extend", + "https-proxy-agent@7.0.6", + "node-fetch" + ] + }, + "gcp-metadata@8.1.2": { + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dependencies": [ + "gaxios", + "google-logging-utils", + "json-bigint" + ] + }, + "gensync@1.0.0-beta.2": { + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file@2.0.5": { + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-east-asian-width@1.7.0": { + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==" + }, + "get-intrinsic@1.3.0": { + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": [ + "call-bind-apply-helpers", + "es-define-property", + "es-errors", + "es-object-atoms", + "function-bind", + "get-proto", + "gopd", + "has-symbols", + "hasown", + "math-intrinsics" + ] + }, + "get-nonce@1.0.1": { + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" + }, + "get-own-enumerable-keys@1.0.0": { + "integrity": "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==" + }, + "get-proto@1.0.1": { + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": [ + "dunder-proto", + "es-object-atoms" + ] + }, + "get-stream@6.0.1": { + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "get-stream@9.0.1": { + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dependencies": [ + "@sec-ant/readable-stream", + "is-stream@4.0.1" + ] + }, + "glob-parent@5.1.2": { + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": [ + "is-glob" + ] + }, + "globrex@0.1.2": { + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, + "google-auth-library@10.9.1": { + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "dependencies": [ + "base64-js", + "ecdsa-sig-formatter", + "gaxios", + "gcp-metadata", + "google-logging-utils", + "jws" + ] + }, + "google-logging-utils@1.1.3": { + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==" + }, + "gopd@1.2.0": { + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "graceful-fs@4.2.11": { + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "graphql@16.14.2": { + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==" + }, + "has-symbols@1.1.0": { + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "hasown@2.0.4": { + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": [ + "function-bind" + ] + }, + "headers-polyfill@5.0.1": { + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dependencies": [ + "@types/set-cookie-parser", + "set-cookie-parser@3.1.2" + ] + }, + "help-me@5.0.0": { + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "hono@4.13.9": { + "integrity": "sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg==" + }, + "http-errors@2.0.1": { + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": [ + "depd", + "inherits", + "setprototypeof", + "statuses", + "toidentifier" + ] + }, + "http-proxy-agent@9.1.0": { + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "dependencies": [ + "agent-base@9.0.0", + "debug", + "proxy-agent-negotiate" + ] + }, + "https-proxy-agent@7.0.6": { + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": [ + "agent-base@7.1.4", + "debug" + ] + }, + "https-proxy-agent@9.1.0": { + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dependencies": [ + "agent-base@9.0.0", + "debug", + "proxy-agent-negotiate" + ] + }, + "human-signals@2.1.0": { + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" + }, + "human-signals@8.0.1": { + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==" + }, + "iconv-lite@0.7.3": { + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dependencies": [ + "safer-buffer" + ] + }, + "ignore@5.3.2": { + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + }, + "ignore@7.0.8": { + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==" + }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from" + ] + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip-address@10.7.2": { + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==" + }, + "ipaddr.js@1.9.1": { + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-arrayish@0.2.1": { + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-docker@2.2.1": { + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": true + }, + "is-docker@3.0.0": { + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "bin": true + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point@3.0.0": { + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-in-ssh@1.0.0": { + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==" + }, + "is-inside-container@1.0.0": { + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dependencies": [ + "is-docker@3.0.0" + ], + "bin": true + }, + "is-interactive@2.0.0": { + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==" + }, + "is-node-process@1.2.0": { + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==" + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj@2.0.0": { + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-obj@3.0.0": { + "integrity": "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==" + }, + "is-plain-obj@4.1.0": { + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==" + }, + "is-promise@4.0.0": { + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "is-regexp@3.1.0": { + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==" + }, + "is-stream@2.0.1": { + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-stream@4.0.1": { + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==" + }, + "is-unicode-supported@1.3.0": { + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==" + }, + "is-unicode-supported@2.1.0": { + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==" + }, + "is-wsl@2.2.0": { + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": [ + "is-docker@2.2.1" + ] + }, + "is-wsl@3.1.1": { + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dependencies": [ + "is-inside-container" + ] + }, + "isbot@5.2.2": { + "integrity": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==" + }, + "isexe@2.0.0": { + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "isexe@3.1.5": { + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==" + }, + "jiti@2.7.0": { + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "bin": true + }, + "jose@6.2.12": { + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==" + }, + "joycon@3.1.1": { + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml@4.3.2": { + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dependencies": [ + "argparse" + ], + "bin": true + }, + "jsesc@3.0.2": { + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "bin": true + }, + "json-bigint@1.0.0": { + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": [ + "bignumber.js" + ] + }, + "json-parse-even-better-errors@2.3.1": { + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json-schema-to-ts@3.1.1": { + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dependencies": [ + "@babel/runtime", + "ts-algebra" + ] + }, + "json-schema-traverse@1.0.0": { + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "json-schema-typed@7.0.3": { + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, + "json-schema-typed@8.0.2": { + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==" + }, + "json5@2.2.3": { + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": true + }, + "jsonfile@6.2.1": { + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dependencies": [ + "universalify" + ], + "optionalDependencies": [ + "graceful-fs" + ] + }, + "jwa@2.0.1": { + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": [ + "buffer-equal-constant-time", + "ecdsa-sig-formatter", + "safe-buffer" + ] + }, + "jws@4.0.1": { + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": [ + "jwa", + "safe-buffer" + ] + }, + "kleur@3.0.3": { + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "kleur@4.1.5": { + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" + }, + "kysely-postgres-js@5.0.1_kysely@0.29.6_postgres@3.4.9": { + "integrity": "sha512-3qY2qzXJszS35CMuknpon79CE5G7MfcTMoIQHuFmGsaQQ2rau3rwx7eNaRJkO9nhli8yQedQroBZ/VehzNJy4w==", + "dependencies": [ + "kysely", + "postgres" + ], + "optionalPeers": [ + "postgres" + ] + }, + "kysely@0.29.6": { + "integrity": "sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg==" + }, + "lightningcss-android-arm64@1.32.0": { + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "os": ["android"], + "cpu": ["arm64"] + }, + "lightningcss-darwin-arm64@1.32.0": { + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "lightningcss-darwin-x64@1.32.0": { + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "lightningcss-freebsd-x64@1.32.0": { + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "lightningcss-linux-arm-gnueabihf@1.32.0": { + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "lightningcss-linux-arm64-gnu@1.32.0": { + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "lightningcss-linux-arm64-musl@1.32.0": { + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "lightningcss-linux-x64-gnu@1.32.0": { + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "lightningcss-linux-x64-musl@1.32.0": { + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "lightningcss-win32-arm64-msvc@1.32.0": { + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "lightningcss-win32-x64-msvc@1.32.0": { + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "os": ["win32"], + "cpu": ["x64"] + }, + "lightningcss@1.32.0": { + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dependencies": [ + "detect-libc" + ], + "optionalDependencies": [ + "lightningcss-android-arm64", + "lightningcss-darwin-arm64", + "lightningcss-darwin-x64", + "lightningcss-freebsd-x64", + "lightningcss-linux-arm-gnueabihf", + "lightningcss-linux-arm64-gnu", + "lightningcss-linux-arm64-musl", + "lightningcss-linux-x64-gnu", + "lightningcss-linux-x64-musl", + "lightningcss-win32-arm64-msvc", + "lightningcss-win32-x64-msvc" + ] + }, + "lines-and-columns@1.2.4": { + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "locate-path@3.0.0": { + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": [ + "p-locate", + "path-exists" + ] + }, + "lodash@4.18.1": { + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "log-symbols@6.0.0": { + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dependencies": [ + "chalk", + "is-unicode-supported@1.3.0" + ] + }, + "long@5.3.2": { + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "lru-cache@5.1.1": { + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": [ + "yallist" + ] + }, + "lucide-react@0.576.0_react@19.3.0": { + "integrity": "sha512-koNxU14BXrxUfZQ9cUaP0ES1uyPZKYDjk31FQZB6dQ/x+tXk979sVAn9ppZ/pVeJJyOxVM8j1E+8QEuSc02Vug==", + "dependencies": [ + "react" + ] + }, + "magic-string@0.30.21": { + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "magic-string@1.4.2": { + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "math-intrinsics@1.1.0": { + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer@1.1.1": { + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==" + }, + "merge-descriptors@2.0.0": { + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" + }, + "merge-stream@2.0.0": { + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "merge2@1.4.1": { + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, + "micromatch@4.0.8": { + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": [ + "braces", + "picomatch@2.3.2" + ] + }, + "mime-db@1.54.0": { + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types@3.0.2": { + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": [ + "mime-db" + ] + }, + "mimic-fn@2.1.0": { + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "mimic-fn@3.1.0": { + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==" + }, + "mimic-function@5.0.1": { + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" + }, + "minimatch@10.2.6": { + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dependencies": [ + "brace-expansion" + ] + }, + "minimist@1.2.8": { + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "msw@2.15.0_typescript@7.0.2_@types+node@24.19.0": { + "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==", + "dependencies": [ + "@inquirer/confirm", + "@mswjs/interceptors", + "@open-draft/deferred-promise@3.0.0", + "@types/statuses", + "cookie@1.1.1", + "graphql", + "headers-polyfill", + "is-node-process", + "outvariant", + "path-to-regexp@6.3.0", + "picocolors", + "rettime", + "statuses", + "strict-event-emitter", + "tough-cookie", + "type-fest", + "typescript", + "until-async", + "yargs" + ], + "optionalPeers": [ + "typescript" + ], + "scripts": true, + "bin": true + }, + "mute-stream@3.0.0": { + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==" + }, + "nanoid@3.3.19": { + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "bin": true + }, + "negotiator@1.1.0": { + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dependencies": [ + "content-type@2.1.0" + ] + }, + "next-themes@0.4.6_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "dependencies": [ + "react", + "react-dom" + ] + }, + "node-domexception@1.0.0": { + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": true + }, + "node-fetch@3.3.2": { + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": [ + "data-uri-to-buffer", + "fetch-blob", + "formdata-polyfill" + ] + }, + "node-releases@2.0.57": { + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==" + }, + "npm-run-path@4.0.1": { + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": [ + "path-key@3.1.1" + ] + }, + "npm-run-path@6.0.0": { + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dependencies": [ + "path-key@4.0.0", + "unicorn-magic" + ] + }, + "object-assign@4.1.1": { + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect@1.13.4": { + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + }, + "object-treeify@1.1.33": { + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==" + }, + "obug@2.2.1": { + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==" + }, + "on-exit-leak-free@2.1.2": { + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" + }, + "on-finished@2.4.1": { + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": [ + "ee-first" + ] + }, + "once@1.4.0": { + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": [ + "wrappy" + ] + }, + "onetime@5.1.2": { + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": [ + "mimic-fn@2.1.0" + ] + }, + "onetime@7.0.0": { + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dependencies": [ + "mimic-function" + ] + }, + "open@11.0.4": { + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", + "dependencies": [ + "default-browser", + "define-lazy-prop@3.0.0", + "is-in-ssh", + "is-inside-container", + "powershell-utils@0.2.1", + "wsl-utils" + ] + }, + "open@8.4.2": { + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": [ + "define-lazy-prop@2.0.0", + "is-docker@2.2.1", + "is-wsl@2.2.0" + ] + }, + "openai@6.40.0_zod@4.6.5": { + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", + "dependencies": [ + "zod@4.6.5" + ], + "optionalPeers": [ + "zod@4.6.5" + ] + }, + "openworkflow@0.10.1_postgres@3.4.9": { + "integrity": "sha512-BdgEjoaazhL26JgQ/SehD/tLVEUYsHD6oZWU79+04jnxc+hPlxmRRfVfaAGeUdS2YEPFr/53C0kcRNodzAdNnA==", + "dependencies": [ + "postgres" + ], + "optionalPeers": [ + "postgres" + ] + }, + "ora@8.2.0": { + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dependencies": [ + "chalk", + "cli-cursor", + "cli-spinners", + "is-interactive", + "is-unicode-supported@2.1.0", + "log-symbols", + "stdin-discarder", + "string-width@7.2.0", + "strip-ansi@7.2.0" + ] + }, + "outvariant@1.4.3": { + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==" + }, + "p-limit@2.3.0": { + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": [ + "p-try" + ] + }, + "p-locate@3.0.0": { + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": [ + "p-limit" + ] + }, + "p-map@7.0.8": { + "integrity": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==" + }, + "p-retry@4.6.2": { + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": [ + "@types/retry", + "retry" + ] + }, + "p-try@2.2.0": { + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "package-manager-detector@1.8.0": { + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==" + }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, + "parse-json@5.2.0": { + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": [ + "@babel/code-frame", + "error-ex", + "json-parse-even-better-errors", + "lines-and-columns" + ] + }, + "parse-ms@4.0.0": { + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==" + }, + "parseurl@1.3.3": { + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "partial-json@0.1.7": { + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==" + }, + "path-browserify@1.0.1": { + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "path-exists@3.0.0": { + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + }, + "path-key@3.1.1": { + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-key@4.0.0": { + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==" + }, + "path-to-regexp@6.3.0": { + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + }, + "path-to-regexp@8.4.2": { + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==" + }, + "pathe@1.1.2": { + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" + }, + "pathe@2.0.3": { + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" + }, + "pg-cloudflare@1.4.0": { + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==" + }, + "pg-connection-string@2.14.0": { + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==" + }, + "pg-int8@1.0.1": { + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-pool@3.14.0_pg@8.23.0": { + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "dependencies": [ + "pg" + ] + }, + "pg-protocol@1.16.0": { + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==" + }, + "pg-types@2.2.0": { + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": [ + "pg-int8", + "postgres-array", + "postgres-bytea", + "postgres-date", + "postgres-interval" + ] + }, + "pg@8.23.0": { + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "dependencies": [ + "pg-connection-string", + "pg-pool", + "pg-protocol", + "pg-types", + "pgpass" + ], + "optionalDependencies": [ + "pg-cloudflare" + ] + }, + "pgpass@1.0.5": { + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": [ + "split2" + ] + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@2.3.2": { + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==" + }, + "picomatch@4.0.7": { + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==" + }, + "pino-abstract-transport@3.0.0": { + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dependencies": [ + "split2" + ] + }, + "pino-pretty@13.1.3": { + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dependencies": [ + "colorette", + "dateformat", + "fast-copy", + "fast-safe-stringify", + "help-me", + "joycon", + "minimist", + "on-exit-leak-free", + "pino-abstract-transport", + "pump", + "secure-json-parse", + "sonic-boom", + "strip-json-comments" + ], + "bin": true + }, + "pino-std-serializers@7.1.0": { + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" + }, + "pino@10.3.1": { + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dependencies": [ + "@pinojs/redact", + "atomic-sleep", + "on-exit-leak-free", + "pino-abstract-transport", + "pino-std-serializers", + "process-warning", + "quick-format-unescaped", + "real-require@0.2.0", + "safe-stable-stringify", + "sonic-boom", + "thread-stream" + ], + "bin": true + }, + "pkce-challenge@5.0.1": { + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==" + }, + "pkg-types@2.3.3": { + "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "dependencies": [ + "confbox", + "exsolve", + "pathe@2.0.3" + ] + }, + "pkg-up@3.1.0": { + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": [ + "find-up" + ] + }, + "postcss-selector-parser@7.1.6": { + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", + "dependencies": [ + "cssesc", + "util-deprecate" + ] + }, + "postcss@8.5.28": { + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "postgres-array@2.0.0": { + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea@1.0.1": { + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==" + }, + "postgres-date@1.0.7": { + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval@1.2.0": { + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": [ + "xtend" + ] + }, + "postgres@3.4.9": { + "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==" + }, + "powershell-utils@0.1.0": { + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==" + }, + "powershell-utils@0.2.1": { + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==" + }, + "prettier@3.9.9": { + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "bin": true + }, + "pretty-ms@9.3.1": { + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "dependencies": [ + "parse-ms" + ] + }, + "process-warning@5.1.0": { + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==" + }, + "prompts@2.4.2": { + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": [ + "kleur@3.0.3", + "sisteransi" + ] + }, + "protobufjs@7.6.6": { + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "dependencies": [ + "@protobufjs/aspromise", + "@protobufjs/base64", + "@protobufjs/codegen", + "@protobufjs/eventemitter", + "@protobufjs/fetch", + "@protobufjs/float", + "@protobufjs/path", + "@protobufjs/pool", + "@protobufjs/utf8", + "@types/node", + "long" + ], + "scripts": true + }, + "proxy-addr@2.0.8": { + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "dependencies": [ + "forwarded", + "ipaddr.js" + ] + }, + "proxy-agent-negotiate@1.1.0": { + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==" + }, + "pump@3.0.4": { + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dependencies": [ + "end-of-stream", + "once" + ] + }, + "qs@6.16.0": { + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "dependencies": [ + "es-define-property", + "side-channel" + ] + }, + "queue-microtask@1.2.3": { + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "quick-format-unescaped@4.0.4": { + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "radix-ui@1.6.7_@types+react@19.3.0_@types+react-dom@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-QBdhh1arIEUvPC0dQ5+nwWAxt7+N+oP/9jPwjJkGFoSk/sqxg32gJtSXGtFh8frAIcS6oC9cx2Q+7KYCQLOAeA==", + "dependencies": [ + "@radix-ui/primitive", + "@radix-ui/react-accessible-icon", + "@radix-ui/react-accordion", + "@radix-ui/react-alert-dialog", + "@radix-ui/react-arrow", + "@radix-ui/react-aspect-ratio", + "@radix-ui/react-avatar", + "@radix-ui/react-checkbox", + "@radix-ui/react-collapsible", + "@radix-ui/react-collection", + "@radix-ui/react-compose-refs", + "@radix-ui/react-context", + "@radix-ui/react-context-menu", + "@radix-ui/react-dialog", + "@radix-ui/react-direction", + "@radix-ui/react-dismissable-layer", + "@radix-ui/react-dropdown-menu", + "@radix-ui/react-focus-guards", + "@radix-ui/react-focus-scope", + "@radix-ui/react-form", + "@radix-ui/react-hover-card", + "@radix-ui/react-label", + "@radix-ui/react-menu", + "@radix-ui/react-menubar", + "@radix-ui/react-navigation-menu", + "@radix-ui/react-one-time-password-field", + "@radix-ui/react-password-toggle-field", + "@radix-ui/react-popover", + "@radix-ui/react-popper", + "@radix-ui/react-portal", + "@radix-ui/react-presence", + "@radix-ui/react-primitive", + "@radix-ui/react-progress", + "@radix-ui/react-radio-group", + "@radix-ui/react-roving-focus", + "@radix-ui/react-scroll-area", + "@radix-ui/react-select", + "@radix-ui/react-separator", + "@radix-ui/react-slider", + "@radix-ui/react-slot", + "@radix-ui/react-switch", + "@radix-ui/react-tabs", + "@radix-ui/react-toast", + "@radix-ui/react-toggle", + "@radix-ui/react-toggle-group", + "@radix-ui/react-toolbar", + "@radix-ui/react-tooltip", + "@radix-ui/react-use-callback-ref", + "@radix-ui/react-use-controllable-state", + "@radix-ui/react-use-effect-event", + "@radix-ui/react-use-escape-keydown", + "@radix-ui/react-use-is-hydrated", + "@radix-ui/react-use-layout-effect", + "@radix-ui/react-use-size", + "@radix-ui/react-visually-hidden", + "@types/react", + "@types/react-dom", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react", + "@types/react-dom" + ] + }, + "range-parser@1.3.0": { + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==" + }, + "raw-body@3.0.2": { + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": [ + "bytes", + "http-errors", + "iconv-lite", + "unpipe" + ] + }, + "react-day-picker@10.0.1_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==", + "dependencies": [ + "@date-fns/tz", + "@types/react", + "date-fns", + "react" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "react-dom@19.3.0_react@19.3.0": { + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dependencies": [ + "react", + "scheduler" + ] + }, + "react-refresh@0.14.2": { + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==" + }, + "react-remove-scroll-bar@2.3.8_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": [ + "@types/react", + "react", + "react-style-singleton", + "tslib" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "react-remove-scroll@2.7.2_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "dependencies": [ + "@types/react", + "react", + "react-remove-scroll-bar", + "react-style-singleton", + "tslib", + "use-callback-ref", + "use-sidecar" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "react-router@7.12.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "dependencies": [ + "cookie@1.1.1", + "react", + "react-dom", + "set-cookie-parser@2.7.2" + ], + "optionalPeers": [ + "react-dom" + ] + }, + "react-style-singleton@2.2.3_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": [ + "@types/react", + "get-nonce", + "react", + "tslib" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "react@19.3.0": { + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==" + }, + "readdirp@4.1.2": { + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + }, + "real-require@0.2.0": { + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, + "real-require@1.0.0": { + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==" + }, + "recast@0.23.21": { + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "dependencies": [ + "ast-types", + "esprima", + "source-map", + "tiny-invariant", + "tslib" + ] + }, + "require-directory@2.1.1": { + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-from-string@2.0.2": { + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "restore-cursor@5.1.0": { + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dependencies": [ + "onetime@7.0.0", + "signal-exit@4.1.0" + ] + }, + "retry@0.13.1": { + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + }, + "rettime@0.11.12": { + "integrity": "sha512-5ExtR1ppiDBQkVoPw5NAqdbfH4Iq2VOmQKHMtUoaFxA9bo2krzFYBtyq6K40xxbYp+2w/YOr6D+1ToF6tTN53A==" + }, + "reusify@1.1.0": { + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" + }, + "rollup@4.63.5": { + "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", + "dependencies": [ + "@types/estree" + ], + "optionalDependencies": [ + "@napi-rs/lzma-linux-x64-gnu", + "@rollup/rollup-android-arm-eabi", + "@rollup/rollup-android-arm64", + "@rollup/rollup-darwin-arm64", + "@rollup/rollup-darwin-x64", + "@rollup/rollup-freebsd-arm64", + "@rollup/rollup-freebsd-x64", + "@rollup/rollup-linux-arm-gnueabihf", + "@rollup/rollup-linux-arm-musleabihf", + "@rollup/rollup-linux-arm64-gnu", + "@rollup/rollup-linux-arm64-musl", + "@rollup/rollup-linux-loong64-gnu", + "@rollup/rollup-linux-loong64-musl", + "@rollup/rollup-linux-ppc64-gnu", + "@rollup/rollup-linux-ppc64-musl", + "@rollup/rollup-linux-riscv64-gnu", + "@rollup/rollup-linux-riscv64-musl", + "@rollup/rollup-linux-s390x-gnu", + "@rollup/rollup-linux-x64-gnu", + "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-openbsd-x64", + "@rollup/rollup-openharmony-arm64", + "@rollup/rollup-win32-arm64-msvc", + "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-gnu", + "@rollup/rollup-win32-x64-msvc", + "fsevents" + ], + "bin": true + }, + "router@2.2.0": { + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": [ + "debug", + "depd", + "is-promise", + "parseurl", + "path-to-regexp@8.4.2" + ] + }, + "run-applescript@7.1.0": { + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==" + }, + "run-parallel@1.2.0": { + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dependencies": [ + "queue-microtask" + ] + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-stable-stringify@2.5.0": { + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" + }, + "safer-buffer@2.1.2": { + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "scheduler@0.28.0": { + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==" + }, + "secure-json-parse@4.1.0": { + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==" + }, + "semver@6.3.1": { + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": true + }, + "semver@7.8.5": { + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "bin": true + }, + "send@1.2.1": { + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": [ + "debug", + "encodeurl", + "escape-html", + "etag", + "fresh", + "http-errors", + "mime-types", + "ms", + "on-finished", + "range-parser", + "statuses" + ] + }, + "serve-static@2.2.1": { + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dependencies": [ + "encodeurl", + "escape-html", + "parseurl", + "send" + ] + }, + "set-cookie-parser@2.7.2": { + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "set-cookie-parser@3.1.2": { + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==" + }, + "setprototypeof@1.2.0": { + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shadcn@3.8.5_@types+node@24.19.0": { + "integrity": "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA==", + "dependencies": [ + "@antfu/ni", + "@babel/core", + "@babel/parser", + "@babel/plugin-transform-typescript", + "@babel/preset-typescript", + "@dotenvx/dotenvx", + "@modelcontextprotocol/sdk", + "@types/validate-npm-package-name", + "browserslist", + "commander@14.0.3", + "cosmiconfig", + "dedent", + "deepmerge", + "diff", + "execa@9.6.1", + "fast-glob", + "fs-extra", + "fuzzysort", + "https-proxy-agent@7.0.6", + "kleur@4.1.5", + "msw", + "node-fetch", + "open@11.0.4", + "ora", + "postcss", + "postcss-selector-parser", + "prompts", + "recast", + "stringify-object", + "tailwind-merge", + "ts-morph", + "tsconfig-paths", + "validate-npm-package-name", + "zod@3.25.76", + "zod-to-json-schema" + ], + "bin": true + }, + "shebang-command@2.0.0": { + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": [ + "shebang-regex" + ] + }, + "shebang-regex@3.0.0": { + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "side-channel-list@1.0.1": { + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": [ + "es-errors", + "object-inspect" + ] + }, + "side-channel-map@1.0.1": { + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": [ + "call-bound", + "es-errors", + "get-intrinsic", + "object-inspect" + ] + }, + "side-channel-weakmap@1.0.2": { + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": [ + "call-bound", + "es-errors", + "get-intrinsic", + "object-inspect", + "side-channel-map" + ] + }, + "side-channel@1.1.1": { + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dependencies": [ + "es-errors", + "object-inspect", + "side-channel-list", + "side-channel-map", + "side-channel-weakmap" + ] + }, + "signal-exit@3.0.7": { + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "signal-exit@4.1.0": { + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, + "sisteransi@1.0.5": { + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "sonic-boom@4.2.1": { + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dependencies": [ + "atomic-sleep" + ] + }, + "sonner@2.0.8_@types+react@19.3.0_react@19.3.0_react-dom@19.3.0": { + "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==", + "dependencies": [ + "@types/react", + "react", + "react-dom" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "source-map@0.6.1": { + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "split2@4.2.0": { + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, + "standardwebhooks@1.1.1": { + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "dependencies": [ + "@stablelib/base64", + "fast-sha256" + ] + }, + "statuses@2.0.2": { + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, + "std-env@4.2.0": { + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==" + }, + "stdin-discarder@0.2.2": { + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==" + }, + "strict-event-emitter@0.5.1": { + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==" + }, + "string-width@4.2.3": { + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": [ + "emoji-regex@8.0.0", + "is-fullwidth-code-point", + "strip-ansi@6.0.1" + ] + }, + "string-width@7.2.0": { + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dependencies": [ + "emoji-regex@10.6.0", + "get-east-asian-width", + "strip-ansi@7.2.0" + ] + }, + "stringify-object@5.0.0": { + "integrity": "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==", + "dependencies": [ + "get-own-enumerable-keys", + "is-obj@3.0.0", + "is-regexp" + ] + }, + "strip-ansi@6.0.1": { + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": [ + "ansi-regex@5.0.1" + ] + }, + "strip-ansi@7.2.0": { + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dependencies": [ + "ansi-regex@6.3.0" + ] + }, + "strip-bom@3.0.0": { + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" + }, + "strip-final-newline@2.0.0": { + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "strip-final-newline@4.0.0": { + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==" + }, + "strip-json-comments@5.0.3": { + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==" + }, + "systeminformation@5.33.13": { + "integrity": "sha512-GtyakkchTQKymM1WeAB652zi3rM1q00RKdc+jZe9KGstuc/mWUwAnQGN/RE5gVYvHIR8xYOUIUD9irVKDtA0wA==", + "os": ["darwin", "linux", "win32", "freebsd", "openbsd", "netbsd", "sunos", "android"], + "bin": true + }, + "tagged-tag@1.0.0": { + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==" + }, + "tailwind-merge@3.7.0": { + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==" + }, + "tailwindcss@4.3.3": { + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==" + }, + "tapable@2.3.3": { + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==" + }, + "thread-stream@4.2.0": { + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dependencies": [ + "real-require@1.0.0" + ] + }, + "tiny-invariant@1.3.3": { + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "tinybench@6.2.0": { + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==" + }, + "tinyexec@1.3.1": { + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==" + }, + "tinyglobby@0.2.17": { + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dependencies": [ + "fdir", + "picomatch@4.0.7" + ] + }, + "tldts-core@7.4.15": { + "integrity": "sha512-ERuv0p98XgSzmlSLJr8vDNxX+uATGInlN97V3R+JpYWaSqn5IG3ewWgCwczk4bkOpgth/o8Nt5FOi4B4w0n/1A==" + }, + "tldts@7.4.15": { + "integrity": "sha512-SJVBeHOxDbNoq14CvpAoA2mLEWdbldGK8nR+yumpjY5nrlZxOflcA7p1Qj1gbTGmbu9DY9qLj/bENSWhBzjxRQ==", + "dependencies": [ + "tldts-core" + ], + "bin": true + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" + ] + }, + "toidentifier@1.0.1": { + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tough-cookie@6.0.2": { + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dependencies": [ + "tldts" + ] + }, + "ts-algebra@2.0.0": { + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==" + }, + "ts-morph@26.0.0": { + "integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==", + "dependencies": [ + "@ts-morph/common", + "code-block-writer" + ] + }, + "tsconfck@3.1.6_typescript@7.0.2": { + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dependencies": [ + "typescript" + ], + "optionalPeers": [ + "typescript" + ], + "deprecated": true, + "bin": true + }, + "tsconfig-paths@4.2.0": { + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dependencies": [ + "json5", + "minimist", + "strip-bom" + ] + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "tw-animate-css@1.4.0": { + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==" + }, + "type-fest@5.10.0": { + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "dependencies": [ + "tagged-tag" + ] + }, + "type-is@2.1.0": { + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dependencies": [ + "content-type@2.1.0", + "media-typer", + "mime-types" + ] + }, + "typebox@1.3.27": { + "integrity": "sha512-zu+jc1pcy4UiNThxikUr36f0Rybk9PEeCg/NE6adeWr/SKsdNO4EzZHYRDlv2YCVAfj3Odq3dESSo/jNyoBXzA==" + }, + "typescript@7.0.2": { + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "optionalDependencies": [ + "@typescript/typescript-aix-ppc64", + "@typescript/typescript-darwin-arm64", + "@typescript/typescript-darwin-x64", + "@typescript/typescript-freebsd-arm64", + "@typescript/typescript-freebsd-x64", + "@typescript/typescript-linux-arm", + "@typescript/typescript-linux-arm64", + "@typescript/typescript-linux-loong64", + "@typescript/typescript-linux-mips64el", + "@typescript/typescript-linux-ppc64", + "@typescript/typescript-linux-riscv64", + "@typescript/typescript-linux-s390x", + "@typescript/typescript-linux-x64", + "@typescript/typescript-netbsd-arm64", + "@typescript/typescript-netbsd-x64", + "@typescript/typescript-openbsd-arm64", + "@typescript/typescript-openbsd-x64", + "@typescript/typescript-sunos-x64", + "@typescript/typescript-win32-arm64", + "@typescript/typescript-win32-x64" + ], + "bin": true + }, + "undici-types@7.24.6": { + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==" + }, + "undici@7.30.0": { + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==" + }, + "unicorn-magic@0.3.0": { + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==" + }, + "universalify@2.0.1": { + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + }, + "unpipe@1.0.0": { + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "until-async@3.0.2": { + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==" + }, + "update-browserslist-db@1.3.3_browserslist@4.29.1": { + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dependencies": [ + "browserslist", + "escalade", + "picocolors" + ], + "bin": true + }, + "use-callback-ref@1.3.3_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": [ + "@types/react", + "react", + "tslib" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "use-sidecar@1.1.3_@types+react@19.3.0_react@19.3.0": { + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": [ + "@types/react", + "detect-node-es", + "react", + "tslib" + ], + "optionalPeers": [ + "@types/react" + ] + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "valibot@1.5.0_typescript@7.0.2": { + "integrity": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "dependencies": [ + "typescript" + ], + "optionalPeers": [ + "typescript" + ] + }, + "validate-npm-package-name@7.0.2": { + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==" + }, + "vary@1.1.2": { + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "vite-node@3.2.4_@types+node@24.19.0": { + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dependencies": [ + "cac", + "debug", + "es-module-lexer@1.7.0", + "pathe@2.0.3", + "vite" + ], + "bin": true + }, + "vite-tsconfig-paths@5.1.4_vite@7.3.6_@types+node@24.19.0": { + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dependencies": [ + "debug", + "globrex", + "tsconfck", + "vite" + ], + "optionalPeers": [ + "vite" + ] + }, + "vite@7.3.6_@types+node@24.19.0": { + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dependencies": [ + "@types/node", + "esbuild", + "fdir", + "picomatch@4.0.7", + "postcss", + "rollup", + "tinyglobby" + ], + "optionalDependencies": [ + "fsevents" + ], + "optionalPeers": [ + "@types/node" + ], + "bin": true + }, + "vitest@5.0.2_@types+node@24.19.0_vite@7.3.6": { + "integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==", + "dependencies": [ + "@types/chai", + "@types/node", + "@vitest/mocker", + "chai", + "es-module-lexer@2.3.2", + "expect-type", + "magic-string@1.4.2", + "obug", + "picomatch@4.0.7", + "std-env", + "tinybench", + "tinyexec", + "tinyglobby", + "vite", + "why-is-node-running" + ], + "optionalPeers": [ + "@types/node" + ], + "bin": true + }, + "web-streams-polyfill@3.3.3": { + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==" + }, + "which@2.0.2": { + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": [ + "isexe@2.0.0" + ], + "bin": true + }, + "which@4.0.0": { + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dependencies": [ + "isexe@3.1.5" + ], + "bin": true + }, + "why-is-node-running@3.2.2": { + "integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==", + "bin": true + }, + "wrap-ansi@7.0.0": { + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": [ + "ansi-styles", + "string-width@4.2.3", + "strip-ansi@6.0.1" + ] + }, + "wrappy@1.0.2": { + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws@8.21.3": { + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==" + }, + "wsl-utils@1.0.0": { + "integrity": "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==", + "dependencies": [ + "is-wsl@3.1.1", + "powershell-utils@0.1.0" + ] + }, + "xtend@4.0.2": { + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n@5.0.8": { + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist@3.1.1": { + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "yaml@2.9.0": { + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "bin": true + }, + "yargs-parser@21.1.1": { + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "yargs@17.7.3": { + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dependencies": [ + "cliui", + "escalade", + "get-caller-file", + "require-directory", + "string-width@4.2.3", + "y18n", + "yargs-parser" + ] + }, + "yocto-spinner@1.2.2": { + "integrity": "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==", + "dependencies": [ + "yoctocolors" + ] + }, + "yoctocolors@2.2.0": { + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==" + }, + "zod-to-json-schema@3.25.2_zod@3.25.76": { + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dependencies": [ + "zod@3.25.76" + ] + }, + "zod@3.25.76": { + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + }, + "zod@4.6.5": { + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/expect@1", + "jsr:@std/testing@1", + "npm:@earendil-works/pi-agent-core@0.87.1", + "npm:@earendil-works/pi-ai@0.87.1", + "npm:@logtape/logtape@2", + "npm:@types/node@24", + "npm:@types/pg@8", + "npm:hono@^4.13.9", + "npm:kysely-postgres-js@^5.0.1", + "npm:kysely@~0.29.6", + "npm:openworkflow@0.10.1", + "npm:pg@^8.23.0", + "npm:pino-pretty@^13.1.3", + "npm:pino@^10.3.1", + "npm:postgres@^3.4.9", + "npm:typebox@1", + "npm:typescript@7.0.2", + "npm:vitest@5.0.2" + ], + "members": { + "packages/contracts": { + "packageJson": { + "dependencies": [ + "npm:zod@^4.3.6" + ] + } + }, + "web": { + "packageJson": { + "dependencies": [ + "npm:@fontsource-variable/inter-tight@^5.2.7", + "npm:@fontsource-variable/space-grotesk@^5.2.10", + "npm:@react-router/dev@7.12.0", + "npm:@react-router/node@7.12.0", + "npm:@tailwindcss/vite@^4.1.13", + "npm:@tanstack/react-query@^5.90.21", + "npm:@types/react-dom@^19.2.3", + "npm:@types/react@^19.2.7", + "npm:class-variance-authority@~0.7.1", + "npm:clsx@^2.1.1", + "npm:date-fns@^4.1.0", + "npm:isbot@^5.1.31", + "npm:lucide-react@0.576", + "npm:next-themes@~0.4.6", + "npm:radix-ui@^1.4.3", + "npm:react-day-picker@^10.0.1", + "npm:react-dom@^19.2.4", + "npm:react-router@7.12.0", + "npm:react@^19.2.4", + "npm:shadcn@^3.8.5", + "npm:sonner@^2.0.7", + "npm:tailwind-merge@^3.5.0", + "npm:tailwindcss@^4.1.13", + "npm:tw-animate-css@^1.4.0", + "npm:vite-tsconfig-paths@^5.1.4", + "npm:vite@^7.1.7", + "npm:zod@^4.3.6" + ] + } + } + } + } +} diff --git a/spikes/deno/logs/compile.txt b/spikes/deno/logs/compile.txt new file mode 100644 index 00000000..cea97659 --- /dev/null +++ b/spikes/deno/logs/compile.txt @@ -0,0 +1,17 @@ +[C] worker started pid=1036397 version=v1 runtime=deno 2.9.7 +[C] run=a8a8a202 step=work +[C] run=d1692ed7 step=work +[C] run=617df139 step=work +[C] run=e22ffd1b step=work +[C] run=6692a31b step=work +[C] worker stopped gracefully +{"ok":true,"runtime":"deno 2.9.7","ts":"6.0.3"}== ow-worker-bundle +[ow-worker-bundle] worker started pid=1038194 version=v1 runtime=deno 2.9.7 +[ow-worker-bundle] run=8c780ba3 step=work +[ow-worker-bundle] run=fc82d7c3 step=work +[ow-worker-bundle] worker stopped gracefully +== ow-worker-slim +[ow-worker-slim] worker started pid=1038731 version=v1 runtime=deno 2.9.7 +[ow-worker-slim] run=4bfca25f step=work +[ow-worker-slim] run=781c6f72 step=work +[ow-worker-slim] worker stopped gracefully diff --git a/spikes/deno/logs/compiled-ow-worker-bundle.out.txt b/spikes/deno/logs/compiled-ow-worker-bundle.out.txt new file mode 100644 index 00000000..2aeca307 --- /dev/null +++ b/spikes/deno/logs/compiled-ow-worker-bundle.out.txt @@ -0,0 +1,4 @@ +[ow-worker-bundle] worker started pid=1038194 version=v1 runtime=deno 2.9.7 +[ow-worker-bundle] run=8c780ba3 step=work +[ow-worker-bundle] run=fc82d7c3 step=work +[ow-worker-bundle] worker stopped gracefully diff --git a/spikes/deno/logs/compiled-ow-worker-slim.out.txt b/spikes/deno/logs/compiled-ow-worker-slim.out.txt new file mode 100644 index 00000000..8fbbe2ba --- /dev/null +++ b/spikes/deno/logs/compiled-ow-worker-slim.out.txt @@ -0,0 +1,4 @@ +[ow-worker-slim] worker started pid=1038731 version=v1 runtime=deno 2.9.7 +[ow-worker-slim] run=4bfca25f step=work +[ow-worker-slim] run=781c6f72 step=work +[ow-worker-slim] worker stopped gracefully diff --git a/spikes/deno/logs/compiled-worker.out.txt b/spikes/deno/logs/compiled-worker.out.txt new file mode 100644 index 00000000..0468441a --- /dev/null +++ b/spikes/deno/logs/compiled-worker.out.txt @@ -0,0 +1,7 @@ +[C] worker started pid=1036397 version=v1 runtime=deno 2.9.7 +[C] run=a8a8a202 step=work +[C] run=d1692ed7 step=work +[C] run=617df139 step=work +[C] run=e22ffd1b step=work +[C] run=6692a31b step=work +[C] worker stopped gracefully diff --git a/spikes/deno/logs/deno-check-real-repo-sloppy.txt b/spikes/deno/logs/deno-check-real-repo-sloppy.txt new file mode 100644 index 00000000..b815c557 --- /dev/null +++ b/spikes/deno/logs/deno-check-real-repo-sloppy.txt @@ -0,0 +1,7 @@ +packages/contracts/src/index.ts: 0 errors; +packages/domain/src/index.ts: 0 errors; +packages/db/src/index.ts: 0 errors; +packages/tasks/src/index.ts: 0 errors; +packages/agent/src/index.ts: 0 errors; +packages/adapters/src/index.ts: 0 errors; +apps/server/src/main.ts: 0 errors; diff --git a/spikes/deno/logs/deno-check-real-repo.txt b/spikes/deno/logs/deno-check-real-repo.txt new file mode 100644 index 00000000..5f36ef2d --- /dev/null +++ b/spikes/deno/logs/deno-check-real-repo.txt @@ -0,0 +1,7 @@ +packages/contracts/src/index.ts: 16 unresolved-imports, 16 errors; first: TS2307 [ERROR]: Cannot find module 'file:///opt/uhd_media_tool/packages/contracts/src/agent'. Maybe add a '.ts' extension or run with --sloppy-imports +packages/domain/src/index.ts: 7 unresolved-imports, 7 errors; first: TS2307 [ERROR]: Cannot find module 'file:///opt/uhd_media_tool/packages/domain/src/acquisition'. Maybe specify path to 'index.ts' file in directory in +packages/db/src/index.ts: 3 unresolved-imports, 3 errors; first: TS2307 [ERROR]: Cannot find module 'file:///opt/uhd_media_tool/packages/db/src/client'. Maybe add a '.ts' extension or run with --sloppy-imports +packages/tasks/src/index.ts: 9 unresolved-imports, 9 errors; first: TS2307 [ERROR]: Cannot find module 'file:///opt/uhd_media_tool/packages/tasks/src/definitions'. Maybe add a '.ts' extension or run with --sloppy-impor +packages/agent/src/index.ts: 11 unresolved-imports, 11 errors; first: TS2307 [ERROR]: Cannot find module 'file:///opt/uhd_media_tool/packages/agent/src/capability/errors'. Maybe add a '.ts' extension or run with --sloppy +packages/adapters/src/index.ts: 6 unresolved-imports, 6 errors; first: TS2307 [ERROR]: Cannot find module 'file:///opt/uhd_media_tool/packages/adapters/src/emby'. Maybe specify path to 'index.ts' file in directory instead +apps/server/src/main.ts: 8 unresolved-imports, 8 errors; first: TS2307 [ERROR]: Cannot find module 'file:///opt/uhd_media_tool/apps/server/src/config'. Maybe add a '.ts' extension or run with --sloppy-imports diff --git a/spikes/deno/logs/deno-test.txt b/spikes/deno/logs/deno-test.txt new file mode 100644 index 00000000..7d3ffb71 --- /dev/null +++ b/spikes/deno/logs/deno-test.txt @@ -0,0 +1,14 @@ +Check test/node_apis_test.ts +running 1 test from ./test/node_apis_test.ts +global ... + node compat under test runner ... + node:http mock server + fetch ... ok (15ms) + node:child_process spawn + kill ... ok (107ms) + fs temp dir + statfs ... ok (6ms) + hono app.request SSE ... ok (314ms) + workspace package import ... ok (0ms) + node compat under test runner ... ok (445ms) +global ... ok (453ms) + +ok | 1 passed (6 steps) | 0 failed (460ms) + diff --git a/spikes/deno/logs/docker.txt b/spikes/deno/logs/docker.txt new file mode 100644 index 00000000..8aa85b90 --- /dev/null +++ b/spikes/deno/logs/docker.txt @@ -0,0 +1,12 @@ +image size 260MB +health: {"ok":true,"runtime":"deno 2.9.7","ts":"6.0.3"} +/ -> 200 text/html; charset=utf-8 +/settings (SPA fallback) -> 404 text/plain; charset=UTF-8 +assets/manifest-f425e467.js -> 200 text/javascript; charset=utf-8 +sse: 3 events +docker stop (SIGTERM) took .309456482s +logs: +listening 8080 +shutdown +server finished +/settings (SPA fallback, container) -> 200 text/html; charset=utf-8 diff --git a/spikes/deno/logs/hono-server.out.txt b/spikes/deno/logs/hono-server.out.txt new file mode 100644 index 00000000..2c1b2095 --- /dev/null +++ b/spikes/deno/logs/hono-server.out.txt @@ -0,0 +1,3 @@ +listening 5199 +shutdown +server finished diff --git a/spikes/deno/logs/hono-shutdown-0.out.txt b/spikes/deno/logs/hono-shutdown-0.out.txt new file mode 100644 index 00000000..2c1b2095 --- /dev/null +++ b/spikes/deno/logs/hono-shutdown-0.out.txt @@ -0,0 +1,3 @@ +listening 5199 +shutdown +server finished diff --git a/spikes/deno/logs/hono-shutdown-1.out.txt b/spikes/deno/logs/hono-shutdown-1.out.txt new file mode 100644 index 00000000..2c1b2095 --- /dev/null +++ b/spikes/deno/logs/hono-shutdown-1.out.txt @@ -0,0 +1,3 @@ +listening 5199 +shutdown +server finished diff --git a/spikes/deno/logs/hono-shutdown.txt b/spikes/deno/logs/hono-shutdown.txt new file mode 100644 index 00000000..f2ac9896 --- /dev/null +++ b/spikes/deno/logs/hono-shutdown.txt @@ -0,0 +1,2 @@ +CLOSE_SSE=0 server exit after .210191521s alive=no +CLOSE_SSE=1 server exit after .208894206s alive=no diff --git a/spikes/deno/logs/hono.txt b/spikes/deno/logs/hono.txt new file mode 100644 index 00000000..8506ec82 --- /dev/null +++ b/spikes/deno/logs/hono.txt @@ -0,0 +1,21 @@ +{"ok":true,"runtime":"deno 2.9.7","ts":"6.0.3"} +{"url":"http://localhost:5199/api/v1/redact?token=[REDACTED]","apiKey":"[REDACTED]"} +HTTP/1.1 200 OK +cache-control: no-cache +connection: keep-alive +content-type: text/event-stream +transfer-encoding: chunked +date: Sun, 27 Sep 2026 10:03:06 GMT + +event: tick +data: {"i":6} +id: 6 + +event: tick +data: {"i":7} +id: 7 + +event: tick +data: {"i":8} +id: 8 + diff --git a/spikes/deno/logs/kysely-real-migrations.txt b/spikes/deno/logs/kysely-real-migrations.txt new file mode 100644 index 00000000..58b3c1d6 --- /dev/null +++ b/spikes/deno/logs/kysely-real-migrations.txt @@ -0,0 +1 @@ +pg migrate: ["0001_init:Success","0002_foundation:Success","0003_instances:Success","0004_content:Success","0005_identity:Success","0006_sources:Success","0007_assets:Success","0008_policy:Success","0009_execution:Success","0010_agent:Success","0011_ops:Success","0012_organization:Success"] error: null diff --git a/spikes/deno/logs/kysely.txt b/spikes/deno/logs/kysely.txt new file mode 100644 index 00000000..d90fdb5d --- /dev/null +++ b/spikes/deno/logs/kysely.txt @@ -0,0 +1,14 @@ +pg migrate: ["0001_init:Success","0002_index:Success"] error: null +pg rows: string:1/Dune/{"q":"1080p"}/true string:2/Arrival/null/true +pg tx error -> rollback +pg titles after tx: Dune,Arrival,tx-ok +pg 50 parallel queries on pool=4: 158ms +pg destroyed cleanly +exit=0 +postgresjs migrate: ["0001_init:Success","0002_index:Success"] error: null +postgresjs rows: string:1/Dune/"{\"q\":\"1080p\"}"/true string:2/Arrival/null/true +postgresjs tx error -> rollback +postgresjs titles after tx: Dune,Arrival,tx-ok +postgresjs 50 parallel queries on pool=4: 170ms +postgresjs destroyed cleanly +exit=0 diff --git a/spikes/deno/logs/logtape.txt b/spikes/deno/logs/logtape.txt new file mode 100644 index 00000000..0dad838f --- /dev/null +++ b/spikes/deno/logs/logtape.txt @@ -0,0 +1,2 @@ +{"@timestamp":"2026-09-27T10:04:24.739Z","level":"INFO","message":"fetch \"https://x/rss?passkey=[REDACTED]\"","logger":"umt.worker","properties":{"url":"https://x/rss?passkey=[REDACTED]","token":"[REDACTED]","n":3}} +{"@timestamp":"2026-09-27T10:04:24.741Z","level":"WARN","message":"child context","logger":"umt.worker","properties":{"runId":"r1","apiKey":"[REDACTED]"}} diff --git a/spikes/deno/logs/node-compat.txt b/spikes/deno/logs/node-compat.txt new file mode 100644 index 00000000..46e60a21 --- /dev/null +++ b/spikes/deno/logs/node-compat.txt @@ -0,0 +1,28 @@ +{ + "parseArgs": { + "role": "api" + }, + "statfs": { + "bsize": 4096, + "bavailGB": 1198 + }, + "os": { + "cpus": 96, + "freememMB": 114164, + "loadavg": 3 + }, + "memoryUsage": "rss,heapTotal,heapUsed,external,arrayBuffers", + "als": "req-1", + "crypto": "2d711642 36", + "worker": { + "echo": "from worker", + "pid": 1041177 + }, + "childKill": { + "code": 0, + "sig": null, + "out": "ready|got-TERM" + }, + "execPath": "/usr/local/bin/deno" +} +process.on(SIGTERM) handler fired diff --git a/spikes/deno/logs/ow-burst.txt b/spikes/deno/logs/ow-burst.txt new file mode 100644 index 00000000..2bc62e99 --- /dev/null +++ b/spikes/deno/logs/ow-burst.txt @@ -0,0 +1,2 @@ +{"count":40} +{"total":"40","runs":"40","a":"24","b":"16"} diff --git a/spikes/deno/logs/ow-happy.txt b/spikes/deno/logs/ow-happy.txt new file mode 100644 index 00000000..e4b5cb1c --- /dev/null +++ b/spikes/deno/logs/ow-happy.txt @@ -0,0 +1,16 @@ +{"runId":"76f4d2cd-7419-41b0-807d-756157fba672","status":"pending","version":"v1"} +{"status":"running","version":"v1","attempts":1,"error":null,"output":null} +{"workflowRunIds":["76f4d2cd-7419-41b0-807d-756157fba672"]} +{"status":"completed","version":"v1","attempts":3,"error":null,"output":{"sub":{"torrentHash":"hash-d1"},"copied":{"dest":"/lib/d1.mkv"},"signal":{"ok":true}}} +submit-download function completed +wait-download signal-wait completed +copy-file function completed +register function completed +retention sleep completed +cleanup function completed +76f4d2cd submit-download A version=v1 +76f4d2cd copy-file A start +76f4d2cd copy-file A end +76f4d2cd register A version=v1 +76f4d2cd cleanup A +{"runId":"76f4d2cd-7419-41b0-807d-756157fba672","status":"completed","version":"v1"} diff --git a/spikes/deno/logs/ow-kill9.txt b/spikes/deno/logs/ow-kill9.txt new file mode 100644 index 00000000..2b9b7cc2 --- /dev/null +++ b/spikes/deno/logs/ow-kill9.txt @@ -0,0 +1,19 @@ +run=1fdbe2ec-6952-4958-bcbb-50903dbdfbe1 +{"workflowRunIds":["1fdbe2ec-6952-4958-bcbb-50903dbdfbe1"]} +kill -9 worker A pid=968976 +1fdbe2ec submit-download A version=v1 +1fdbe2ec copy-file A start +after 12x3s: {"status":"completed","version":"v1","attempts":4,"error":null,"output":{"sub":{"torrentHash":"hash-d2"},"copied":{"dest":"/lib/d2.mkv"},"signal":{"ok":true}}} +submit-download function completed +wait-download signal-wait completed +copy-file function running +copy-file function completed +register function completed +retention sleep completed +cleanup function completed +1fdbe2ec submit-download A version=v1 +1fdbe2ec copy-file A start +1fdbe2ec copy-file B start +1fdbe2ec copy-file B end +1fdbe2ec register B version=v1 +1fdbe2ec cleanup B diff --git a/spikes/deno/logs/ow-tables.txt b/spikes/deno/logs/ow-tables.txt new file mode 100644 index 00000000..289c7d7f --- /dev/null +++ b/spikes/deno/logs/ow-tables.txt @@ -0,0 +1,5 @@ +biz.side_effect_ledger +openworkflow.openworkflow_migrations +openworkflow.step_attempts +openworkflow.workflow_runs +openworkflow.workflow_signals diff --git a/spikes/deno/logs/pi-abort.txt b/spikes/deno/logs/pi-abort.txt new file mode 100644 index 00000000..1256d7ea --- /dev/null +++ b/spikes/deno/logs/pi-abort.txt @@ -0,0 +1,19 @@ +open operations at attach: [] +[tool_start] inventory__query +[tool_end] inventory__query "{\"title\":\"Dune\",\"inLibrary\":false,\"versions\":[]}" +[tool_start] download__submit +>> lane.abort() +[operation_abort] +[tool_end] download__submit "aborted by signal" +[tool_start] bash +[tool_end] bash "Tool execution was cancelled before completion." +[run_end] aborted +abort result: {"ok":true,"value":{"operationId":"01a0e251-49a9-7098-9037-ced15867c0d4","steer":[],"followUp":[]}} +PROMPT RESULT {"ok":true,"value":{"operationId":"01a0e251-49a9-7098-9037-ced15867c0d4","kind":"run","status":"aborted","fromTipId":null,"tipId":"01a0e251-49c2-7098-9037-cee4bf55ca98","startedAt":1790503373225,"endedAt":1790503373567}} +SUBMITTED in this process [] fake calls: 2 + user 把《Dune》1080p 收进来 + assistant | 我先查一下库里有没有《Dune》。 | toolCall(inventory__query) + toolResult inventory__query {"title":"Dune","inLibrary":false,"versions":[]} + assistant 库里没有,提交 1080p 下载;顺便试一下未授权的 bash。 | toolCall(download__submit) | toolCall(bash) + toolResult download__submit [isError] aborted by signal + toolResult bash [isError] Tool execution was cancelled before completion. diff --git a/spikes/deno/logs/pi-crash.txt b/spikes/deno/logs/pi-crash.txt new file mode 100644 index 00000000..1e47bbde --- /dev/null +++ b/spikes/deno/logs/pi-crash.txt @@ -0,0 +1,5 @@ +open operations at attach: [] +[tool_start] inventory__query +[tool_end] inventory__query "{\"title\":\"Dune\",\"inLibrary\":false,\"versions\":[]}" +[tool_start] download__submit +>> SIGKILL self during download.submit diff --git a/spikes/deno/logs/pi-fake-run.txt b/spikes/deno/logs/pi-fake-run.txt new file mode 100644 index 00000000..cd9db401 --- /dev/null +++ b/spikes/deno/logs/pi-fake-run.txt @@ -0,0 +1,39 @@ +[run_start] lane=main +[turn_start] lane=main + [text_delta] "我先查一下库里有没有《D" + [text_delta] "une》。" + [toolcall_end] inventory__query +[usage] in=145 totals.input=145 output=18 + [hook before_tool] inventory__query args={"title":"Dune","year":2021} +[tool_start] inventory__query {"title":"Dune","year":2021} +[tool_update] lane=main +[tool_end] inventory__query isError=false -> "{\"title\":\"Dune\",\"inLibrary\":false,\"versions\":[]}" +[turn_end] lane=main +[turn_start] lane=main + [text_delta] "库里没有,提交 1080p 下载" + [text_delta] ";顺便试一下未授权的 b" + [text_delta] "ash。" + [toolcall_end] download__submit + [toolcall_end] bash +[usage] in=40 totals.input=185 output=46 + [hook before_tool] download__submit args={"title":"Dune","quality":"1080p"} +[tool_start] download__submit {"title":"Dune","quality":"1080p"} +[tool_end] download__submit isError=false -> "{\"accepted\":true,\"idempotencyKey\":\"dl:Dune:1080p\",\"status\":\"submitted\"}" +[tool_start] bash {"command":"rm -rf /"} +[tool_end] bash isError=true -> "Tool \"bash\" is unavailable" +[turn_end] lane=main +[turn_start] lane=main + [text_delta] "下载已提交(仅代表已提交" + [text_delta] ",不代表已入库):{\"a" + [text_delta] "ccepted\":tru" + [text_delta] "e,\"idempotencyKey\":\"" + [text_delta] "dl:Dune:1080" + [text_delta] "p\",\"status\":\"sub" + [text_delta] "mitted\"};bash 结果 isE" + [text_delta] "rror=true。" +[usage] in=67 totals.input=252 output=75 +[turn_end] lane=main +[run_end] status=completed +RESULT {"ok":true,"value":{"operationId":"01a0e251-372b-714d-8e79-7300ed37b0d8","kind":"run","status":"completed","fromTipId":null,"tipId":"01a0e251-3749-714d-8e79-731888a9468b","startedAt":1790503368491,"endedAt":1790503368525}} +SUBMITTED [{"key":"dl:Dune:1080p","title":"Dune"}] fake calls: 3 +TRANSCRIPT roles: assistant,toolResult,toolResult,assistant,toolResult,assistant,user diff --git a/spikes/deno/logs/pi-resume.txt b/spikes/deno/logs/pi-resume.txt new file mode 100644 index 00000000..baf6266d --- /dev/null +++ b/spikes/deno/logs/pi-resume.txt @@ -0,0 +1,15 @@ +found session crash-demo /opt/uhd_media_tool/spikes/deno/apps/server/src/pi/.sessions/--opt-uhd_media_tool-spikes-deno-apps-server-src-pi-.sessions--/2026-09-27T10-02-53-768Z_crash-demo.jsonl +open operations at attach: [{"lane":"main","operationId":"01a0e251-4bd4-7649-b068-537faf822316","kind":"run","startedAt":1790503373780}] +[tool_end] (recovery) download__submit "[Tool execution was interrupted. The preceding output is the latest durable progress snapshot; newer live output may be missing, and the external outcome is unknown.]" +[tool_start] (recovery) bash +[tool_end] (recovery) bash "Tool \"bash\" is unavailable" +[run_end] completed +RESUME RESULT {"ok":true,"value":{"operationId":"01a0e251-4bd4-7649-b068-537faf822316","kind":"run","status":"completed","fromTipId":null,"tipId":"01a0e251-4dfd-74a9-9dee-40c910a4e623","startedAt":1790503373780,"endedAt":1790503374346}} +SUBMITTED in this process [] fake calls: 1 + user 把《Dune》1080p 收进来 + assistant | 我先查一下库里有没有《Dune》。 | toolCall(inventory__query) + toolResult inventory__query {"title":"Dune","inLibrary":false,"versions":[]} + assistant 库里没有,提交 1080p 下载;顺便试一下未授权的 bash。 | toolCall(download__submit) | toolCall(bash) + toolResult download__submit [isError] [Tool execution was interrupted. The preceding output is the latest durable progress snapshot; newer live output may be missing, and the external outcome is unknown.] + toolResult bash [isError] Tool "bash" is unavailable + assistant 下载已提交(仅代表已提交,不代表已入库):[Tool execution was interrupted. The preceding output is the latest durable progress snapshot; newer live output may be missing, and the external outcome is unknown.];bash 结果 isEr diff --git a/spikes/deno/logs/pino.txt b/spikes/deno/logs/pino.txt new file mode 100644 index 00000000..b3fd01cd --- /dev/null +++ b/spikes/deno/logs/pino.txt @@ -0,0 +1,7 @@ +custom-dest: {"level":30,"time":1790503464308,"pid":985225,"hostname":"DS4199","token":"[REDACTED]","url":"https://x/rss?passkey=[REDACTED]&a=1","n":1,"msg":"fetch https://x?api_key=[REDACTED]"} +{"level":40,"time":1790503464309,"pid":985225,"hostname":"DS4199","component":"worker","password":"p","err":{},"msg":"child"} +{"level":30,"time":1790503464311,"pid":985225,"hostname":"DS4199","cookie":"[REDACTED]","msg":"stdout default destination"} +{"level":30,"time":1790503464364,"pid":985225,"hostname":"DS4199","secret":"[REDACTED]","msg":"via pino.transport (worker_threads)"} +{"level":30,"time":1790503482109,"pid":987284,"hostname":"DS4199","secret":"[REDACTED]","msg":"via pino.transport (worker_threads)"} +[18:04:42] INFO (987284): via pino-pretty transport + token: "[REDACTED]" diff --git a/spikes/deno/logs/sloppy-fix.txt b/spikes/deno/logs/sloppy-fix.txt new file mode 100644 index 00000000..a4b4026b --- /dev/null +++ b/spikes/deno/logs/sloppy-fix.txt @@ -0,0 +1,3 @@ +import { a } from "./sloppy"; +export const b = a; +export * from "./a"; diff --git a/spikes/deno/logs/tokens.txt b/spikes/deno/logs/tokens.txt new file mode 100644 index 00000000..5cc7735e --- /dev/null +++ b/spikes/deno/logs/tokens.txt @@ -0,0 +1,4 @@ +已生成 app/lib/design-tokens.generated.ts(字号 36 / 缓动 5 / 圆角 10 / 时长 6) +design token 登记表是最新的(字号 36 / 缓动 5 / 圆角 10 / 时长 6) +check exit=0 +设计 token 检查通过 diff --git a/spikes/deno/logs/tsc7-real-repo.txt b/spikes/deno/logs/tsc7-real-repo.txt new file mode 100644 index 00000000..ab8056bd --- /dev/null +++ b/spikes/deno/logs/tsc7-real-repo.txt @@ -0,0 +1,8 @@ +packages/adapters: ts7 errors=0 +packages/agent: ts7 errors=0 +packages/contracts: ts7 errors=0 +packages/db: ts7 errors=0 +packages/domain: ts7 errors=0 +packages/tasks: ts7 errors=0 +packages/testkit: ts7 errors=0 +apps/server: ts7 errors=0 diff --git a/spikes/deno/logs/tsc7-web.txt b/spikes/deno/logs/tsc7-web.txt new file mode 100644 index 00000000..d1782ec9 --- /dev/null +++ b/spikes/deno/logs/tsc7-web.txt @@ -0,0 +1,3 @@ +tsconfig.json(13,5): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration. + Use '"paths": {"*": ["./*"]}' instead. +exit=0 diff --git a/spikes/deno/logs/typecheck-compare.txt b/spikes/deno/logs/typecheck-compare.txt new file mode 100644 index 00000000..481ee701 --- /dev/null +++ b/spikes/deno/logs/typecheck-compare.txt @@ -0,0 +1,11 @@ +== deno check +real 0m2.574s +== tsc 7.0.2 +Task tsgo deno run -A npm:typescript@7.0.2/tsc --noEmit -p tsconfig.tsc.json +real 0m1.525s +user 0m12.882s +sys 0m1.994s +exit=0 +== logtape (npm) rerun +{"@timestamp":"2026-09-27T10:10:40.172Z","level":"INFO","message":"fetch \"https://x/rss?passkey=[REDACTED]\"","logger":"umt.worker","properties":{"url":"https://x/rss?passkey=[REDACTED]","token":"[REDACTED]","n":3}} +{"@timestamp":"2026-09-27T10:10:40.174Z","level":"WARN","message":"child context","logger":"umt.worker","properties":{"runId":"r1","apiKey":"[REDACTED]"}} diff --git a/spikes/deno/logs/vitest-real-packages.txt b/spikes/deno/logs/vitest-real-packages.txt new file mode 100644 index 00000000..95e890be --- /dev/null +++ b/spikes/deno/logs/vitest-real-packages.txt @@ -0,0 +1,12 @@ +packages/adapters | deno: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed | 8 passed (9) Tests 1 failed | 108 passed | 2 skipped (111) | node: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed | 8 passed (9) Tests 1 failed | 108 passed | 2 skipped (111) +packages/agent | deno: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed | 3 passed (4) Tests 1 failed | 26 passed (27) | node: Test Files 4 passed (4) Tests 27 passed (27) +packages/contracts | deno: Test Files 3 passed (3) Tests 361 passed (361) | node: Test Files 3 passed (3) Tests 361 passed (361) +packages/db | deno: Test Files 2 passed (2) Tests 11 passed (11) | node: Test Files 2 passed (2) Tests 11 passed (11) +packages/domain | deno: Test Files 2 passed (2) Tests 122 passed (122) | node: Test Files 2 passed (2) Tests 122 passed (122) +packages/tasks | deno: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ Test Files 3 failed | 2 passed (5) Tests 1 failed | 7 passed | 7 skipped (15) | node: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ Test Files 1 failed | 4 passed (5) Tests 1 failed | 14 passed (15) +packages/testkit | deno: Test Files 1 passed (1) Tests 1 passed (1) | node: Test Files 1 passed (1) Tests 1 passed (1) + +# notes +# adapters: same 1 failure under node and deno (pre-existing, WIP by other agent) +# agent: deno-only failure = crash-recovery child spawned via process.execPath (=deno) hits extensionless import in packages/contracts/src/index.ts +# tasks: dev PG 55433 shared/unstable in both runtimes; rerun under deno with TASKS_TEST_DATABASE_URL=postgres://postgres:spike@127.0.0.1:55435/umt_tasks -> Test Files 5 passed (5), Tests 15 passed (15) (incl. kill -9 crash-recovery) diff --git a/spikes/deno/logs/vitest-real-server.txt b/spikes/deno/logs/vitest-real-server.txt new file mode 100644 index 00000000..3281558b --- /dev/null +++ b/spikes/deno/logs/vitest-real-server.txt @@ -0,0 +1,9 @@ + + RUN v5.0.2 /opt/uhd_media_tool/apps/server + + + Test Files 4 passed (4) + Tests 15 passed (15) + Start at 18:05:17 + Duration 733ms (import 48%, transform 46%, tests 6%, worker 1%) + diff --git a/spikes/deno/logs/vitest.txt b/spikes/deno/logs/vitest.txt new file mode 100644 index 00000000..740c6edd --- /dev/null +++ b/spikes/deno/logs/vitest.txt @@ -0,0 +1,27 @@ + + RUN v5.0.2 /opt/uhd_media_tool/spikes/deno/apps/server + + + Test Files 1 passed (1) + Tests 5 passed (5) + Start at 18:05:04 + Duration 712ms (tests 80%, transform 15%, import 4%, worker 1%) + + + RUN v5.0.2 /opt/uhd_media_tool/spikes/deno/apps/server + + + Test Files 1 passed (1) + Tests 5 passed (5) + Start at 18:05:05 + Duration 747ms (tests 79%, transform 16%, import 5%, worker 1%) + + + RUN v5.0.2 /opt/uhd_media_tool/spikes/deno/apps/server + + + Test Files 1 passed (1) + Tests 5 passed (5) + Start at 18:05:06 + Duration 728ms (tests 79%, transform 16%, import 4%, worker 1%) + diff --git a/spikes/deno/logs/web-build.txt b/spikes/deno/logs/web-build.txt new file mode 100644 index 00000000..ed8b2bd6 --- /dev/null +++ b/spikes/deno/logs/web-build.txt @@ -0,0 +1,28 @@ +build/server/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 18.94 kB +build/server/assets/inter-tight-greek-wght-normal-sS1B39LI.woff2 22.19 kB +build/server/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 22.29 kB +build/server/assets/inter-tight-cyrillic-ext-wght-normal-okcGN4Nq.woff2 26.66 kB +build/server/assets/inter-tight-latin-wght-normal-DX-nOvPD.woff2 44.87 kB +build/server/assets/inter-tight-latin-ext-wght-normal-B4u9PlvR.woff2 89.80 kB +build/server/assets/server-build-DhK8dDjm.css 90.20 kB +build/server/index.js 21.63 kB + +✓ 11 assets cleaned from React Router server build. +build/client/assets/inter-tight-cyrillic-ext-wght-normal-okcGN4Nq.woff2 +build/client/assets/inter-tight-cyrillic-wght-normal-BS41q_Tv.woff2 +build/client/assets/inter-tight-greek-ext-wght-normal-DQgQPNk4.woff2 +build/client/assets/inter-tight-greek-wght-normal-sS1B39LI.woff2 +build/client/assets/inter-tight-vietnamese-wght-normal-TKKdAiBA.woff2 +build/client/assets/inter-tight-latin-ext-wght-normal-B4u9PlvR.woff2 +build/client/assets/inter-tight-latin-wght-normal-DX-nOvPD.woff2 +build/client/assets/space-grotesk-vietnamese-wght-normal-D0rl6rjA.woff2 +build/client/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 +build/client/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +build/server/assets/server-build-DhK8dDjm.css + +SPA Mode: Generated build/client/index.html +Removing the server build in /opt/uhd_media_tool/spikes/deno/web/build/server due to ssr:false +✓ built in 690ms +SPA Mode: Generated build/client/index.html +Removing the server build in /opt/uhd_media_tool/spikes/deno/web/build/server due to ssr:false +✓ built in 421ms diff --git a/spikes/deno/logs/web-dev-server.out.txt b/spikes/deno/logs/web-dev-server.out.txt new file mode 100644 index 00000000..7e1705fe --- /dev/null +++ b/spikes/deno/logs/web-dev-server.out.txt @@ -0,0 +1,68 @@ +[tsconfig-paths] An error occurred while parsing "/opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json". See below for details. To disable this message, set the `ignoreConfigErrors` option to true. +TSConfckParseError: failed to resolve "extends":"../../tsconfig.base.json" in /opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json + at resolveExtends (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:262:8) + at parseExtends (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:197:24) + at Module.parse (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:54:23) + at async Promise.all (index 10) + at BasicMinimalPluginContext.configResolved (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/vite-tsconfig-paths@5.1.4/node_modules/vite-tsconfig-paths/src/index.ts:106:9) + at async Promise.all (index 1) + at resolveConfig (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/vite@7.3.6/node_modules/vite/dist/node/chunks/config.js:35762:2) + at _createServer (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/vite@7.3.6/node_modules/vite/dist/node/chunks/config.js:25441:67) + at dev (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/@react-router+dev@7.12.0/node_modules/@react-router/dev/dist/cli/index.js:2104:16) + at dev2 (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/@react-router+dev@7.12.0/node_modules/@react-router/dev/dist/cli/index.js:2275:3) { + code: "EXTENDS_RESOLVE", + tsconfigFile: "/opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json", + cause: Error: Cannot find module '../../tsconfig.base.json' + Require stack: + - /opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json + at Module._resolveFilename (node:module:4:14) + at nextResolve (node:module:1:13073) + at wrappedNext (node:module:1:13273) + at Ie (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/@tailwindcss+node@4.3.3/node_modules/@tailwindcss/node/dist/index.mjs:1:539) + at nextResolve (node:module:1:13299) + at executeResolveHookChain (node:module:1:13643) + at Module._resolveFilename (node:module:1:25781) + at require.resolve (node:module:5:3624) + at resolveExtends (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:250:14) + at parseExtends (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:197:24) { + code: "MODULE_NOT_FOUND", + requireStack: [ + "/opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json" + ] + } +} +[tsconfig-paths] An error occurred while parsing "/opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json". See below for details. To disable this message, set the `ignoreConfigErrors` option to true. +TSConfckParseError: failed to resolve "extends":"../../tsconfig.base.json" in /opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json + at resolveExtends (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:262:8) + at parseExtends (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:197:24) + at Module.parse (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:54:23) + at async Promise.all (index 10) + at BasicMinimalPluginContext.configResolved (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/vite-tsconfig-paths@5.1.4/node_modules/vite-tsconfig-paths/src/index.ts:106:9) + at async Promise.all (index 1) + at resolveConfig (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/vite@7.3.6/node_modules/vite/dist/node/chunks/config.js:35762:2) + at _createServer (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/vite@7.3.6/node_modules/vite/dist/node/chunks/config.js:25441:67) + at BasicMinimalPluginContext.configResolved (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/@react-router+dev@7.12.0/node_modules/@react-router/dev/dist/vite.js:3405:29) + at async Promise.all (index 2) { + code: "EXTENDS_RESOLVE", + tsconfigFile: "/opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json", + cause: Error: Cannot find module '../../tsconfig.base.json' + Require stack: + - /opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json + at Module._resolveFilename (node:module:4:14) + at nextResolve (node:module:1:13073) + at wrappedNext (node:module:1:13273) + at Ie (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/@tailwindcss+node@4.3.3/node_modules/@tailwindcss/node/dist/index.mjs:1:539) + at nextResolve (node:module:1:13299) + at executeResolveHookChain (node:module:1:13643) + at Module._resolveFilename (node:module:1:25781) + at require.resolve (node:module:5:3624) + at resolveExtends (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:250:14) + at parseExtends (/opt/uhd_media_tool/spikes/deno/node_modules/.deno/tsconfck@3.1.6/node_modules/tsconfck/src/parse.js:197:24) { + code: "MODULE_NOT_FOUND", + requireStack: [ + "/opt/uhd_media_tool/spikes/deno/packages/contracts/tsconfig.json" + ] + } +} + ➜ Local: http://127.0.0.1:5181/ + ➜ Network: use --host to expose diff --git a/spikes/deno/logs/web-dev.txt b/spikes/deno/logs/web-dev.txt new file mode 100644 index 00000000..5f2f5bd4 --- /dev/null +++ b/spikes/deno/logs/web-dev.txt @@ -0,0 +1,8 @@ +GET / -> 200 228202B +UHD 媒体自动化控制台 +GET /app/root.tsx -> 200 text/javascript +GET /app/app.css -> 200 +[npm:vite dev] GET / -> 200 228202B +UHD 媒体自动化控制台 +GET /app/root.tsx -> 200 text/javascript +GET /app/api/health.ts -> 200 diff --git a/spikes/deno/logs/web-vite-dev-server.out.txt b/spikes/deno/logs/web-vite-dev-server.out.txt new file mode 100644 index 00000000..83c0a9ee --- /dev/null +++ b/spikes/deno/logs/web-vite-dev-server.out.txt @@ -0,0 +1,7 @@ + + VITE v7.3.6 ready in 561 ms + + ➜ Local: http://127.0.0.1:5181/ + ➜ Network: use --host to expose +✘ [ERROR] The build was canceled + diff --git a/spikes/deno/logs/workerA.out.txt b/spikes/deno/logs/workerA.out.txt new file mode 100644 index 00000000..e7bcc8de --- /dev/null +++ b/spikes/deno/logs/workerA.out.txt @@ -0,0 +1,8 @@ +[A] worker started pid=968976 version=v1 runtime=deno 2.9.7 +[A] run=76f4d2cd step=submit-download version=v1 +[A] run=76f4d2cd step=copy-file start +[A] run=76f4d2cd step=copy-file end +[A] run=76f4d2cd step=register version=v1 +[A] run=76f4d2cd step=cleanup +[A] run=1fdbe2ec step=submit-download version=v1 +[A] run=1fdbe2ec step=copy-file start diff --git a/spikes/deno/logs/workerA2.out.txt b/spikes/deno/logs/workerA2.out.txt new file mode 100644 index 00000000..5335c095 --- /dev/null +++ b/spikes/deno/logs/workerA2.out.txt @@ -0,0 +1,26 @@ +[A] worker started pid=974721 version=v1 runtime=deno 2.9.7 +[A] run=0ef64009 step=work +[A] run=db4b002d step=work +[A] run=59c3f0a3 step=work +[A] run=753c094f step=work +[A] run=322c2a2e step=work +[A] run=85126efd step=work +[A] run=24bd06f4 step=work +[A] run=d1635912 step=work +[A] run=1d5b5a33 step=work +[A] run=ac3a2ef4 step=work +[A] run=ca92602e step=work +[A] run=70e3a89b step=work +[A] run=f82c62f0 step=work +[A] run=832cdd09 step=work +[A] run=33d05100 step=work +[A] run=e83b537d step=work +[A] run=cf8a4c6c step=work +[A] run=030d8db0 step=work +[A] run=85854e33 step=work +[A] run=67f86be7 step=work +[A] run=0b99c858 step=work +[A] run=542354a5 step=work +[A] run=818f3e55 step=work +[A] run=a4739536 step=work +[A] worker stopped gracefully diff --git a/spikes/deno/logs/workerB.out.txt b/spikes/deno/logs/workerB.out.txt new file mode 100644 index 00000000..a2ee51fb --- /dev/null +++ b/spikes/deno/logs/workerB.out.txt @@ -0,0 +1,22 @@ +[B] worker started pid=970631 version=v1 runtime=deno 2.9.7 +[B] run=1fdbe2ec step=copy-file start +[B] run=1fdbe2ec step=copy-file end +[B] run=1fdbe2ec step=register version=v1 +[B] run=1fdbe2ec step=cleanup +[B] run=a6855da5 step=work +[B] run=3306be9c step=work +[B] run=5400dd52 step=work +[B] run=7d9932a2 step=work +[B] run=356cd393 step=work +[B] run=493d6178 step=work +[B] run=4aed4f53 step=work +[B] run=89194738 step=work +[B] run=42285c79 step=work +[B] run=981eb93b step=work +[B] run=a929d9fd step=work +[B] run=4db4dc54 step=work +[B] run=3e1a7577 step=work +[B] run=b5c77ed8 step=work +[B] run=3dcac3f6 step=work +[B] run=9fefd4e0 step=work +[B] worker stopped gracefully diff --git a/spikes/deno/packages/contracts/deno.json b/spikes/deno/packages/contracts/deno.json new file mode 100644 index 00000000..54b289f8 --- /dev/null +++ b/spikes/deno/packages/contracts/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@umt/contracts", + "version": "0.0.0", + "exports": { ".": "./src/index.ts" } +} diff --git a/spikes/deno/packages/contracts/package.json b/spikes/deno/packages/contracts/package.json new file mode 100644 index 00000000..89817a1b --- /dev/null +++ b/spikes/deno/packages/contracts/package.json @@ -0,0 +1,16 @@ +{ + "name": "@umt/contracts", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc -p tsconfig.json", + "test": "vitest run" + }, + "dependencies": { + "zod": "^4.3.6" + } +} diff --git a/spikes/deno/packages/contracts/src/agent.ts b/spikes/deno/packages/contracts/src/agent.ts new file mode 100644 index 00000000..1f4f6a53 --- /dev/null +++ b/spikes/deno/packages/contracts/src/agent.ts @@ -0,0 +1,284 @@ +import { z } from "zod"; +import { EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common"; + +// 人工/AI 域:Agent 运行、证据/计划产物、待处理事项、审批、持续授权(REQ §10、§11.3、§11.4) + +export const AgentTrigger = z.enum(["user", "event", "schedule", "subagent"]); +export type AgentTrigger = z.infer; +export const AgentRunStatus = z.enum([ + "queued", + "running", + "waiting", + "succeeded", + "failed", + "cancelled", + "budget_exceeded", +]); +export type AgentRunStatus = z.infer; + +/** 单次运行预算(调用次数/token/时长/费用,REQ §10.5) */ +export const AgentBudget = z.object({ + maxToolCalls: z.number().int().positive().optional(), + maxTokens: z.number().int().positive().optional(), + maxDurationSec: z.number().int().positive().optional(), + maxCostUsd: z.number().nonnegative().optional(), +}); +export type AgentBudget = z.infer; + +export const AgentUsage = z.object({ + inputTokens: NonNegInt.optional(), + outputTokens: NonNegInt.optional(), + cacheReadTokens: NonNegInt.optional(), + cacheWriteTokens: NonNegInt.optional(), + costUsd: z.number().nonnegative().optional(), + durationMs: NonNegInt.optional(), +}); +export type AgentUsage = z.infer; + +export const AgentRun = EntityBase.extend({ + taskId: Id.nullable(), + parentRunId: Id.nullable(), + trigger: AgentTrigger, + goal: z.string().min(1), + status: AgentRunStatus, + provider: z.string().nullable(), + model: z.string().nullable(), + thinkingLevel: z.string().nullable(), + /** 授权范围(能力白名单、对象范围);子代理继承父任务上限 */ + scope: JsonObject, + budget: AgentBudget, + usage: AgentUsage, + evidenceVersion: z.string().nullable(), + /** Pi 会话持久化状态;不对普通列表返回 */ + sessionState: JsonObject.nullable(), + summary: z.string().nullable(), + toolCallCount: NonNegInt, + lastEventSeq: NonNegInt, + error: JsonObject.nullable(), + cancelRequestedAt: Timestamp.nullable(), + startedAt: Timestamp.nullable(), + finishedAt: Timestamp.nullable(), + createdBy: z.string().nullable(), +}); +export type AgentRun = z.infer; +export const AgentRunCreate = AgentRun.pick({ + taskId: true, + parentRunId: true, + trigger: true, + goal: true, + scope: true, + budget: true, +}).partial({ taskId: true, parentRunId: true, trigger: true, scope: true, budget: true }); +export type AgentRunCreate = z.infer; +export const AgentRunUpdate = AgentRun.pick({ + status: true, + provider: true, + model: true, + thinkingLevel: true, + usage: true, + evidenceVersion: true, + sessionState: true, + summary: true, + toolCallCount: true, + lastEventSeq: true, + error: true, + startedAt: true, + finishedAt: true, +}).partial(); +export type AgentRunUpdate = z.infer; + +export const ArtifactKind = z.enum(["evidence", "plan", "proposal", "report", "snapshot", "log"]); +export type ArtifactKind = z.infer; + +/** 证据/计划/提案产物:提案不直接晋升为事实 */ +export const Artifact = EntityBase.extend({ + kind: ArtifactKind, + agentRunId: Id.nullable(), + taskId: Id.nullable(), + subjectType: z.string().nullable(), + subjectId: Id.nullable(), + title: z.string().min(1), + mimeType: z.string(), + content: JsonObject, + contentHash: z.string().min(1), + sizeBytes: NonNegInt.nullable(), + expiresAt: Timestamp.nullable(), +}); +export type Artifact = z.infer; +/** contentHash / sizeBytes 由服务端计算 */ +export const ArtifactCreate = Artifact.omit({ ...SERVER_MANAGED, contentHash: true, sizeBytes: true }).partial({ + agentRunId: true, + taskId: true, + subjectType: true, + subjectId: true, + mimeType: true, + expiresAt: true, +}); +export type ArtifactCreate = z.infer; +/** 产物内容不可变;只能调整过期时间 */ +export const ArtifactUpdate = z.object({ expiresAt: Timestamp.nullable() }); +export type ArtifactUpdate = z.infer; + +export const DecisionKind = z.enum([ + "identity_ambiguous", + "download_stalled", + "download_failed", + "path_conflict", + "space_insufficient", + "approval_required", + "metadata_conflict", + "source_unreachable", + "other", +]); +export type DecisionKind = z.infer; +export const DecisionStatus = z.enum(["open", "investigating", "awaiting_user", "resolved", "dismissed", "expired"]); +export type DecisionStatus = z.infer; +export const Severity = z.enum(["info", "warning", "critical"]); +export type Severity = z.infer; + +export const DecisionOption = z.object({ + key: z.string().min(1), + label: z.string().min(1), + description: z.string().optional(), + /** 选择后执行的能力与参数(仍经授权校验) */ + capabilityId: z.string().optional(), + input: JsonObject.optional(), +}); +export type DecisionOption = z.infer; + +/** 待处理中心条目;同一 dedupeKey 同时只有一个未关闭条目 */ +export const Decision = EntityBase.extend({ + kind: DecisionKind, + status: DecisionStatus, + severity: Severity, + title: z.string().min(1), + summary: z.string().nullable(), + subjectType: z.string().nullable(), + subjectId: Id.nullable(), + dedupeKey: z.string().min(1), + evidence: JsonObject, + options: z.array(DecisionOption), + resolution: JsonObject.nullable(), + taskId: Id.nullable(), + agentRunId: Id.nullable(), + attempts: NonNegInt, + nextReviewAt: Timestamp.nullable(), + resolvedBy: z.string().nullable(), + resolvedAt: Timestamp.nullable(), +}); +export type Decision = z.infer; +export const DecisionCreate = Decision.pick({ + kind: true, + severity: true, + title: true, + summary: true, + subjectType: true, + subjectId: true, + dedupeKey: true, + evidence: true, + options: true, + taskId: true, + agentRunId: true, + nextReviewAt: true, +}).partial({ + severity: true, + summary: true, + subjectType: true, + subjectId: true, + evidence: true, + options: true, + taskId: true, + agentRunId: true, + nextReviewAt: true, +}); +export type DecisionCreate = z.infer; +export const DecisionUpdate = Decision.pick({ + status: true, + severity: true, + summary: true, + evidence: true, + options: true, + agentRunId: true, + nextReviewAt: true, +}).partial(); +export type DecisionUpdate = z.infer; +/** 用户处理待处理事项:选一个选项或关闭 */ +export const DecisionResolveInput = z.object({ + optionKey: z.string().min(1).optional(), + dismiss: z.boolean().optional(), + note: z.string().max(2000).optional(), +}); +export type DecisionResolveInput = z.infer; + +export const ApprovalStatus = z.enum(["pending", "approved", "rejected", "revoked", "expired", "consumed"]); +export type ApprovalStatus = z.infer; + +/** 单次审批:绑定 planType+planId + payloadHash + expectedRevision;换候选/路径后旧审批失效 */ +export const Approval = EntityBase.extend({ + planType: z.string().min(1), + planId: Id, + payloadHash: z.string().min(1), + expectedRevision: z.number().int().min(0), + scope: JsonObject, + status: ApprovalStatus, + decisionId: Id.nullable(), + requestedBy: z.string().min(1), + decidedBy: z.string().nullable(), + decidedAt: Timestamp.nullable(), + expiresAt: Timestamp.nullable(), + reason: z.string().nullable(), +}); +export type Approval = z.infer; +export const ApprovalCreate = Approval.pick({ + planType: true, + planId: true, + payloadHash: true, + expectedRevision: true, + scope: true, + decisionId: true, + requestedBy: true, + expiresAt: true, + reason: true, +}).partial({ scope: true, decisionId: true, expiresAt: true, reason: true }); +export type ApprovalCreate = z.infer; +/** 批准/拒绝时回传 payloadHash,服务端比对防止审批被挪用到变更后的计划 */ +export const ApprovalUpdate = z.object({ + status: z.enum(["approved", "rejected", "revoked"]), + payloadHash: z.string().min(1), + reason: z.string().max(2000).optional(), +}); +export type ApprovalUpdate = z.infer; + +export const GrantStatus = z.enum(["active", "revoked", "expired"]); +export type GrantStatus = z.infer; + +/** 持续授权:绑定策略版本、动作、对象范围与预算 */ +export const AutomationGrant = EntityBase.extend({ + action: z.string().min(1), + policyRevisionId: Id.nullable(), + scope: JsonObject, + budget: JsonObject, + status: GrantStatus, + grantedBy: z.string().min(1), + reason: z.string().nullable(), + expiresAt: Timestamp.nullable(), + revokedAt: Timestamp.nullable(), + revokedBy: z.string().nullable(), +}); +export type AutomationGrant = z.infer; +export const AutomationGrantCreate = AutomationGrant.pick({ + action: true, + policyRevisionId: true, + scope: true, + budget: true, + reason: true, + expiresAt: true, +}).partial({ policyRevisionId: true, scope: true, budget: true, reason: true, expiresAt: true }); +export type AutomationGrantCreate = z.infer; +/** 只能撤销或缩短有效期,不能就地扩大范围(扩大 = 新授权) */ +export const AutomationGrantUpdate = z.object({ + status: z.literal("revoked").optional(), + expiresAt: Timestamp.nullable().optional(), + reason: z.string().max(2000).optional(), +}); +export type AutomationGrantUpdate = z.infer; diff --git a/spikes/deno/packages/contracts/src/api.ts b/spikes/deno/packages/contracts/src/api.ts new file mode 100644 index 00000000..fdb52963 --- /dev/null +++ b/spikes/deno/packages/contracts/src/api.ts @@ -0,0 +1,783 @@ +import { z } from "zod"; +import * as agent from "./agent"; +import * as assets from "./assets"; +import { CapabilityDescriptor, KnownCapabilityId } from "./capability"; +import { type ApiScope, CursorQuery, cursorPage, Id, IdParam } from "./common"; +import * as content from "./content"; +import * as execution from "./execution"; +import * as identity from "./identity"; +import * as instances from "./instances"; +import * as ops from "./ops"; +import * as organization from "./organization"; +import * as policy from "./policy"; +import { GlobalSettings, GlobalSettingsUpdate } from "./settings"; +import * as sources from "./sources"; + +// 所有 REST 端点的路径常量与请求/响应 schema 汇总。 +// path 相对 API_PREFIX,参数用 OpenAPI 风格 `{id}`(server 的 createRoute 直接使用); +// web 用 buildPath() 填参数。响应体统一套 apiResponse() 信封,这里的 response 是 data 部分。 + +export const API_PREFIX = "/api/v1"; + +export type HttpMethod = "get" | "post" | "patch" | "put" | "delete"; + +export interface EndpointDef { + method: HttpMethod; + path: string; + scope: ApiScope; + summary: string; + params?: z.ZodObject; + query?: z.ZodType; + body?: z.ZodType; + response: z.ZodType; +} + +const ep = (e: E): E => e; + +/** 列表查询:游标分页 + 资源自有过滤 */ +const listQuery = (filters: S) => CursorQuery.extend(filters); + +/** 删除/无内容动作的响应 */ +export const OkResult = z.object({ ok: z.literal(true) }); + +interface CrudSchemas { + row: z.ZodType; + create?: z.ZodType; + update?: z.ZodType; + filters?: z.ZodRawShape; +} + +/** 标准资源端点:list / get / create / update / remove(按给出的 schema 决定是否生成) */ +function crud( + base: B, + name: string, + s: CrudSchemas, + scopes: { read: ApiScope; write: ApiScope }, +) { + const item = `${base}/{id}` as const; + return { + list: ep({ + method: "get", + path: base, + scope: scopes.read, + summary: `${name}列表`, + query: listQuery(s.filters ?? {}), + response: cursorPage(s.row), + }), + get: ep({ + method: "get", + path: item, + scope: scopes.read, + summary: `${name}详情`, + params: IdParam, + response: s.row, + }), + ...(s.create + ? { + create: ep({ + method: "post", + path: base, + scope: scopes.write, + summary: `新建${name}`, + body: s.create, + response: s.row, + }), + } + : {}), + ...(s.update + ? { + update: ep({ + method: "patch", + path: item, + scope: scopes.write, + summary: `修改${name}`, + params: IdParam, + body: s.update, + response: s.row, + }), + } + : {}), + }; +} + +const R = { read: "read", write: "admin" } as const; + +const del = (path: string, summary: string) => + ep({ method: "delete", path, scope: "admin", summary, params: IdParam, response: OkResult }); + +export const Api = { + // ── 内容 / 身份 ── + mediaEntities: { + ...crud( + "/media-entities", + "作品", + { + row: content.MediaEntity, + create: content.MediaEntityCreate, + update: content.MediaEntityUpdate, + filters: { + kind: content.MediaKind.optional(), + status: content.MediaEntityStatus.optional(), + q: z.string().max(200).optional(), + }, + }, + R, + ), + merge: ep({ + method: "post", + path: "/media-entities/{id}/merge", + scope: "admin", + summary: "合并重复作品(保留旧 ID 跳转)", + params: IdParam, + body: z.object({ intoId: Id, reason: z.string().max(2000) }), + response: content.MediaEntity, + }), + }, + contentUnits: crud( + "/content-units", + "内容单元", + { + row: content.ContentUnit, + create: content.ContentUnitCreate, + update: content.ContentUnitUpdate, + filters: { mediaEntityId: Id.optional(), seasonNumber: z.coerce.number().int().optional() }, + }, + R, + ), + titleAliases: { + ...crud( + "/title-aliases", + "别名", + { + row: content.TitleAlias, + create: content.TitleAliasCreate, + update: content.TitleAliasUpdate, + filters: { mediaEntityId: Id.optional(), q: z.string().max(200).optional() }, + }, + R, + ), + remove: del("/title-aliases/{id}", "删除别名"), + }, + externalIdentities: { + ...crud( + "/external-identities", + "外部身份", + { + row: identity.ExternalIdentity, + create: identity.ExternalIdentityCreate, + update: identity.ExternalIdentityUpdate, + filters: { + mediaEntityId: Id.optional(), + provider: z.string().optional(), + status: identity.IdentityStatus.optional(), + }, + }, + R, + ), + revisions: ep({ + method: "get", + path: "/external-identities/{id}/revisions", + scope: "read", + summary: "身份修订历史", + params: IdParam, + response: z.object({ items: z.array(identity.IdentityRevision) }), + }), + }, + + // ── 文件 ── + mediaAssets: crud( + "/media-assets", + "资产", + { + row: assets.MediaAsset, + update: assets.MediaAssetUpdate, + filters: { mediaEntityId: Id.optional(), status: assets.AssetStatus.optional() }, + }, + R, + ), + assetUnitCoverage: crud( + "/asset-unit-coverage", + "资产覆盖", + { + row: assets.AssetUnitCoverage, + create: assets.AssetUnitCoverageCreate, + update: assets.AssetUnitCoverageUpdate, + filters: { mediaAssetId: Id.optional(), contentUnitId: Id.optional() }, + }, + R, + ), + assetLocations: crud( + "/asset-locations", + "资产位置", + { + row: assets.AssetLocation, + update: assets.AssetLocationUpdate, + filters: { mediaAssetId: Id.optional(), status: assets.LocationStatus.optional() }, + }, + R, + ), + pathBindings: crud( + "/path-bindings", + "路径绑定", + { + row: assets.PathBinding, + create: assets.PathBindingCreate, + update: assets.PathBindingUpdate, + filters: { mediaEntityId: Id.optional(), status: assets.BindingStatus.optional() }, + }, + R, + ), + + // ── 实例 ── + libraryRoots: crud( + "/library-roots", + "媒体库根", + { + row: instances.LibraryRoot, + create: instances.LibraryRootCreate, + update: instances.LibraryRootUpdate, + }, + R, + ), + downloaderInstances: { + ...crud( + "/downloader-instances", + "下载器实例", + { + row: instances.DownloaderInstance, + create: instances.DownloaderInstanceCreate, + update: instances.DownloaderInstanceUpdate, + }, + R, + ), + test: ep({ + method: "post", + path: "/downloader-instances/{id}/test", + scope: "admin", + summary: "测试下载器连接", + params: IdParam, + response: z.object({ ok: z.boolean(), version: z.string().nullable(), message: z.string().nullable() }), + }), + }, + mediaServerInstances: crud( + "/media-server-instances", + "媒体服务器实例", + { + row: instances.MediaServerInstance, + create: instances.MediaServerInstanceCreate, + update: instances.MediaServerInstanceUpdate, + }, + R, + ), + pathMappings: { + ...crud( + "/path-mappings", + "路径映射", + { + row: instances.PathMapping, + create: instances.PathMappingCreate, + update: instances.PathMappingUpdate, + }, + R, + ), + verify: ep({ + method: "post", + path: "/path-mappings/{id}/verify", + scope: "admin", + summary: "校验路径映射可读写与挂载健康", + params: IdParam, + response: instances.PathMapping, + }), + remove: del("/path-mappings/{id}", "删除路径映射"), + }, + + // ── 来源 ── + sites: crud("/sites", "站点", { row: sources.Site, create: sources.SiteCreate, update: sources.SiteUpdate }, R), + siteAccounts: crud( + "/site-accounts", + "站点账号", + { + row: sources.SiteAccount, + create: sources.SiteAccountCreate, + update: sources.SiteAccountUpdate, + filters: { siteId: Id.optional() }, + }, + R, + ), + feedSources: { + ...crud( + "/feed-sources", + "Feed", + { + row: sources.FeedSource, + create: sources.FeedSourceCreate, + update: sources.FeedSourceUpdate, + filters: { siteId: Id.optional(), health: sources.FeedHealth.optional() }, + }, + R, + ), + fetchNow: ep({ + method: "post", + path: "/feed-sources/{id}/fetch", + scope: "admin", + summary: "立即抓取(仍受站点限流)", + params: IdParam, + response: z.object({ taskId: Id }), + }), + }, + sourceReleases: crud( + "/source-releases", + "站点资源", + { + row: sources.SourceRelease, + filters: { + siteId: Id.optional(), + mediaEntityId: Id.optional(), + resolutionStatus: sources.ResolutionStatus.optional(), + q: z.string().max(200).optional(), + }, + }, + R, + ), + observations: crud( + "/observations", + "观察", + { + row: sources.Observation, + filters: { feedId: Id.optional(), status: sources.ObservationStatus.optional() }, + }, + R, + ), + + // ── 规则 ── + subscriptions: crud( + "/subscriptions", + "订阅", + { + row: policy.Subscription, + create: policy.SubscriptionCreate, + update: policy.SubscriptionUpdate, + filters: { status: policy.SubscriptionStatus.optional(), mediaEntityId: Id.optional() }, + }, + R, + ), + policyRevisions: { + ...crud( + "/policy-revisions", + "策略版本", + { + row: policy.PolicyRevision, + create: policy.PolicyRevisionCreate, + update: policy.PolicyRevisionUpdate, + filters: { policyKey: z.string().optional(), status: policy.PolicyStatus.optional() }, + }, + R, + ), + preview: ep({ + method: "post", + path: "/policy-revisions/{id}/preview", + scope: "read", + summary: "历史回放预览", + params: IdParam, + response: policy.PolicyRevision, + }), + publish: ep({ + method: "post", + path: "/policy-revisions/{id}/publish", + scope: "admin", + summary: "发布策略版本", + params: IdParam, + body: z.object({ expectedRevision: z.number().int().min(1) }), + response: policy.PolicyRevision, + }), + rollback: ep({ + method: "post", + path: "/policy-revisions/{id}/rollback", + scope: "admin", + summary: "回滚到该版本", + params: IdParam, + response: policy.PolicyRevision, + }), + }, + acquisitionSlots: crud( + "/acquisition-slots", + "收录目标", + { + row: policy.AcquisitionSlot, + create: policy.AcquisitionSlotCreate, + update: policy.AcquisitionSlotUpdate, + filters: { mediaEntityId: Id.optional(), status: policy.SlotStatus.optional() }, + }, + R, + ), + preferenceTags: { + ...crud( + "/preference-tags", + "偏好标签", + { + row: policy.PreferenceTag, + create: policy.PreferenceTagCreate, + update: policy.PreferenceTagUpdate, + filters: { status: policy.PreferenceStatus.optional(), kind: policy.PreferenceKind.optional() }, + }, + R, + ), + /** 删除 = 标记 deleted + 写抑制记录 */ + remove: del("/preference-tags/{id}", "删除偏好标签(留抑制记录)"), + }, + preferenceSuppressions: crud( + "/preference-suppressions", + "偏好抑制", + { + row: policy.PreferenceSuppression, + create: policy.PreferenceSuppressionCreate, + update: policy.PreferenceSuppressionUpdate, + filters: { active: z.enum(["true", "false"]).optional() }, + }, + R, + ), + + // ── 执行 ── + tasks: { + ...crud( + "/tasks", + "任务", + { + row: execution.Task, + filters: { + status: execution.TaskStatus.optional(), + kind: execution.TaskKind.optional(), + subjectId: Id.optional(), + }, + }, + R, + ), + cancel: ep({ + method: "post", + path: "/tasks/{id}/cancel", + scope: "organize.execute", + summary: "取消任务(停止未来步骤)", + params: IdParam, + body: execution.TaskCancelRequest, + response: execution.Task, + }), + retry: ep({ + method: "post", + path: "/tasks/{id}/retry", + scope: "organize.execute", + summary: "重试失败任务", + params: IdParam, + response: execution.Task, + }), + operations: ep({ + method: "get", + path: "/tasks/{id}/operations", + scope: "read", + summary: "任务的操作账本(业务时间线)", + params: IdParam, + response: z.object({ items: z.array(execution.AssetOperation) }), + }), + }, + acquisitionPlans: crud( + "/acquisition-plans", + "下载计划", + { + row: execution.AcquisitionPlan, + filters: { slotId: Id.optional(), status: execution.PlanStatus.optional() }, + }, + R, + ), + downloadTasks: { + ...crud( + "/download-tasks", + "下载任务", + { + row: execution.DownloadTask, + filters: { + instanceId: Id.optional(), + status: execution.DownloadStatus.optional(), + managed: z.enum(["true", "false"]).optional(), + }, + }, + R, + ), + pause: ep({ + method: "post", + path: "/download-tasks/{id}/pause", + scope: "organize.execute", + summary: "暂停(记为用户暂停,阈值恢复不会自动恢复)", + params: IdParam, + response: execution.DownloadTask, + }), + resume: ep({ + method: "post", + path: "/download-tasks/{id}/resume", + scope: "organize.execute", + summary: "恢复下载", + params: IdParam, + response: execution.DownloadTask, + }), + }, + + // ── 人工 / AI ── + decisions: { + ...crud( + "/decisions", + "待处理事项", + { + row: agent.Decision, + filters: { status: agent.DecisionStatus.optional(), kind: agent.DecisionKind.optional() }, + }, + R, + ), + resolve: ep({ + method: "post", + path: "/decisions/{id}/resolve", + scope: "organize.execute", + summary: "处理待处理事项", + params: IdParam, + body: agent.DecisionResolveInput, + response: agent.Decision, + }), + }, + approvals: crud( + "/approvals", + "审批", + { + row: agent.Approval, + update: agent.ApprovalUpdate, + filters: { status: agent.ApprovalStatus.optional(), planId: Id.optional() }, + }, + { read: "read", write: "organize.execute" }, + ), + automationGrants: crud( + "/automation-grants", + "持续授权", + { + row: agent.AutomationGrant, + create: agent.AutomationGrantCreate, + update: agent.AutomationGrantUpdate, + filters: { status: agent.GrantStatus.optional() }, + }, + R, + ), + agentRuns: { + ...crud( + "/agent-runs", + "Agent 运行", + { + row: agent.AgentRun, + create: agent.AgentRunCreate, + filters: { status: agent.AgentRunStatus.optional(), taskId: Id.optional() }, + }, + { read: "read", write: "organize.execute" }, + ), + cancel: ep({ + method: "post", + path: "/agent-runs/{id}/cancel", + scope: "organize.execute", + summary: "取消 Agent 运行", + params: IdParam, + response: agent.AgentRun, + }), + message: ep({ + method: "post", + path: "/agent-runs/{id}/messages", + scope: "organize.execute", + summary: "向运行中的会话追加用户消息(纠正/补充)", + params: IdParam, + body: z.object({ text: z.string().min(1).max(8000) }), + response: agent.AgentRun, + }), + }, + artifacts: crud( + "/artifacts", + "产物", + { + row: agent.Artifact, + filters: { agentRunId: Id.optional(), taskId: Id.optional(), kind: agent.ArtifactKind.optional() }, + }, + R, + ), + + // ── 运维 ── + reconcileRuns: crud( + "/reconcile-runs", + "校验运行", + { + row: ops.ReconcileRun, + create: ops.ReconcileRunCreate, + filters: { kind: ops.ReconcileKind.optional(), status: ops.ReconcileStatus.optional() }, + }, + R, + ), + serverMappings: crud( + "/server-mappings", + "库存映射", + { + row: ops.ServerMapping, + update: ops.ServerMappingUpdate, + filters: { + mediaServerInstanceId: Id.optional(), + mediaEntityId: Id.optional(), + matchStatus: ops.ServerMatchStatus.optional(), + }, + }, + R, + ), + apiKeys: { + list: ep({ + method: "get", + path: "/api-keys", + scope: "admin", + summary: "API Key 列表", + query: CursorQuery, + response: cursorPage(ops.ApiKey), + }), + create: ep({ + method: "post", + path: "/api-keys", + scope: "admin", + summary: "新建 API Key(明文只返回一次)", + body: ops.ApiKeyCreate, + response: ops.ApiKeyCreated, + }), + update: ep({ + method: "patch", + path: "/api-keys/{id}", + scope: "admin", + summary: "修改/撤销 API Key", + params: IdParam, + body: ops.ApiKeyUpdate, + response: ops.ApiKey, + }), + }, + auditEvents: { + list: ep({ + method: "get", + path: "/audit-events", + scope: "admin", + summary: "审计事件", + query: listQuery({ + targetType: z.string().optional(), + targetId: z.string().optional(), + action: z.string().optional(), + }), + response: cursorPage(ops.AuditEvent), + }), + }, + secrets: { + list: ep({ + method: "get", + path: "/secrets", + scope: "admin", + summary: "凭证元数据列表", + query: CursorQuery, + response: cursorPage(ops.Secret), + }), + create: ep({ + method: "post", + path: "/secrets", + scope: "admin", + summary: "新建凭证(只写)", + body: ops.SecretCreate, + response: ops.Secret, + }), + update: ep({ + method: "patch", + path: "/secrets/{id}", + scope: "admin", + summary: "轮换凭证", + params: IdParam, + body: ops.SecretUpdate, + response: ops.Secret, + }), + }, + + // ── 整理(公开合同) ── + organization: { + list: ep({ + method: "get", + path: "/organization-records", + scope: "organize.read", + summary: "整理记录列表", + query: organization.OrganizationListQuery, + response: cursorPage(organization.PublicOrganizationRecord), + }), + get: ep({ + method: "get", + path: "/organization-records/{id}", + scope: "organize.read", + summary: "整理记录详情(含逐文件结果)", + params: IdParam, + response: organization.PublicOrganizationRecord, + }), + changes: ep({ + method: "get", + path: "/organization-records/changes", + scope: "organize.read", + summary: "按 changeSeq 增量读取(含既有记录状态更新)", + query: organization.OrganizationChangesQuery, + response: organization.OrganizationChangesResponse, + }), + create: ep({ + method: "post", + path: "/organization-records", + scope: "organize.execute", + summary: "手动发起整理", + body: organization.OrganizeRequest, + response: organization.OrganizeAccepted, + }), + }, + + // ── 能力 ── + capabilities: { + list: ep({ + method: "get", + path: "/capabilities", + scope: "read", + summary: "能力注册中心描述", + response: z.object({ items: z.array(CapabilityDescriptor) }), + }), + invoke: ep({ + method: "post", + path: "/capabilities/{capabilityId}/invoke", + scope: "read", + summary: "调用能力(实际所需 scope 见描述符;服务端按能力再次鉴权)", + params: z.object({ capabilityId: KnownCapabilityId }), + body: z.object({ input: z.unknown(), preview: z.boolean().default(false) }), + response: z.object({ output: z.unknown(), taskId: Id.nullable(), planId: Id.nullable() }), + }), + }, + + // ── 设置 ── + settings: { + get: ep({ method: "get", path: "/settings", scope: "read", summary: "全局设置", response: GlobalSettings }), + update: ep({ + method: "patch", + path: "/settings", + scope: "admin", + summary: "修改全局设置", + body: GlobalSettingsUpdate, + response: GlobalSettings, + }), + }, +} as const; + +/** 用参数填充 `{name}` 路径占位,返回带 API_PREFIX 的完整路径 */ +export function buildPath(path: string, params: Record = {}): string { + const filled = path.replace(/\{(\w+)\}/g, (_, k: string) => { + const v = params[k]; + if (v === undefined) throw new Error(`缺少路径参数 ${k}`); + return encodeURIComponent(v); + }); + return `${API_PREFIX}${filled}`; +} + +/** 扁平列出全部端点(生成 OpenAPI、权限表与测试用) */ +export function listEndpoints(): EndpointDef[] { + return Object.values(Api).flatMap((group) => Object.values(group) as EndpointDef[]); +} + +export type EndpointBody = E["body"] extends z.ZodType ? z.input : never; +export type EndpointResponse = z.infer; diff --git a/spikes/deno/packages/contracts/src/assets.ts b/spikes/deno/packages/contracts/src/assets.ts new file mode 100644 index 00000000..4e744fd5 --- /dev/null +++ b/spikes/deno/packages/contracts/src/assets.ts @@ -0,0 +1,188 @@ +import { z } from "zod"; +import { EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common"; +import { Segment } from "./content"; + +// 文件域:资产、资产-内容单元覆盖、资产位置、路径绑定(REQ §7.1、§8.2、§12.2) +// 资产与位置分离;path_binding 一旦建立不因元数据刷新改变,只有显式整理可替换(旧绑定标 superseded)。 + +export const AssetKind = z.enum(["video", "bluray_dir", "subtitle", "audio", "nfo", "image", "other"]); +export type AssetKind = z.infer; +export const AssetStatus = z.enum(["active", "replaced", "retired", "missing"]); +export type AssetStatus = z.infer; + +/** 交付特征(分辨率/编码/音轨/来源类型/发布组),与内容版本 contentEdition 分开(REQ §6.1) */ +export const DeliveryTraits = z.object({ + resolution: z.string().optional(), + videoCodec: z.string().optional(), + audioCodecs: z.array(z.string()).optional(), + hdr: z.array(z.string()).optional(), + sourceType: z.string().optional(), + releaseGroup: z.string().optional(), + subtitles: z.array(z.string()).optional(), +}); +export type DeliveryTraits = z.infer; + +export const MediaAsset = EntityBase.extend({ + mediaEntityId: Id.nullable(), + kind: AssetKind, + /** 附属文件(字幕/音轨)指向主视频资产 */ + parentAssetId: Id.nullable(), + sizeBytes: NonNegInt.nullable(), + contentHash: z.string().nullable(), + hashAlgorithm: z.string().nullable(), + sampleDigest: z.string().nullable(), + /** ffprobe/MediaInfo 结果 */ + mediaInfo: JsonObject, + delivery: DeliveryTraits, + /** 内容版本:院线版/导演剪辑版等 */ + contentEdition: z.string().nullable(), + sourceReleaseId: Id.nullable(), + downloadTaskId: Id.nullable(), + status: AssetStatus, + verifiedAt: Timestamp.nullable(), +}); +export type MediaAsset = z.infer; +export const MediaAssetCreate = MediaAsset.omit(SERVER_MANAGED).partial({ + mediaEntityId: true, + parentAssetId: true, + sizeBytes: true, + contentHash: true, + hashAlgorithm: true, + sampleDigest: true, + mediaInfo: true, + delivery: true, + contentEdition: true, + sourceReleaseId: true, + downloadTaskId: true, + status: true, + verifiedAt: true, +}); +export type MediaAssetCreate = z.infer; +export const MediaAssetUpdate = MediaAssetCreate.partial(); +export type MediaAssetUpdate = z.infer; + +export const CoverageKind = z.enum(["full", "partial", "segment", "unknown"]); +export type CoverageKind = z.infer; + +/** 一个资产覆盖哪些内容单元(E01-E03 合集、上/下分段) */ +export const AssetUnitCoverage = EntityBase.extend({ + mediaAssetId: Id, + contentUnitId: Id, + coverage: CoverageKind, + segment: Segment.nullable(), + evidence: JsonObject, +}); +export type AssetUnitCoverage = z.infer; +export const AssetUnitCoverageCreate = AssetUnitCoverage.omit(SERVER_MANAGED).partial({ + coverage: true, + segment: true, + evidence: true, +}); +export type AssetUnitCoverageCreate = z.infer; +export const AssetUnitCoverageUpdate = AssetUnitCoverageCreate.omit({ + mediaAssetId: true, + contentUnitId: true, +}).partial(); +export type AssetUnitCoverageUpdate = z.infer; + +export const LocationKind = z.enum(["library", "download_cache", "hardlink", "external"]); +export type LocationKind = z.infer; +/** 路径不存在时先区分根离线/权限/外部移动,再按 unavailable → suspected_missing → confirmed_missing 推进 */ +export const LocationStatus = z.enum(["available", "unavailable", "suspected_missing", "confirmed_missing", "removed"]); +export type LocationStatus = z.infer; + +export const AssetLocation = EntityBase.extend({ + mediaAssetId: Id, + locationKind: LocationKind, + libraryRootId: Id.nullable(), + downloaderInstanceId: Id.nullable(), + /** 相对库根(或下载器保存根)的路径 */ + relativePath: z.string().min(1), + status: LocationStatus, + isPrimary: z.boolean(), + sizeBytes: NonNegInt.nullable(), + inode: z.number().int().nullable(), + device: z.number().int().nullable(), + mtime: Timestamp.nullable(), + sidecarOfLocationId: Id.nullable(), + lastSeenAt: Timestamp.nullable(), + lastCheckedAt: Timestamp.nullable(), + checkDetail: JsonObject, +}); +export type AssetLocation = z.infer; +export const AssetLocationCreate = AssetLocation.omit(SERVER_MANAGED) + .partial({ + locationKind: true, + libraryRootId: true, + downloaderInstanceId: true, + status: true, + isPrimary: true, + sizeBytes: true, + inode: true, + device: true, + mtime: true, + sidecarOfLocationId: true, + lastSeenAt: true, + lastCheckedAt: true, + checkDetail: true, + }) + .refine((v) => (v.locationKind ?? "library") !== "library" || !!v.libraryRootId, { + message: "library 位置必须指定 libraryRootId", + }); +export type AssetLocationCreate = z.infer; +export const AssetLocationUpdate = AssetLocation.pick({ + status: true, + isPrimary: true, + sizeBytes: true, + inode: true, + device: true, + mtime: true, + lastSeenAt: true, + lastCheckedAt: true, + checkDetail: true, +}).partial(); +export type AssetLocationUpdate = z.infer; + +export const BindingKind = z.enum(["entity", "season"]); +export type BindingKind = z.infer; +export const BindingStatus = z.enum(["active", "conflict", "superseded"]); +export type BindingStatus = z.infer; + +export const PathBinding = EntityBase.extend({ + mediaEntityId: Id, + contentUnitId: Id.nullable(), + seasonNumber: z.number().int().nonnegative().nullable(), + bindingKind: BindingKind, + libraryRootId: Id, + relativePath: z.string().min(1), + templateVersion: z.string().nullable(), + status: BindingStatus, + boundAt: Timestamp, + boundBy: z.string().nullable(), + supersededById: Id.nullable(), + note: z.string().nullable(), +}); +export type PathBinding = z.infer; +export const PathBindingCreate = PathBinding.pick({ + mediaEntityId: true, + contentUnitId: true, + seasonNumber: true, + bindingKind: true, + libraryRootId: true, + relativePath: true, + templateVersion: true, + status: true, + note: true, +}) + .partial({ contentUnitId: true, seasonNumber: true, templateVersion: true, status: true, note: true }) + .refine((v) => (v.bindingKind === "season") === (v.seasonNumber !== undefined && v.seasonNumber !== null), { + message: "season 绑定必须给 seasonNumber,entity 绑定不能给", + }); +export type PathBindingCreate = z.infer; +/** 绑定路径不可就地修改:只能改备注或标记冲突/被替代;换目录走整理操作生成新绑定 */ +export const PathBindingUpdate = z.object({ + status: BindingStatus.optional(), + supersededById: Id.nullable().optional(), + note: z.string().nullable().optional(), +}); +export type PathBindingUpdate = z.infer; diff --git a/spikes/deno/packages/contracts/src/capabilities-io.ts b/spikes/deno/packages/contracts/src/capabilities-io.ts new file mode 100644 index 00000000..4cafc07e --- /dev/null +++ b/spikes/deno/packages/contracts/src/capabilities-io.ts @@ -0,0 +1,218 @@ +import { z } from "zod"; +import { MediaAsset } from "./assets"; +import type { KnownCapabilityId } from "./capability"; +import { Id, JsonObject, NonNegInt } from "./common"; +import { MediaEntity, MediaKind } from "./content"; +import { AcquisitionPlan, DownloadTask, Task, TaskStatus } from "./execution"; +import { ExternalEntityType, ExternalIdentity } from "./identity"; +import { ServerMapping } from "./ops"; +import { OrganizationAction, OrganizationRecord } from "./organization"; +import { AcquisitionSlot, PolicyRevision } from "./policy"; +import { SourceRelease } from "./sources"; + +// 首批能力的输入/输出 schema;名字与 CAPABILITY_CATALOG 中的 inputSchema/outputSchema 一致。 +// 写能力的输入只接受计划/对象 ID,不接受模型自拟的幂等键或任意路径。 + +const Limit = z.number().int().min(1).max(100).default(20); + +/** 写能力的通用受理结果:返回 task/plan ID,由任务页追踪(REQ §12.3) */ +export const Accepted = z.object({ taskId: Id, planId: Id.nullable() }); +export type Accepted = z.infer; + +export const CatalogLookupInput = z + .object({ + query: z.string().min(1).max(200).optional(), + externalId: z.object({ provider: z.string(), entityType: ExternalEntityType, externalId: z.string() }).optional(), + kind: MediaKind.optional(), + year: z.number().int().optional(), + includeRemote: z.boolean().default(false), + limit: Limit, + }) + .refine((v) => !!v.query || !!v.externalId, { message: "query 与 externalId 至少给一个" }); +export const CatalogLookupOutput = z.object({ + local: z.array(z.object({ entity: MediaEntity, score: z.number(), matchedAlias: z.string().nullable() })), + remote: z.array( + z.object({ + provider: z.string(), + entityType: ExternalEntityType, + externalId: z.string(), + title: z.string(), + year: z.number().int().nullable(), + }), + ), +}); + +export const ReleaseSearchInput = z.object({ + query: z.string().min(1).max(200).optional(), + mediaEntityId: Id.optional(), + siteIds: z.array(Id).optional(), + /** 本地快照不足时是否实时补查 */ + allowLiveSearch: z.boolean().default(true), + limit: Limit, +}); +export const ReleaseSearchOutput = z.object({ + items: z.array(SourceRelease), + /** 各站点快照新鲜度与是否实时补查 */ + coverage: z.array( + z.object({ siteId: Id, fromSnapshot: z.boolean(), live: z.boolean(), error: z.string().nullable() }), + ), +}); + +export const ReleaseInspectInput = z.object({ sourceReleaseId: Id, refreshDetail: z.boolean().default(false) }); +export const ReleaseInspectOutput = z.object({ release: SourceRelease, inventory: z.array(MediaAsset) }); + +export const IdentityResolveInput = z + .object({ + observationId: Id.optional(), + sourceReleaseId: Id.optional(), + candidate: z + .object({ + mediaEntityId: Id.optional(), + provider: z.string().optional(), + entityType: ExternalEntityType.optional(), + externalId: z.string().optional(), + }) + .optional(), + evidenceIds: z.array(Id).default([]), + }) + .refine((v) => !!v.observationId || !!v.sourceReleaseId, { message: "需要 observationId 或 sourceReleaseId" }); +export const IdentityResolveOutput = z.object({ + status: z.enum(["resolved", "proposed", "ambiguous", "unidentifiable"]), + mediaEntityId: Id.nullable(), + identities: z.array(ExternalIdentity), + decisionId: Id.nullable(), +}); + +export const DownloadPlanInput = z.object({ slotId: Id, sourceReleaseId: Id }); +export const DownloadPlanOutput = z.object({ + plan: AcquisitionPlan.nullable(), + slot: AcquisitionSlot, + /** 硬条件不满足或预算/阈值阻止时的原因 */ + rejected: z.array(z.object({ code: z.string(), message: z.string() })), + requiresApproval: z.boolean(), +}); + +export const DownloadSubmitInput = z.object({ planId: Id, approvalId: Id.optional() }); +export const DownloadSubmitOutput = Accepted.extend({ downloadTask: DownloadTask.nullable() }); + +export const OrganizePlanInput = z + .object({ + downloadTaskId: Id.optional(), + mediaAssetIds: z.array(Id).optional(), + action: OrganizationAction.default("copy"), + targetLibraryRootId: Id.optional(), + }) + .refine((v) => !!v.downloadTaskId || !!v.mediaAssetIds?.length, { message: "需要 downloadTaskId 或 mediaAssetIds" }); +export const OrganizePlanOutput = z.object({ + planId: Id, + payloadHash: z.string(), + files: z.array( + z.object({ + sourcePath: z.string(), + targetPath: z.string().nullable(), + action: OrganizationAction, + skipReason: z.string().nullable(), + }), + ), + conflicts: z.array(z.object({ code: z.string(), message: z.string(), path: z.string().nullable() })), + requiresApproval: z.boolean(), +}); + +export const OrganizeExecuteInput = z.object({ planId: Id, payloadHash: z.string().min(1), approvalId: Id.optional() }); +export const OrganizeExecuteOutput = Accepted.extend({ record: OrganizationRecord.nullable() }); + +export const TaskInspectInput = z.object({ taskId: Id }); +export const TaskInspectOutput = z.object({ + task: Task, + children: z.array(Task), + timeline: z.array(z.object({ at: z.string(), type: z.string(), summary: z.string(), data: JsonObject.optional() })), +}); + +export const TaskRetryInput = z.object({ taskId: Id, reason: z.string().max(500).optional() }); +export const TaskRetryOutput = z.object({ taskId: Id, status: TaskStatus }); + +export const TaskCancelInput = z.object({ taskId: Id, reason: z.string().min(1).max(500) }); +export const TaskCancelOutput = z.object({ taskId: Id, status: TaskStatus, cancelRequested: z.boolean() }); + +export const PolicyPreviewInput = z.object({ + policyRevisionId: Id, + sampleLimit: z.number().int().min(1).max(1000).default(200), +}); +export const PolicyPreviewOutput = z.object({ + newlyMatched: NonNegInt, + newlyExcluded: NonNegInt, + samples: z.array(z.object({ sourceReleaseId: Id, before: z.string(), after: z.string(), reason: z.string() })), +}); + +export const PolicyApplyInput = z.object({ + policyRevisionId: Id, + expectedRevision: z.number().int().min(1), + approvalId: Id.optional(), +}); +export const PolicyApplyOutput = z.object({ policy: PolicyRevision }); + +export const InventoryQueryInput = z.object({ + mediaEntityId: Id.optional(), + query: z.string().max(200).optional(), + includeServerInventory: z.boolean().default(false), + limit: Limit, +}); +export const InventoryQueryOutput = z.object({ + items: z.array( + z.object({ + entity: MediaEntity, + assets: z.array(MediaAsset), + slots: z.array(AcquisitionSlot), + identities: z.array(ExternalIdentity.pick({ provider: true, entityType: true, externalId: true, status: true })), + /** 外部库存证据;"未知 ≠ 缺失" */ + server: z.array(ServerMapping), + }), + ), +}); + +/** 能力 ID → 输入/输出 schema(server 注册处理器、agent 生成工具定义时共用) */ +export const CAPABILITY_IO = { + "catalog.lookup": { input: CatalogLookupInput, output: CatalogLookupOutput }, + "release.search": { input: ReleaseSearchInput, output: ReleaseSearchOutput }, + "release.inspect": { input: ReleaseInspectInput, output: ReleaseInspectOutput }, + "identity.resolve": { input: IdentityResolveInput, output: IdentityResolveOutput }, + "download.plan": { input: DownloadPlanInput, output: DownloadPlanOutput }, + "download.submit": { input: DownloadSubmitInput, output: DownloadSubmitOutput }, + "organize.plan": { input: OrganizePlanInput, output: OrganizePlanOutput }, + "organize.execute": { input: OrganizeExecuteInput, output: OrganizeExecuteOutput }, + "task.inspect": { input: TaskInspectInput, output: TaskInspectOutput }, + "task.retry": { input: TaskRetryInput, output: TaskRetryOutput }, + "task.cancel": { input: TaskCancelInput, output: TaskCancelOutput }, + "policy.preview": { input: PolicyPreviewInput, output: PolicyPreviewOutput }, + "policy.apply": { input: PolicyApplyInput, output: PolicyApplyOutput }, + "inventory.query": { input: InventoryQueryInput, output: InventoryQueryOutput }, +} as const satisfies Record; + +export type CatalogLookupInput = z.infer; +export type CatalogLookupOutput = z.infer; +export type ReleaseSearchInput = z.infer; +export type ReleaseSearchOutput = z.infer; +export type ReleaseInspectInput = z.infer; +export type ReleaseInspectOutput = z.infer; +export type IdentityResolveInput = z.infer; +export type IdentityResolveOutput = z.infer; +export type DownloadPlanInput = z.infer; +export type DownloadPlanOutput = z.infer; +export type DownloadSubmitInput = z.infer; +export type DownloadSubmitOutput = z.infer; +export type OrganizePlanInput = z.infer; +export type OrganizePlanOutput = z.infer; +export type OrganizeExecuteInput = z.infer; +export type OrganizeExecuteOutput = z.infer; +export type TaskInspectInput = z.infer; +export type TaskInspectOutput = z.infer; +export type TaskRetryInput = z.infer; +export type TaskRetryOutput = z.infer; +export type TaskCancelInput = z.infer; +export type TaskCancelOutput = z.infer; +export type PolicyPreviewInput = z.infer; +export type PolicyPreviewOutput = z.infer; +export type PolicyApplyInput = z.infer; +export type PolicyApplyOutput = z.infer; +export type InventoryQueryInput = z.infer; +export type InventoryQueryOutput = z.infer; diff --git a/spikes/deno/packages/contracts/src/capability.ts b/spikes/deno/packages/contracts/src/capability.ts new file mode 100644 index 00000000..2aff4c78 --- /dev/null +++ b/spikes/deno/packages/contracts/src/capability.ts @@ -0,0 +1,280 @@ +import { z } from "zod"; +import { ApiScope } from "./common"; + +/** + * 能力调用上下文(REQ §10.3)。产品自有接口,不是 Pi SDK 原生 API。 + * signal 由任务取消传播而来。 + */ +export interface CapabilityContext { + actorId: string; + taskId: string; + scopeIds: string[]; + signal: AbortSignal; +} + +/** 变更计划:由业务端生成与保存,审批绑定 id + payloadHash + expectedRevision */ +export const MutationPlan = z.object({ + id: z.string(), + operation: z.string(), + targetIds: z.array(z.string()), + expectedRevision: z.number().int().nonnegative(), + evidenceIds: z.array(z.string()), + payloadHash: z.string(), + requiresApproval: z.boolean(), +}); +export type MutationPlan = z.infer; + +/** 能力风险级别 */ +export const CapabilityRisk = z.enum(["read", "preview", "write", "destructive"]); +export type CapabilityRisk = z.infer; + +/** 能力 ID:`域.动作`,如 download.submit */ +export const CapabilityId = z.string().regex(/^[a-z][a-z0-9]*\.[a-z][a-z0-9_]*$/); +export type CapabilityId = z.infer; + +/** 能力定义(注册中心元数据;处理器在 server 侧挂载) */ +export interface CapabilityDefinition { + id: CapabilityId; + version: number; + description: string; + input: I; + output: O; + scopes: string[]; + risk: CapabilityRisk; + idempotent: boolean; + supportsPreview: boolean; +} + +// ── 能力描述符与首批能力(任务书 §3.1-9;REQ §10.3) ── + +/** 风险级别:low 只读;medium 预览/可撤回写入;high 触发下载、文件变更或策略发布 */ +export const RiskLevel = z.enum(["low", "medium", "high"]); +export type RiskLevel = z.infer; + +/** 接口类别(REQ §12.3):read / preview / propose / approve / execute */ +export const CapabilityKind = z.enum(["read", "preview", "propose", "approve", "execute"]); +export type CapabilityKind = z.infer; + +/** 首批能力 ID */ +export const CAPABILITY_IDS = [ + "catalog.lookup", + "release.search", + "release.inspect", + "identity.resolve", + "download.plan", + "download.submit", + "organize.plan", + "organize.execute", + "task.inspect", + "task.retry", + "task.cancel", + "policy.preview", + "policy.apply", + "inventory.query", +] as const; +export const KnownCapabilityId = z.enum(CAPABILITY_IDS); +export type KnownCapabilityId = z.infer; + +/** schema 引用:指向 contracts 中导出的 schema 名(如 "ReleaseSearchInput"),便于序列化给 UI/MCP 列表 */ +export const SchemaRef = z.string().regex(/^[A-Z][A-Za-z0-9]*$/); + +/** 能力注册中心的可序列化描述(处理器在 server 侧挂载;模型只能看到白名单内的描述) */ +export const CapabilityDescriptor = z.object({ + id: CapabilityId, + version: z.number().int().min(1), + title: z.string().min(1), + description: z.string().min(1), + kind: CapabilityKind, + inputSchema: SchemaRef, + outputSchema: SchemaRef, + /** 调用所需 API scope */ + scope: ApiScope, + riskLevel: RiskLevel, + idempotent: z.boolean(), + supportsPreview: z.boolean(), +}); +export type CapabilityDescriptor = z.infer; + +/** 首批能力的描述(inputSchema/outputSchema 名对应 capabilities-io.ts 中的导出) */ +export const CAPABILITY_CATALOG: Record = { + "catalog.lookup": d( + "catalog.lookup", + "查作品", + "按标题/别名/外部 ID 查本地作品与元数据候选", + "read", + "CatalogLookup", + "read", + "low", + true, + false, + ), + "release.search": d( + "release.search", + "搜资源", + "先查本地资源索引与站点快照,不足时对已开启站点实时补查", + "read", + "ReleaseSearch", + "read", + "low", + true, + false, + ), + "release.inspect": d( + "release.inspect", + "看资源详情", + "读取站点资源详情、文件清单、促销与解析结果", + "read", + "ReleaseInspect", + "read", + "low", + true, + false, + ), + "identity.resolve": d( + "identity.resolve", + "识别身份", + "为观察/资源提出身份映射;高证据自动接受,歧义进入待处理", + "propose", + "IdentityResolve", + "organize.execute", + "medium", + true, + true, + ), + "download.plan": d( + "download.plan", + "生成下载计划", + "评估候选并生成带 Slot/预算预留的下载计划(不提交)", + "preview", + "DownloadPlan", + "organize.execute", + "medium", + true, + true, + ), + "download.submit": d( + "download.submit", + "提交下载", + "按已保存的计划提交到下载器;校验授权与硬阈值", + "execute", + "DownloadSubmit", + "organize.execute", + "high", + true, + false, + ), + "organize.plan": d( + "organize.plan", + "生成整理计划", + "冻结源/目标路径、操作类型与前置条件", + "preview", + "OrganizePlan", + "organize.read", + "low", + true, + true, + ), + "organize.execute": d( + "organize.execute", + "执行整理", + "执行已保存的整理计划(Copy/Move),生成整理记录", + "execute", + "OrganizeExecute", + "organize.execute", + "high", + true, + false, + ), + "task.inspect": d( + "task.inspect", + "查看任务", + "读取任务状态、业务时间线与错误", + "read", + "TaskInspect", + "read", + "low", + true, + false, + ), + "task.retry": d( + "task.retry", + "重试任务", + "对失败/死信任务重新排队;副作用经操作账本去重", + "execute", + "TaskRetry", + "organize.execute", + "medium", + true, + false, + ), + "task.cancel": d( + "task.cancel", + "取消任务", + "停止未来步骤,不撤销已完成的外部动作", + "execute", + "TaskCancel", + "organize.execute", + "medium", + true, + false, + ), + "policy.preview": d( + "policy.preview", + "预览策略", + "用历史数据回放策略草案,展示新增命中与误排除", + "preview", + "PolicyPreview", + "read", + "low", + true, + true, + ), + "policy.apply": d( + "policy.apply", + "发布策略", + "发布策略版本(需授权);不直接执行 JavaScript", + "execute", + "PolicyApply", + "admin", + "high", + true, + true, + ), + "inventory.query": d( + "inventory.query", + "查库存", + "查询本地资产、Slot 满足度与外部库存证据", + "read", + "InventoryQuery", + "read", + "low", + true, + false, + ), +}; + +function d( + id: KnownCapabilityId, + title: string, + description: string, + kind: CapabilityKind, + schemaBase: string, + scope: ApiScope, + riskLevel: RiskLevel, + idempotent: boolean, + supportsPreview: boolean, +): CapabilityDescriptor { + return { + id, + version: 1, + title, + description, + kind, + inputSchema: `${schemaBase}Input`, + outputSchema: `${schemaBase}Output`, + scope, + riskLevel, + idempotent, + supportsPreview, + }; +} diff --git a/spikes/deno/packages/contracts/src/common.ts b/spikes/deno/packages/contracts/src/common.ts new file mode 100644 index 00000000..351b482d --- /dev/null +++ b/spikes/deno/packages/contracts/src/common.ts @@ -0,0 +1,122 @@ +import { z } from "zod"; + +/** 本地实体 ID 一律 UUID */ +export const Id = z.uuid(); +export type Id = z.infer; + +/** ISO 8601 时间串 */ +export const Timestamp = z.iso.datetime({ offset: true }); +export type Timestamp = z.infer; + +/** 游标分页请求:cursor 为不透明字符串,由服务端编码 */ +export const CursorQuery = z.object({ + cursor: z.string().min(1).optional(), + limit: z.coerce.number().int().min(1).max(200).default(50), +}); +export type CursorQuery = z.infer; + +/** 游标分页结果:nextCursor 为 null 表示已到末尾 */ +export const cursorPage = (item: T) => + z.object({ + items: z.array(item), + nextCursor: z.string().nullable(), + }); +export type CursorPage = { items: T[]; nextCursor: string | null }; + +/** 错误码表:新增错误码只在这里加 */ +export const ApiErrorCode = z.enum([ + "bad_request", + "validation_failed", + "unauthorized", + "forbidden", + "not_found", + "conflict", + "precondition_failed", + "rate_limited", + "automation_disabled", + "upstream_unavailable", + "internal", +]); +export type ApiErrorCode = z.infer; + +/** 错误码对应的 HTTP 状态 */ +export const API_ERROR_STATUS = { + bad_request: 400, + validation_failed: 422, + unauthorized: 401, + forbidden: 403, + not_found: 404, + conflict: 409, + precondition_failed: 412, + rate_limited: 429, + automation_disabled: 423, + upstream_unavailable: 502, + internal: 500, +} as const satisfies Record; + +export const ApiError = z.object({ + code: ApiErrorCode, + message: z.string(), +}); +export type ApiError = z.infer; + +/** 统一响应信封 */ +export const apiResponse = (data: T) => + z.object({ + ok: z.boolean(), + data: data.optional(), + error: ApiError.optional(), + }); +export type ApiResponse = + | { ok: true; data: T; error?: undefined } + | { ok: false; data?: undefined; error: ApiError }; + +/** 没有 data 的失败响应 schema(OpenAPI 错误分支用) */ +export const ApiErrorResponse = z.object({ + ok: z.literal(false), + error: ApiError, +}); + +/** API Key 权限范围 */ +export const ApiScope = z.enum(["read", "organize.read", "organize.execute", "admin"]); +export type ApiScope = z.infer; + +/** 健康检查 */ +export const HealthStatus = z.object({ + status: z.enum(["ok", "degraded"]), + version: z.string(), + role: z.enum(["api", "worker", "all"]), + uptimeSec: z.number(), + db: z.enum(["ok", "down", "unknown"]), + time: Timestamp, +}); +export type HealthStatus = z.infer; + +/** 日期(无时区),如 air_date:YYYY-MM-DD */ +export const DateOnly = z.iso.date(); +export type DateOnly = z.infer; + +/** JSONB 对象列(证据、规则参数、原文快照等)的 API 表示 */ +export const JsonObject = z.record(z.string(), z.unknown()); +export type JsonObject = z.infer; + +/** 字节数、计数等非负整数(DB 为 bigint,客户端按 number 解析,上限 2^53) */ +export const NonNegInt = z.number().int().nonnegative(); + +/** 操作者类型:与 DB 中 actor_type 列一致 */ +export const ActorType = z.enum(["user", "agent", "system", "api_key"]); +export type ActorType = z.infer; + +/** 实体行的公共字段 */ +export const EntityBase = z.object({ + id: Id, + createdAt: Timestamp, + updatedAt: Timestamp, +}); + +/** 所有实体 Create 输入都要剔除的服务端字段 */ +export const SERVER_MANAGED = { id: true, createdAt: true, updatedAt: true } as const; + +/** 路径参数 `{id}` */ +export const IdParam = z.object({ id: Id }); +export type IdParam = z.infer; diff --git a/spikes/deno/packages/contracts/src/content.ts b/spikes/deno/packages/contracts/src/content.ts new file mode 100644 index 00000000..22a6b46f --- /dev/null +++ b/spikes/deno/packages/contracts/src/content.ts @@ -0,0 +1,129 @@ +import { z } from "zod"; +import { DateOnly, EntityBase, Id, JsonObject, SERVER_MANAGED, Timestamp } from "./common"; + +// 内容域:本地作品、内容单元、别名(REQ §3.1、§5.1、§5.4、§7.5) +// 本地 UUID 是主身份;季/集编号、外部 ID 都可修订,不是唯一身份。 + +export const MediaKind = z.enum(["movie", "series", "variety", "collection", "other"]); +export type MediaKind = z.infer; +export const MediaEntityStatus = z.enum(["provisional", "active", "merged", "archived"]); +export type MediaEntityStatus = z.infer; + +export const MediaEntity = EntityBase.extend({ + kind: MediaKind, + title: z.string().min(1), + originalTitle: z.string().nullable(), + year: z.number().int().min(1870).max(2200).nullable(), + status: MediaEntityStatus, + /** status=merged 时指向合并后的实体(旧 ID 跳转) */ + mergedIntoId: Id.nullable(), + /** 最终展示字段(每个字段带来源/语言/时间) */ + metadata: JsonObject, + /** 人工锁定的字段名;刷新不得覆盖 */ + fieldLocks: z.array(z.string()), + lastMetadataRefreshAt: Timestamp.nullable(), +}); +export type MediaEntity = z.infer; +export const MediaEntityCreate = MediaEntity.pick({ + kind: true, + title: true, + originalTitle: true, + year: true, + status: true, + metadata: true, + fieldLocks: true, +}).partial({ originalTitle: true, year: true, status: true, metadata: true, fieldLocks: true }); +export type MediaEntityCreate = z.infer; +/** 合并通过专门动作完成,Update 不接受 status=merged */ +export const MediaEntityUpdate = MediaEntityCreate.partial().extend({ + status: MediaEntityStatus.exclude(["merged"]).optional(), +}); +export type MediaEntityUpdate = z.infer; + +export const ContentUnitKind = z.enum(["season", "episode", "special", "part"]); +export type ContentUnitKind = z.infer; +/** 综艺内容类型(REQ §5.1) */ +export const ContentType = z.enum([ + "main", + "extra", + "behind_the_scenes", + "performance_only", + "preview", + "reaction", + "unknown", +]); +export type ContentType = z.infer; +export const Segment = z.enum(["full", "upper", "middle", "lower", "unknown"]); +export type Segment = z.infer; +export const ContentUnitStatus = z.enum(["provisional", "confirmed", "retired"]); +export type ContentUnitStatus = z.infer; + +export const ContentUnit = EntityBase.extend({ + mediaEntityId: Id, + parentUnitId: Id.nullable(), + kind: ContentUnitKind, + contentType: ContentType, + segment: Segment, + seasonNumber: z.number().int().nonnegative().nullable(), + episodeNumber: z.number().int().nonnegative().nullable(), + /** 来源期号原文,如 "第十期"、"20260927" */ + sourceIssue: z.string().nullable(), + airDate: DateOnly.nullable(), + displayOrder: z.number().nullable(), + title: z.string().nullable(), + status: ContentUnitStatus, + /** 人工确认的编号锁定;刷新不得改编号 */ + numberingLocked: z.boolean(), + metadata: JsonObject, + fieldLocks: z.array(z.string()), +}); +export type ContentUnit = z.infer; +export const ContentUnitCreate = ContentUnit.omit(SERVER_MANAGED).partial({ + parentUnitId: true, + contentType: true, + segment: true, + seasonNumber: true, + episodeNumber: true, + sourceIssue: true, + airDate: true, + displayOrder: true, + title: true, + status: true, + numberingLocked: true, + metadata: true, + fieldLocks: true, +}); +export type ContentUnitCreate = z.infer; +export const ContentUnitUpdate = ContentUnitCreate.omit({ mediaEntityId: true }).partial(); +export type ContentUnitUpdate = z.infer; + +export const AliasKind = z.enum(["original", "translated", "short", "site_title", "romanized", "other"]); +export type AliasKind = z.infer; +export const AliasSource = z.enum(["tmdb", "user", "site", "agent", "nfo", "import"]); +export type AliasSource = z.infer; + +export const TitleAlias = EntityBase.extend({ + mediaEntityId: Id, + contentUnitId: Id.nullable(), + alias: z.string().min(1), + /** 归一化后的检索键(小写、去标点、全半角统一),建 trgm 索引 */ + normalized: z.string().min(1), + language: z.string().nullable(), + kind: AliasKind, + source: AliasSource, + locked: z.boolean(), +}); +export type TitleAlias = z.infer; +/** normalized 由服务端计算,不接受客户端传入 */ +export const TitleAliasCreate = TitleAlias.pick({ + mediaEntityId: true, + contentUnitId: true, + alias: true, + language: true, + kind: true, + source: true, + locked: true, +}).partial({ contentUnitId: true, language: true, kind: true, locked: true }); +export type TitleAliasCreate = z.infer; +export const TitleAliasUpdate = TitleAliasCreate.omit({ mediaEntityId: true }).partial(); +export type TitleAliasUpdate = z.infer; diff --git a/spikes/deno/packages/contracts/src/events.ts b/spikes/deno/packages/contracts/src/events.ts new file mode 100644 index 00000000..cb3682a2 --- /dev/null +++ b/spikes/deno/packages/contracts/src/events.ts @@ -0,0 +1,192 @@ +import { z } from "zod"; +import { AgentRunStatus, DecisionKind, DecisionStatus, Severity } from "./agent"; +import { Id, NonNegInt, Timestamp } from "./common"; +import { DownloadStatus, PauseReason, TaskKind, TaskStatus } from "./execution"; +import { OrganizationStatus } from "./organization"; +import { FeedHealth } from "./sources"; + +/** SSE 事件信封:seq 在进程内单调递增,客户端断线后用 ?after= 补读 */ +export const EventEnvelope = z.object({ + seq: z.number().int().nonnegative(), + type: z.string().min(1), + at: Timestamp, + payload: z.unknown(), +}); +export type EventEnvelope

= { + seq: number; + type: string; + at: string; + payload: P; +}; + +/** 已知事件类型:`域.动作`。新增事件先在这里加类型与 payload schema */ +export const EventType = { + heartbeat: "system.heartbeat", + resync: "system.resync", + + taskCreated: "task.created", + taskStatusChanged: "task.status_changed", + + downloadSubmitted: "download.submitted", + downloadProgress: "download.progress", + downloadStatusChanged: "download.status_changed", + downloadThreshold: "download.threshold", + + organizationChanged: "organization.changed", + + agentRunStatus: "agent.run_status", + agentTextDelta: "agent.text_delta", + agentToolCall: "agent.tool_call", + + feedFetched: "feed.fetched", + feedHealthChanged: "feed.health_changed", + + decisionCreated: "decision.created", + decisionUpdated: "decision.updated", +} as const; +export type EventType = (typeof EventType)[keyof typeof EventType]; + +/** 心跳 payload */ +export const HeartbeatPayload = z.object({ + role: z.string(), +}); + +/** 服务端环形缓冲不足以补读时发出,客户端应整页刷新 */ +export const ResyncPayload = z.object({ reason: z.string() }); + +export const TaskCreatedPayload = z.object({ + taskId: Id, + kind: TaskKind, + subjectType: z.string().nullable(), + subjectId: Id.nullable(), +}); +export const TaskStatusChangedPayload = z.object({ + taskId: Id, + kind: TaskKind, + from: TaskStatus.nullable(), + to: TaskStatus, + error: z.string().nullable(), +}); + +export const DownloadSubmittedPayload = z.object({ + downloadTaskId: Id, + instanceId: Id, + torrentHash: z.string(), + planId: Id.nullable(), + name: z.string(), +}); +export const DownloadProgressPayload = z.object({ + downloadTaskId: Id, + progress: z.number().min(0).max(1), + downloadedBytes: NonNegInt, + sizeBytes: NonNegInt.nullable(), +}); +export const DownloadStatusChangedPayload = z.object({ + downloadTaskId: Id, + from: DownloadStatus.nullable(), + to: DownloadStatus, + pauseReason: PauseReason.nullable(), +}); +/** 缓存卷越过硬阈值(tripped)或回到恢复水位(recovered) */ +export const DownloadThresholdPayload = z.object({ + volumeKey: z.string(), + state: z.enum(["tripped", "recovered", "unknown"]), + usedPercent: z.number().min(0).max(100).nullable(), + pausedTaskIds: z.array(Id), +}); + +/** 整理记录新增或状态变化;changeSeq 与公开 API 的游标一致 */ +export const OrganizationChangedPayload = z.object({ + recordId: Id, + status: OrganizationStatus, + changeSeq: z.string().regex(/^\d+$/), + mediaEntityId: Id.nullable(), +}); + +export const AgentRunStatusPayload = z.object({ + runId: Id, + status: AgentRunStatus, + summary: z.string().nullable(), +}); +/** Agent 文本增量:runSeq 为该 run 内序号,配合 agent_run.last_event_seq 断点续读 */ +export const AgentTextDeltaPayload = z.object({ + runId: Id, + runSeq: NonNegInt, + delta: z.string(), +}); +export const AgentToolCallPayload = z.object({ + runId: Id, + runSeq: NonNegInt, + capabilityId: z.string(), + phase: z.enum(["started", "succeeded", "failed", "denied"]), + /** 已脱敏的摘要,不含凭据/私有 URL */ + summary: z.string(), +}); + +export const FeedFetchedPayload = z.object({ + feedId: Id, + newItems: NonNegInt, + updatedItems: NonNegInt, + notModified: z.boolean(), +}); +export const FeedHealthChangedPayload = z.object({ + feedId: Id, + from: FeedHealth.nullable(), + to: FeedHealth, + reason: z.string().nullable(), +}); + +export const DecisionCreatedPayload = z.object({ + decisionId: Id, + kind: DecisionKind, + severity: Severity, + title: z.string(), +}); +export const DecisionUpdatedPayload = z.object({ + decisionId: Id, + status: DecisionStatus, +}); + +/** 事件类型 → payload schema */ +export const EVENT_PAYLOADS = { + "system.heartbeat": HeartbeatPayload, + "system.resync": ResyncPayload, + "task.created": TaskCreatedPayload, + "task.status_changed": TaskStatusChangedPayload, + "download.submitted": DownloadSubmittedPayload, + "download.progress": DownloadProgressPayload, + "download.status_changed": DownloadStatusChangedPayload, + "download.threshold": DownloadThresholdPayload, + "organization.changed": OrganizationChangedPayload, + "agent.run_status": AgentRunStatusPayload, + "agent.text_delta": AgentTextDeltaPayload, + "agent.tool_call": AgentToolCallPayload, + "feed.fetched": FeedFetchedPayload, + "feed.health_changed": FeedHealthChangedPayload, + "decision.created": DecisionCreatedPayload, + "decision.updated": DecisionUpdatedPayload, +} as const satisfies Record; + +export type EventPayload = z.infer<(typeof EVENT_PAYLOADS)[T]>; + +/** 已知事件的判别联合:`{ type, payload }` */ +export type TypedEvent = { [T in EventType]: { type: T; payload: EventPayload } }[EventType]; + +/** 已知事件信封的 schema(判别联合) */ +export const TypedEventEnvelope = z.discriminatedUnion( + "type", + Object.entries(EVENT_PAYLOADS).map(([type, payload]) => + z.object({ seq: z.number().int().nonnegative(), at: Timestamp, type: z.literal(type), payload }), + ) as unknown as [z.ZodObject, ...z.ZodObject[]], +); + +export function isEventType(type: string): type is EventType { + return Object.hasOwn(EVENT_PAYLOADS, type); +} + +/** 校验一个事件 payload;不合法返回 null */ +export function parseEventPayload(type: T, payload: unknown): EventPayload | null { + const schema: z.ZodType = EVENT_PAYLOADS[type]; + const r = schema.safeParse(payload); + return r.success ? (r.data as EventPayload) : null; +} diff --git a/spikes/deno/packages/contracts/src/execution.ts b/spikes/deno/packages/contracts/src/execution.ts new file mode 100644 index 00000000..14cd2052 --- /dev/null +++ b/spikes/deno/packages/contracts/src/execution.ts @@ -0,0 +1,292 @@ +import { z } from "zod"; +import { EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common"; +import { AcquisitionPriority } from "./policy"; + +// 执行域:业务任务、下载计划、下载任务、操作账本(REQ §8.4、§11;ADR-002) + +/** 持久任务类型(任务书 §3.1-8)+ Agent 有界任务 */ +export const TaskKind = z.enum([ + "ingestFeed", + "resolveObservation", + "evaluateAcquisition", + "submitDownload", + "verifyAndImport", + "refreshMetadata", + "reconcileLibrary", + "cleanupDownloadCache", + "resolveDecision", + "agentRun", +]); +export type TaskKind = z.infer; +export const TaskStatus = z.enum(["pending", "running", "waiting", "succeeded", "failed", "cancelled", "dead"]); +export type TaskStatus = z.infer; + +export const Task = EntityBase.extend({ + kind: TaskKind, + handlerVersion: z.number().int().min(1), + idempotencyKey: z.string().min(1), + status: TaskStatus, + /** 数值越小越先执行 */ + priority: z.number().int(), + input: JsonObject, + output: JsonObject.nullable(), + subjectType: z.string().nullable(), + subjectId: Id.nullable(), + parentTaskId: Id.nullable(), + engineRunId: z.string().nullable(), + attempts: NonNegInt, + maxAttempts: z.number().int().min(1), + nextRunAt: Timestamp, + waitReason: z.string().nullable(), + cancelRequestedAt: Timestamp.nullable(), + cancelReason: z.string().nullable(), + lastError: JsonObject.nullable(), + startedAt: Timestamp.nullable(), + finishedAt: Timestamp.nullable(), + triggeredBy: z.string().nullable(), +}); +export type Task = z.infer; +/** 内部创建输入(幂等键由服务端按语义生成,不能由模型挑选) */ +export const TaskCreate = Task.pick({ + kind: true, + handlerVersion: true, + idempotencyKey: true, + priority: true, + input: true, + subjectType: true, + subjectId: true, + parentTaskId: true, + maxAttempts: true, + nextRunAt: true, + triggeredBy: true, +}).partial({ + handlerVersion: true, + priority: true, + input: true, + subjectType: true, + subjectId: true, + parentTaskId: true, + maxAttempts: true, + nextRunAt: true, + triggeredBy: true, +}); +export type TaskCreate = z.infer; +export const TaskUpdate = Task.pick({ + status: true, + output: true, + engineRunId: true, + attempts: true, + nextRunAt: true, + waitReason: true, + lastError: true, + startedAt: true, + finishedAt: true, +}).partial(); +export type TaskUpdate = z.infer; +/** 取消 = 停止未来步骤,不撤销已完成外部动作(REQ §11.4) */ +export const TaskCancelRequest = z.object({ reason: z.string().min(1).max(500) }); +export type TaskCancelRequest = z.infer; + +export const PlanPurpose = z.enum(["fill", "upgrade", "replace_source"]); +export type PlanPurpose = z.infer; +export const PlanStatus = z.enum(["draft", "reserved", "submitted", "completed", "cancelled", "failed", "expired"]); +export type PlanStatus = z.infer; + +/** 下载计划:与 Slot 预留、预算预留同事务创建;一个 Slot 同时只能有一个 reserved/submitted 计划 */ +export const AcquisitionPlan = EntityBase.extend({ + slotId: Id, + sourceReleaseId: Id, + taskId: Id.nullable(), + purpose: PlanPurpose, + status: PlanStatus, + priority: AcquisitionPriority, + /** evaluateAcquisition 的确定性评估结果与拒绝/接受理由 */ + evaluation: JsonObject, + reservedBytes: NonNegInt, + budgetReservation: JsonObject, + payloadHash: z.string().min(1), + expectedRevision: z.number().int().min(1), + idempotencyKey: z.string().min(1), + expiresAt: Timestamp.nullable(), + statusReason: z.string().nullable(), +}); +export type AcquisitionPlan = z.infer; +export const AcquisitionPlanCreate = AcquisitionPlan.omit({ ...SERVER_MANAGED, status: true }).partial({ + taskId: true, + purpose: true, + priority: true, + evaluation: true, + reservedBytes: true, + budgetReservation: true, + expectedRevision: true, + expiresAt: true, + statusReason: true, +}); +export type AcquisitionPlanCreate = z.infer; +export const AcquisitionPlanUpdate = AcquisitionPlan.pick({ status: true, taskId: true, statusReason: true }).partial(); +export type AcquisitionPlanUpdate = z.infer; + +export const DownloadOrigin = z.enum(["submitted", "adopted"]); +export type DownloadOrigin = z.infer; +export const DownloadStatus = z.enum([ + "queued", + "submitting", + "downloading", + "paused", + "stalled", + "completed", + "importing", + "imported", + "failed", + "removed", + "missing", +]); +export type DownloadStatus = z.infer; +/** 暂停原因:阈值恢复时只恢复 threshold 暂停项,不恢复用户手动暂停(REQ §8.4.1) */ +export const PauseReason = z.enum(["user", "threshold", "policy", "agent"]); +export type PauseReason = z.infer; +export const TorrentHash = z.string().regex(/^[0-9a-f]{40}$|^[0-9a-f]{64}$/); + +export const DownloadTask = EntityBase.extend({ + instanceId: Id, + /** 该下载器实例内的 torrent 标识(v1 hash 或 v2 hash,小写十六进制) */ + torrentHash: TorrentHash, + infohashV1: z.string().nullable(), + infohashV2: z.string().nullable(), + acquisitionPlanId: Id.nullable(), + sourceReleaseId: Id.nullable(), + name: z.string(), + savePath: z.string().nullable(), + contentPath: z.string().nullable(), + category: z.string().nullable(), + tags: z.array(z.string()), + /** 是否在管理标签范围内;标签移除后暂停后续自动动作 */ + managed: z.boolean(), + origin: DownloadOrigin, + status: DownloadStatus, + pauseReason: PauseReason.nullable(), + rawState: z.string().nullable(), + progress: z.number().min(0).max(1), + sizeBytes: NonNegInt.nullable(), + downloadedBytes: NonNegInt, + remainingBytes: NonNegInt.nullable(), + ratio: z.number().nullable(), + seedingTimeSec: NonNegInt.nullable(), + completedAt: Timestamp.nullable(), + lastProgressAt: Timestamp.nullable(), + /** 无进展起点:只记录并汇入待处理,不自动换源(REQ §8.4.2) */ + noProgressSince: Timestamp.nullable(), + lastSyncedAt: Timestamp.nullable(), + nextCheckAt: Timestamp.nullable(), + removedAt: Timestamp.nullable(), + fileList: z.array(z.object({ path: z.string(), size: NonNegInt, progress: z.number().optional() })).nullable(), + lastError: JsonObject.nullable(), +}); +export type DownloadTask = z.infer; +export const DownloadTaskCreate = DownloadTask.pick({ + instanceId: true, + torrentHash: true, + infohashV1: true, + infohashV2: true, + acquisitionPlanId: true, + sourceReleaseId: true, + name: true, + savePath: true, + category: true, + tags: true, + managed: true, + origin: true, +}).partial({ + infohashV1: true, + infohashV2: true, + acquisitionPlanId: true, + sourceReleaseId: true, + savePath: true, + category: true, + tags: true, + managed: true, + origin: true, +}); +export type DownloadTaskCreate = z.infer; +export const DownloadTaskUpdate = DownloadTask.omit({ + ...SERVER_MANAGED, + instanceId: true, + torrentHash: true, +}).partial(); +export type DownloadTaskUpdate = z.infer; + +export const OperationPhase = z.enum(["decision", "prepare", "transfer", "verify", "commit", "cleanup", "compensate"]); +export type OperationPhase = z.infer; +export const OperationKind = z.enum([ + "copy", + "move", + "rename", + "mkdir", + "hardlink", + "delete_source", + "delete_target", + "verify", + "register", + "qb_pause", + "qb_resume", + "qb_remove", + "qb_set_tags", + "other", +]); +export type OperationKind = z.infer; +export const OperationStatus = z.enum(["planned", "started", "succeeded", "failed", "skipped", "compensated"]); +export type OperationStatus = z.infer; + +/** 副作用账本:plan → attempt(started, attemptToken) → result;提交时以 attemptToken 做 fencing */ +export const AssetOperation = EntityBase.extend({ + operationId: z.string().min(1), + taskId: Id.nullable(), + organizationRecordId: Id.nullable(), + planFingerprint: z.string().nullable(), + ordinal: z.number().int().nonnegative(), + phase: OperationPhase, + kind: OperationKind, + status: OperationStatus, + intent: JsonObject, + attemptToken: Id.nullable(), + attempts: NonNegInt, + result: JsonObject.nullable(), + error: JsonObject.nullable(), + mediaAssetId: Id.nullable(), + downloadTaskId: Id.nullable(), + startedAt: Timestamp.nullable(), + finishedAt: Timestamp.nullable(), +}); +export type AssetOperation = z.infer; +export const AssetOperationCreate = AssetOperation.pick({ + operationId: true, + taskId: true, + organizationRecordId: true, + planFingerprint: true, + ordinal: true, + phase: true, + kind: true, + intent: true, + mediaAssetId: true, + downloadTaskId: true, +}).partial({ + taskId: true, + organizationRecordId: true, + planFingerprint: true, + ordinal: true, + intent: true, + mediaAssetId: true, + downloadTaskId: true, +}); +export type AssetOperationCreate = z.infer; +export const AssetOperationUpdate = AssetOperation.pick({ + status: true, + attemptToken: true, + attempts: true, + result: true, + error: true, + mediaAssetId: true, + startedAt: true, + finishedAt: true, +}).partial(); +export type AssetOperationUpdate = z.infer; diff --git a/spikes/deno/packages/contracts/src/identity.ts b/spikes/deno/packages/contracts/src/identity.ts new file mode 100644 index 00000000..7aef5b14 --- /dev/null +++ b/spikes/deno/packages/contracts/src/identity.ts @@ -0,0 +1,121 @@ +import { z } from "zod"; +import { ActorType, EntityBase, Id, JsonObject, Timestamp } from "./common"; + +// 身份域:外部身份映射与修订(REQ §7.2) +// provider + entityType + externalId 对 accepted 唯一;TMDB movie:123 与 tv:123 是两个身份。 + +export const IdentityStatus = z.enum(["accepted", "proposed", "conflicted", "retired"]); +export type IdentityStatus = z.infer; +export const ExternalEntityType = z.enum(["movie", "tv", "season", "episode", "collection", "person"]); +export type ExternalEntityType = z.infer; +/** 证据等级:未经标注集校准前不展示概率(REQ §5.3) */ +export const EvidenceLevel = z.enum(["manual", "rule_confirmed", "multi_evidence", "needs_review"]); +export type EvidenceLevel = z.infer; +export const IdentityVerifyStatus = z.enum(["unverified", "ok", "not_found", "type_mismatch", "error"]); +export type IdentityVerifyStatus = z.infer; +export const IdentityProvider = z.string().regex(/^[a-z][a-z0-9_]*$/); + +export const ExternalIdentity = EntityBase.extend({ + mediaEntityId: Id.nullable(), + contentUnitId: Id.nullable(), + provider: IdentityProvider, + entityType: ExternalEntityType, + externalId: z.string().min(1), + status: IdentityStatus, + evidenceLevel: EvidenceLevel, + evidence: JsonObject, + /** 人工锁:自动匹配/刷新不得改动 */ + locked: z.boolean(), + revision: z.number().int().min(1), + validFrom: Timestamp.nullable(), + validUntil: Timestamp.nullable(), + verifiedAt: Timestamp.nullable(), + /** 404/超时/鉴权失败只记录到这里,不是删除映射的依据 */ + verifyStatus: IdentityVerifyStatus, + /** 附属字段,如 TMDB episode_group */ + extra: JsonObject, +}); +export type ExternalIdentity = z.infer; + +const oneTarget = (v: { mediaEntityId?: string | null; contentUnitId?: string | null }) => + Number(!!v.mediaEntityId) + Number(!!v.contentUnitId) === 1; + +export const ExternalIdentityCreate = ExternalIdentity.pick({ + mediaEntityId: true, + contentUnitId: true, + provider: true, + entityType: true, + externalId: true, + status: true, + evidenceLevel: true, + evidence: true, + locked: true, + validFrom: true, + validUntil: true, + extra: true, +}) + .partial({ + mediaEntityId: true, + contentUnitId: true, + status: true, + evidenceLevel: true, + evidence: true, + locked: true, + validFrom: true, + validUntil: true, + extra: true, + }) + .refine(oneTarget, { message: "mediaEntityId 与 contentUnitId 必须且只能给一个" }); +export type ExternalIdentityCreate = z.infer; + +/** 变更一律生成 identity_revision;expectedRevision 用于乐观并发 */ +export const ExternalIdentityUpdate = z.object({ + expectedRevision: z.number().int().min(1), + status: IdentityStatus.optional(), + evidenceLevel: EvidenceLevel.optional(), + evidence: JsonObject.optional(), + locked: z.boolean().optional(), + validUntil: Timestamp.nullable().optional(), + extra: JsonObject.optional(), + reason: z.string().max(2000).optional(), +}); +export type ExternalIdentityUpdate = z.infer; + +export const IdentityRevisionAction = z.enum([ + "created", + "status_changed", + "relinked", + "evidence_updated", + "locked", + "unlocked", +]); +export type IdentityRevisionAction = z.infer; + +/** 身份修订历史:只追加,不修改 */ +export const IdentityRevision = EntityBase.extend({ + externalIdentityId: Id, + revision: z.number().int().min(1), + action: IdentityRevisionAction, + fromStatus: IdentityStatus.nullable(), + toStatus: IdentityStatus.nullable(), + before: JsonObject.nullable(), + after: JsonObject, + actorType: ActorType, + actorId: z.string().nullable(), + reason: z.string().nullable(), +}); +export type IdentityRevision = z.infer; +/** 由服务端在身份变更事务内写入;此输入仅供内部仓储使用 */ +export const IdentityRevisionCreate = IdentityRevision.pick({ + externalIdentityId: true, + revision: true, + action: true, + fromStatus: true, + toStatus: true, + before: true, + after: true, + actorType: true, + actorId: true, + reason: true, +}).partial({ fromStatus: true, toStatus: true, before: true, actorId: true, reason: true }); +export type IdentityRevisionCreate = z.infer; diff --git a/spikes/deno/packages/contracts/src/index.ts b/spikes/deno/packages/contracts/src/index.ts new file mode 100644 index 00000000..a6aae240 --- /dev/null +++ b/spikes/deno/packages/contracts/src/index.ts @@ -0,0 +1,16 @@ +export * from "./agent"; +export * from "./api"; +export * from "./assets"; +export * from "./capabilities-io"; +export * from "./capability"; +export * from "./common"; +export * from "./content"; +export * from "./events"; +export * from "./execution"; +export * from "./identity"; +export * from "./instances"; +export * from "./ops"; +export * from "./organization"; +export * from "./policy"; +export * from "./settings"; +export * from "./sources"; diff --git a/spikes/deno/packages/contracts/src/instances.ts b/spikes/deno/packages/contracts/src/instances.ts new file mode 100644 index 00000000..1dca43ab --- /dev/null +++ b/spikes/deno/packages/contracts/src/instances.ts @@ -0,0 +1,161 @@ +import { z } from "zod"; +import { EntityBase, Id, JsonObject, SERVER_MANAGED, Timestamp } from "./common"; + +// 实例域:媒体库根、下载器实例、媒体服务器实例、路径映射(REQ §7.1、§8.4、§12.5) +// 凭证只以 secretRef 暴露;创建/更新时的 credential 是只写明文,服务端加密为 secret 后丢弃。 + +export const InstanceHealth = z.enum(["unknown", "healthy", "degraded", "offline"]); +export type InstanceHealth = z.infer; + +export const LibraryMediaKind = z.enum(["movie", "series", "variety", "mixed", "other"]); +export type LibraryMediaKind = z.infer; + +/** register_only:只登记不自动维护;managed:允许按策略补齐/升级(REQ §6.6) */ +export const ManagementMode = z.enum(["register_only", "managed"]); +export type ManagementMode = z.infer; + +export const LibraryRoot = EntityBase.extend({ + name: z.string().min(1), + mediaKind: LibraryMediaKind, + /** 执行进程可访问的绝对路径 */ + localPath: z.string().startsWith("/"), + managementMode: ManagementMode, + enabled: z.boolean(), + sentinelFile: z.string().nullable(), + healthStatus: InstanceHealth, + lastHealthAt: Timestamp.nullable(), + healthDetail: JsonObject, + notes: z.string().nullable(), +}); +export type LibraryRoot = z.infer; +export const LibraryRootCreate = LibraryRoot.pick({ + name: true, + mediaKind: true, + localPath: true, + managementMode: true, + enabled: true, + sentinelFile: true, + notes: true, +}).partial({ mediaKind: true, managementMode: true, enabled: true, sentinelFile: true, notes: true }); +export type LibraryRootCreate = z.infer; +export const LibraryRootUpdate = LibraryRootCreate.partial(); +export type LibraryRootUpdate = z.infer; + +/** 只写凭证:明文仅在请求中出现,绝不回显 */ +const Credential = z.string().min(1).max(4096); + +export const DownloaderKind = z.enum(["qbittorrent"]); +export type DownloaderKind = z.infer; + +export const DownloaderInstance = EntityBase.extend({ + name: z.string().min(1), + kind: DownloaderKind, + baseUrl: z.url(), + username: z.string().nullable(), + secretRef: Id.nullable(), + /** 管理标签名(REQ §8.4) */ + managedTag: z.string().min(1), + /** 共享缓存卷标识:多 qB 共用同一卷时按此汇总水位 */ + cacheVolumeKey: z.string().nullable(), + enabled: z.boolean(), + healthStatus: InstanceHealth, + lastSeenAt: Timestamp.nullable(), + versionInfo: JsonObject, + settings: JsonObject, +}); +export type DownloaderInstance = z.infer; +export const DownloaderInstanceCreate = DownloaderInstance.pick({ + name: true, + kind: true, + baseUrl: true, + username: true, + managedTag: true, + cacheVolumeKey: true, + enabled: true, + settings: true, +}) + .partial({ kind: true, username: true, managedTag: true, cacheVolumeKey: true, enabled: true, settings: true }) + .extend({ credential: Credential.optional() }); +export type DownloaderInstanceCreate = z.infer; +export const DownloaderInstanceUpdate = DownloaderInstanceCreate.partial(); +export type DownloaderInstanceUpdate = z.infer; + +export const MediaServerKind = z.enum(["emby", "jellyfin"]); +export type MediaServerKind = z.infer; + +export const MediaServerInstance = EntityBase.extend({ + name: z.string().min(1), + kind: MediaServerKind, + baseUrl: z.url(), + userId: z.string().nullable(), + secretRef: Id.nullable(), + enabled: z.boolean(), + healthStatus: InstanceHealth, + lastSeenAt: Timestamp.nullable(), + versionInfo: JsonObject, + settings: JsonObject, +}); +export type MediaServerInstance = z.infer; +export const MediaServerInstanceCreate = MediaServerInstance.pick({ + name: true, + kind: true, + baseUrl: true, + userId: true, + enabled: true, + settings: true, +}) + .partial({ kind: true, userId: true, enabled: true, settings: true }) + .extend({ credential: Credential.optional() }); +export type MediaServerInstanceCreate = z.infer; +export const MediaServerInstanceUpdate = MediaServerInstanceCreate.partial(); +export type MediaServerInstanceUpdate = z.infer; + +export const PathViewKind = z.enum(["downloader", "media_server"]); +export type PathViewKind = z.infer; +export const PathMappingVerifyStatus = z.enum(["unverified", "ok", "failed"]); +export type PathMappingVerifyStatus = z.infer; + +/** 下载器/媒体服务器路径视图 → 执行进程可访问路径的前缀映射(REQ §12.5) */ +export const PathMapping = EntityBase.extend({ + viewKind: PathViewKind, + downloaderInstanceId: Id.nullable(), + mediaServerInstanceId: Id.nullable(), + libraryRootId: Id.nullable(), + remotePrefix: z.string().min(1), + localPrefix: z.string().startsWith("/"), + priority: z.number().int(), + enabled: z.boolean(), + verifyStatus: PathMappingVerifyStatus, + verifiedAt: Timestamp.nullable(), + verifyDetail: JsonObject, +}); +export type PathMapping = z.infer; + +const pathMappingOwnerOk = (v: { + viewKind?: PathViewKind; + downloaderInstanceId?: string | null; + mediaServerInstanceId?: string | null; +}) => + v.viewKind === undefined || + (v.viewKind === "downloader" + ? !!v.downloaderInstanceId && !v.mediaServerInstanceId + : !!v.mediaServerInstanceId && !v.downloaderInstanceId); + +const PathMappingInput = PathMapping.omit({ + ...SERVER_MANAGED, + verifyStatus: true, + verifiedAt: true, + verifyDetail: true, +}); +export const PathMappingCreate = PathMappingInput.partial({ + downloaderInstanceId: true, + mediaServerInstanceId: true, + libraryRootId: true, + priority: true, + enabled: true, +}).refine(pathMappingOwnerOk, { message: "viewKind 与实例 ID 不匹配" }); +export type PathMappingCreate = z.infer; +export const PathMappingUpdate = PathMappingInput.partial().refine(pathMappingOwnerOk, { + message: "viewKind 与实例 ID 不匹配", +}); +export type PathMappingUpdate = z.infer; diff --git a/spikes/deno/packages/contracts/src/ops.ts b/spikes/deno/packages/contracts/src/ops.ts new file mode 100644 index 00000000..8dfea48a --- /dev/null +++ b/spikes/deno/packages/contracts/src/ops.ts @@ -0,0 +1,194 @@ +import { z } from "zod"; +import { ActorType, ApiScope, EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common"; + +// 运维域:校验运行、媒体服务器库存映射、API Key、审计、outbox、凭证(REQ §8.1、§7.4、§13.2) + +export const ReconcileKind = z.enum(["identity", "metadata", "asset", "acquisition", "server"]); +export type ReconcileKind = z.infer; +/** read_only:恢复后默认只读对账模式(REQ §8.5) */ +export const ReconcileMode = z.enum(["normal", "read_only"]); +export type ReconcileMode = z.infer; +export const ReconcileStatus = z.enum(["queued", "running", "succeeded", "partial", "failed", "cancelled"]); +export type ReconcileStatus = z.infer; + +export const ReconcileRun = EntityBase.extend({ + kind: ReconcileKind, + mode: ReconcileMode, + status: ReconcileStatus, + scope: JsonObject, + /** 中断续扫位置 */ + resumeCursor: JsonObject, + stats: JsonObject, + taskId: Id.nullable(), + error: JsonObject.nullable(), + startedAt: Timestamp.nullable(), + finishedAt: Timestamp.nullable(), +}); +export type ReconcileRun = z.infer; +export const ReconcileRunCreate = ReconcileRun.pick({ kind: true, mode: true, scope: true }).partial({ + mode: true, + scope: true, +}); +export type ReconcileRunCreate = z.infer; +export const ReconcileRunUpdate = ReconcileRun.pick({ + status: true, + resumeCursor: true, + stats: true, + taskId: true, + error: true, + startedAt: true, + finishedAt: true, +}).partial(); +export type ReconcileRunUpdate = z.infer; + +export const ServerItemType = z.enum(["movie", "series", "season", "episode", "other"]); +export type ServerItemType = z.infer; +/** 未知 ≠ 缺失:stale/unmatched 不能推出"没有资源" */ +export const ServerMatchStatus = z.enum(["matched", "ambiguous", "unmatched", "stale"]); +export type ServerMatchStatus = z.infer; + +/** Emby 兼容库存条目与本地对象的映射(辅助证据,不构成文件纳管授权) */ +export const ServerMapping = EntityBase.extend({ + mediaServerInstanceId: Id, + serverItemId: z.string().min(1), + itemType: ServerItemType, + mediaEntityId: Id.nullable(), + contentUnitId: Id.nullable(), + mediaAssetId: Id.nullable(), + serverPath: z.string().nullable(), + pathMappingId: Id.nullable(), + matchStatus: ServerMatchStatus, + versionInfo: JsonObject, + evidence: JsonObject, + observedAt: Timestamp, +}); +export type ServerMapping = z.infer; +export const ServerMappingCreate = ServerMapping.omit(SERVER_MANAGED).partial({ + mediaEntityId: true, + contentUnitId: true, + mediaAssetId: true, + serverPath: true, + pathMappingId: true, + matchStatus: true, + versionInfo: true, + evidence: true, + observedAt: true, +}); +export type ServerMappingCreate = z.infer; +export const ServerMappingUpdate = ServerMappingCreate.omit({ + mediaServerInstanceId: true, + serverItemId: true, +}).partial(); +export type ServerMappingUpdate = z.infer; + +/** API Key(不含 key_hash) */ +export const ApiKey = EntityBase.extend({ + name: z.string().min(1), + scopes: z.array(ApiScope), + revokedAt: Timestamp.nullable(), + lastUsedAt: Timestamp.nullable(), +}); +export type ApiKey = z.infer; +export const ApiKeyCreate = z.object({ + name: z.string().min(1).max(80), + scopes: z.array(ApiScope).min(1), +}); +export type ApiKeyCreate = z.infer; +/** 创建响应:明文 key 只在此返回一次 */ +export const ApiKeyCreated = z.object({ apiKey: ApiKey, plaintextKey: z.string().min(16) }); +export type ApiKeyCreated = z.infer; +export const ApiKeyUpdate = z.object({ + name: z.string().min(1).max(80).optional(), + scopes: z.array(ApiScope).min(1).optional(), + revoked: z.literal(true).optional(), +}); +export type ApiKeyUpdate = z.infer; + +/** 审计事件(只追加) */ +export const AuditEvent = z.object({ + id: Id, + at: Timestamp, + actorType: z.string(), + actorId: z.string().nullable(), + action: z.string().min(1), + targetType: z.string().nullable(), + targetId: z.string().nullable(), + payload: JsonObject, + traceId: z.string().nullable(), +}); +export type AuditEvent = z.infer; +export const AuditEventCreate = AuditEvent.omit({ id: true, at: true }) + .partial({ actorId: true, targetType: true, targetId: true, payload: true, traceId: true }) + .extend({ actorType: ActorType }); +export type AuditEventCreate = z.infer; + +export const OutboxStatus = z.enum(["pending", "dispatched", "failed", "dead"]); +export type OutboxStatus = z.infer; +/** outbox:业务事务内写入,分发器轮询 pending 并以 idempotencyKey 入队(ADR-002 §2.1) */ +export const OutboxEvent = z.object({ + id: Id, + at: Timestamp, + type: z.string().min(1), + payload: JsonObject, + publishedAt: Timestamp.nullable(), + idempotencyKey: z.string().nullable(), + status: OutboxStatus, + attempts: NonNegInt, + nextAttemptAt: Timestamp, + lastError: JsonObject.nullable(), +}); +export type OutboxEvent = z.infer; +export const OutboxEventCreate = OutboxEvent.pick({ + type: true, + payload: true, + idempotencyKey: true, + nextAttemptAt: true, +}).partial({ + payload: true, + idempotencyKey: true, + nextAttemptAt: true, +}); +export type OutboxEventCreate = z.infer; +export const OutboxEventUpdate = OutboxEvent.pick({ + status: true, + attempts: true, + nextAttemptAt: true, + lastError: true, + publishedAt: true, +}).partial(); +export type OutboxEventUpdate = z.infer; + +export const SecretPurpose = z.enum([ + "site_cookie", + "site_passkey", + "site_api_key", + "site_password", + "feed_url", + "downloader_password", + "media_server_api_key", + "metadata_api_key", + "llm_api_key", + "other", +]); +export type SecretPurpose = z.infer; + +/** 加密凭证的元数据视图:密文/nonce 永不出 API */ +export const Secret = EntityBase.extend({ + name: z.string().min(1), + purpose: SecretPurpose, + algorithm: z.string(), + kdf: z.string(), + kdfParams: JsonObject, + keyVersion: z.number().int().min(1), + rotatedAt: Timestamp.nullable(), +}); +export type Secret = z.infer; +/** plaintext 只写;服务端用 SECRET_MASTER_KEY 派生密钥加密后存 ciphertext/nonce */ +export const SecretCreate = z.object({ + name: z.string().min(1).max(120), + purpose: SecretPurpose, + plaintext: z.string().min(1).max(16384), +}); +export type SecretCreate = z.infer; +export const SecretUpdate = z.object({ plaintext: z.string().min(1).max(16384) }); +export type SecretUpdate = z.infer; diff --git a/spikes/deno/packages/contracts/src/organization.ts b/spikes/deno/packages/contracts/src/organization.ts new file mode 100644 index 00000000..1850da97 --- /dev/null +++ b/spikes/deno/packages/contracts/src/organization.ts @@ -0,0 +1,288 @@ +import { z } from "zod"; +import { ActorType, EntityBase, Id, JsonObject, NonNegInt, Timestamp } from "./common"; +import { MediaKind } from "./content"; + +// 整理域:整理记录(批次)+ 逐文件结果,以及对外开放的整理记录 API 合同(REQ §12.4) +// 自动与手动整理产生同格式记录;读记录需 organize.read,发起整理需 organize.execute。 + +export const OrganizationTrigger = z.enum(["auto", "manual", "agent"]); +export type OrganizationTrigger = z.infer; +export const OrganizationStatus = z.enum(["queued", "running", "succeeded", "partial", "failed", "cancelled"]); +export type OrganizationStatus = z.infer; +/** Copy / Move / 重命名 / 目录调整 / 硬链接(仅兼容既有) */ +export const OrganizationAction = z.enum(["copy", "move", "rename", "relocate", "hardlink"]); +export type OrganizationAction = z.infer; +/** 源路径所在视图:下载器视角 / 执行进程本地 / 库根相对 */ +export const SourcePathView = z.enum(["downloader", "local", "library"]); +export type SourcePathView = z.infer; +export const FileRole = z.enum(["main", "subtitle", "audio", "bluray_dir", "nfo", "image", "other"]); +export type FileRole = z.infer; +export const FileResultStatus = z.enum(["queued", "running", "succeeded", "failed", "skipped", "cancelled"]); +export type FileResultStatus = z.infer; + +/** 稳定变更游标:bigint 序号的十进制字符串 */ +export const ChangeSeq = z.number().int().nonnegative(); +export const ChangeCursor = z.string().regex(/^\d{1,16}$/); +export type ChangeCursor = z.infer; + +// ── 内部行表示(与 organization_record / organization_file_result 列一一对应,不含 change_xid) ── + +export const OrganizationRecord = EntityBase.extend({ + taskId: Id.nullable(), + trigger: OrganizationTrigger, + actorType: ActorType, + actorId: z.string().nullable(), + mediaEntityId: Id.nullable(), + contentUnitId: Id.nullable(), + downloadTaskId: Id.nullable(), + sourceDownloaderInstanceId: Id.nullable(), + sourcePathView: SourcePathView, + sourcePath: z.string().nullable(), + targetLibraryRootId: Id.nullable(), + targetPath: z.string().nullable(), + action: OrganizationAction, + status: OrganizationStatus, + reason: z.string().nullable(), + error: JsonObject.nullable(), + /** 冻结的整理计划 */ + plan: JsonObject, + planFingerprint: z.string().nullable(), + fileTotal: NonNegInt, + fileSucceeded: NonNegInt, + fileFailed: NonNegInt, + fileSkipped: NonNegInt, + startedAt: Timestamp.nullable(), + finishedAt: Timestamp.nullable(), + changeSeq: ChangeSeq, +}); +export type OrganizationRecord = z.infer; +export const OrganizationRecordCreate = OrganizationRecord.pick({ + taskId: true, + trigger: true, + actorType: true, + actorId: true, + mediaEntityId: true, + contentUnitId: true, + downloadTaskId: true, + sourceDownloaderInstanceId: true, + sourcePathView: true, + sourcePath: true, + targetLibraryRootId: true, + targetPath: true, + action: true, + plan: true, + planFingerprint: true, +}).partial({ + taskId: true, + actorId: true, + mediaEntityId: true, + contentUnitId: true, + downloadTaskId: true, + sourceDownloaderInstanceId: true, + sourcePathView: true, + sourcePath: true, + targetLibraryRootId: true, + targetPath: true, + plan: true, + planFingerprint: true, +}); +export type OrganizationRecordCreate = z.infer; +export const OrganizationRecordUpdate = OrganizationRecord.pick({ + status: true, + reason: true, + error: true, + targetPath: true, + fileTotal: true, + fileSucceeded: true, + fileFailed: true, + fileSkipped: true, + startedAt: true, + finishedAt: true, +}).partial(); +export type OrganizationRecordUpdate = z.infer; + +export const OrganizationFileResult = EntityBase.extend({ + organizationRecordId: Id, + ordinal: z.number().int().nonnegative(), + assetOperationId: Id.nullable(), + mediaAssetId: Id.nullable(), + contentUnitId: Id.nullable(), + role: FileRole, + mode: OrganizationAction, + sourcePath: z.string().min(1), + sourceSizeBytes: NonNegInt.nullable(), + targetLibraryRootId: Id.nullable(), + targetPath: z.string().nullable(), + status: FileResultStatus, + skipReason: z.string().nullable(), + error: JsonObject.nullable(), + checksum: z.string().nullable(), + startedAt: Timestamp.nullable(), + finishedAt: Timestamp.nullable(), +}); +export type OrganizationFileResult = z.infer; +export const OrganizationFileResultCreate = OrganizationFileResult.pick({ + organizationRecordId: true, + ordinal: true, + assetOperationId: true, + contentUnitId: true, + role: true, + mode: true, + sourcePath: true, + sourceSizeBytes: true, + targetLibraryRootId: true, + targetPath: true, +}).partial({ + ordinal: true, + assetOperationId: true, + contentUnitId: true, + role: true, + sourceSizeBytes: true, + targetLibraryRootId: true, + targetPath: true, +}); +export type OrganizationFileResultCreate = z.infer; +export const OrganizationFileResultUpdate = OrganizationFileResult.pick({ + assetOperationId: true, + mediaAssetId: true, + targetPath: true, + status: true, + skipReason: true, + error: true, + checksum: true, + startedAt: true, + finishedAt: true, +}).partial(); +export type OrganizationFileResultUpdate = z.infer; + +// ── 对外开放的整理记录合同(外部程序据此适配媒体服务器) ── + +/** 失败/跳过原因:稳定 code + 可读说明;不含凭据与私有 URL */ +export const PublicReason = z.object({ + code: z.string().min(1), + message: z.string(), +}); +export type PublicReason = z.infer; + +/** 路径端点:路径 + 所在视图标识,避免跨实例同路径歧义 */ +export const PublicPathRef = z.object({ + path: z.string().nullable(), + view: z.enum(["downloader", "local", "library"]), + downloaderInstanceId: Id.nullable(), + libraryRootId: Id.nullable(), +}); +export type PublicPathRef = z.infer; + +export const PublicOrganizationFile = z.object({ + id: Id, + ordinal: z.number().int().nonnegative(), + role: FileRole, + mode: OrganizationAction, + status: FileResultStatus, + source: PublicPathRef, + target: PublicPathRef, + sizeBytes: NonNegInt.nullable(), + contentUnitId: Id.nullable(), + mediaAssetId: Id.nullable(), + operationId: Id.nullable(), + skipReason: PublicReason.nullable(), + failure: PublicReason.nullable(), + startedAt: Timestamp.nullable(), + finishedAt: Timestamp.nullable(), +}); +export type PublicOrganizationFile = z.infer; + +export const PublicOrganizationRecord = z.object({ + /** 稳定记录 ID */ + id: Id, + taskId: Id.nullable(), + /** 相关资产操作账本 ID */ + operationIds: z.array(Id), + media: z + .object({ + entityId: Id, + kind: MediaKind, + title: z.string(), + year: z.number().int().nullable(), + contentUnitId: Id.nullable(), + seasonNumber: z.number().int().nullable(), + episodeNumber: z.number().int().nullable(), + /** 已确认的外部身份,如 tmdb:movie:693134 */ + externalIds: z.array(z.object({ provider: z.string(), entityType: z.string(), externalId: z.string() })), + }) + .nullable(), + trigger: OrganizationTrigger, + /** 手动 / 自动来源(agent 视为自动) */ + origin: z.enum(["manual", "auto"]), + actor: z.object({ type: ActorType, id: z.string().nullable() }), + action: OrganizationAction, + status: OrganizationStatus, + source: PublicPathRef, + target: PublicPathRef, + reason: PublicReason.nullable(), + counts: z.object({ total: NonNegInt, succeeded: NonNegInt, failed: NonNegInt, skipped: NonNegInt }), + /** includeFiles=false 时为 null */ + files: z.array(PublicOrganizationFile).nullable(), + createdAt: Timestamp, + updatedAt: Timestamp, + startedAt: Timestamp.nullable(), + finishedAt: Timestamp.nullable(), + /** 该记录最近一次变更的游标值;客户端按 id 去重、以最大 changeSeq 续读 */ + changeSeq: ChangeCursor, +}); +export type PublicOrganizationRecord = z.infer; + +/** 增量读取请求:after 为上次响应的 nextCursor(首读省略即从头) */ +export const OrganizationChangesQuery = z.object({ + after: ChangeCursor.optional(), + limit: z.coerce.number().int().min(1).max(500).default(100), + includeFiles: z.union([z.boolean(), z.enum(["true", "false"]).transform((v) => v === "true")]).default(true), + status: OrganizationStatus.optional(), + mediaEntityId: Id.optional(), +}); +export type OrganizationChangesQuery = z.infer; + +/** + * 增量读取响应:items 按 changeSeq 升序;nextCursor 总是返回(无新数据时等于请求的 after), + * hasMore=true 表示应立即继续拉取。同一记录状态更新会以更大的 changeSeq 再次出现。 + */ +export const OrganizationChangesResponse = z.object({ + items: z.array(PublicOrganizationRecord), + nextCursor: ChangeCursor, + hasMore: z.boolean(), +}); +export type OrganizationChangesResponse = z.infer; + +/** 列表查询(控制台):按创建时间倒序的不透明游标分页 */ +export const OrganizationListQuery = z.object({ + cursor: z.string().min(1).optional(), + limit: z.coerce.number().int().min(1).max(200).default(50), + status: OrganizationStatus.optional(), + trigger: OrganizationTrigger.optional(), + mediaEntityId: Id.optional(), + q: z.string().max(200).optional(), +}); +export type OrganizationListQuery = z.infer; + +/** 手动发起整理(organize.execute):返回 record/task ID,由任务页追踪 */ +export const OrganizeRequest = z + .object({ + downloadTaskId: Id.optional(), + /** 执行进程可访问的源路径(与 downloadTaskId 二选一) */ + sourcePaths: z.array(z.string().startsWith("/")).min(1).max(1000).optional(), + mediaEntityId: Id.optional(), + contentUnitId: Id.optional(), + action: z.enum(["copy", "move"]), + targetLibraryRootId: Id.optional(), + /** true 只生成计划(organize.plan),不执行 */ + dryRun: z.boolean().default(false), + }) + .refine((v) => !!v.downloadTaskId !== !!v.sourcePaths, { message: "downloadTaskId 与 sourcePaths 必须二选一" }); +export type OrganizeRequest = z.infer; + +export const OrganizeAccepted = z.object({ + recordId: Id.nullable(), + taskId: Id, + planId: Id.nullable(), +}); +export type OrganizeAccepted = z.infer; diff --git a/spikes/deno/packages/contracts/src/policy.ts b/spikes/deno/packages/contracts/src/policy.ts new file mode 100644 index 00000000..46a41460 --- /dev/null +++ b/spikes/deno/packages/contracts/src/policy.ts @@ -0,0 +1,247 @@ +import { z } from "zod"; +import { EntityBase, Id, JsonObject, SERVER_MANAGED, Timestamp } from "./common"; + +// 规则域:订阅、策略版本、收录 Slot、偏好标签与抑制记录(REQ §6) + +export const PolicyKind = z.enum(["acquisition", "preference", "retention", "cleanup", "instance", "automation"]); +export type PolicyKind = z.infer; +export const PolicyStatus = z.enum(["draft", "published", "superseded", "rolled_back", "archived"]); +export type PolicyStatus = z.infer; + +/** 策略版本:草案 → 预览 → 授权发布 → 版本化/回滚;同一 policyKey 只有一个 published */ +export const PolicyRevision = EntityBase.extend({ + policyKey: z.string().min(1), + revision: z.number().int().min(1), + kind: PolicyKind, + status: PolicyStatus, + title: z.string().min(1), + summary: z.string().nullable(), + scope: JsonObject, + /** 结构化规则数据;不是可执行脚本或工作流 DSL */ + rules: JsonObject, + preview: JsonObject.nullable(), + basedOnRevisionId: Id.nullable(), + createdBy: z.string().nullable(), + publishedAt: Timestamp.nullable(), + publishedBy: z.string().nullable(), +}); +export type PolicyRevision = z.infer; +/** 新建总是草案;revision 由服务端分配 */ +export const PolicyRevisionCreate = PolicyRevision.pick({ + policyKey: true, + kind: true, + title: true, + summary: true, + scope: true, + rules: true, + basedOnRevisionId: true, +}).partial({ summary: true, scope: true, basedOnRevisionId: true }); +export type PolicyRevisionCreate = z.infer; +/** 只有草案可改内容;发布/回滚走动作端点 */ +export const PolicyRevisionUpdate = PolicyRevisionCreate.omit({ policyKey: true, kind: true }).partial(); +export type PolicyRevisionUpdate = z.infer; + +export const SubscriptionMediaKind = z.enum(["movie", "series", "variety"]); +export type SubscriptionMediaKind = z.infer; +export const SubscriptionMode = z.enum(["once", "continuous"]); +export type SubscriptionMode = z.infer; +/** 仅本季 / 持续后续季(REQ §6.5) */ +export const SeasonScope = z.enum(["current_season", "following_seasons"]); +export type SeasonScope = z.infer; +export const SubscriptionOrigin = z.enum(["user", "agent", "policy", "import"]); +export type SubscriptionOrigin = z.infer; +/** 默认优先级:即时 > 追更 > 新作品 > 升级(REQ §6.8) */ +export const AcquisitionPriority = z.enum(["immediate", "tracking", "new_work", "upgrade"]); +export type AcquisitionPriority = z.infer; +export const SubscriptionStatus = z.enum(["active", "paused", "completed", "cancelled"]); +export type SubscriptionStatus = z.infer; + +export const Subscription = EntityBase.extend({ + mediaEntityId: Id.nullable(), + seasonUnitId: Id.nullable(), + /** 作品尚未建档时的检索词 */ + titleQuery: z.string().nullable(), + mediaKind: SubscriptionMediaKind, + mode: SubscriptionMode, + seasonScope: SeasonScope, + origin: SubscriptionOrigin, + priority: AcquisitionPriority, + status: SubscriptionStatus, + policyRevisionId: Id.nullable(), + params: JsonObject, + statusReason: z.string().nullable(), + lastCheckedAt: Timestamp.nullable(), + createdBy: z.string().nullable(), +}); +export type Subscription = z.infer; +export const SubscriptionCreate = Subscription.pick({ + mediaEntityId: true, + seasonUnitId: true, + titleQuery: true, + mediaKind: true, + mode: true, + seasonScope: true, + origin: true, + priority: true, + policyRevisionId: true, + params: true, +}) + .partial({ + mediaEntityId: true, + seasonUnitId: true, + titleQuery: true, + mode: true, + seasonScope: true, + origin: true, + priority: true, + policyRevisionId: true, + params: true, + }) + .refine((v) => !!v.mediaEntityId || !!v.titleQuery, { message: "mediaEntityId 与 titleQuery 至少给一个" }); +export type SubscriptionCreate = z.infer; +export const SubscriptionUpdate = Subscription.pick({ + mode: true, + seasonScope: true, + priority: true, + status: true, + policyRevisionId: true, + params: true, + statusReason: true, +}).partial(); +export type SubscriptionUpdate = z.infer; + +/** Slot 状态(REQ §6.1) */ +export const SlotStatus = z.enum([ + "empty", + "reserved", + "downloading", + "verifying", + "satisfied", + "upgrade_pending", + "attention", +]); +export type SlotStatus = z.infer; + +/** 独立收录目标:硬条件 / 软偏好 / 回退 / 升级阈值 / 完成条件 */ +export const AcquisitionSlot = EntityBase.extend({ + mediaEntityId: Id, + contentUnitId: Id.nullable(), + subscriptionId: Id.nullable(), + policyRevisionId: Id.nullable(), + /** 用户命名,如 archival / streaming */ + name: z.string().min(1).max(64), + status: SlotStatus, + hardConditions: JsonObject, + softPreferences: JsonObject, + fallback: JsonObject, + upgradeThreshold: JsonObject, + completion: JsonObject, + /** 同组 Slot 在 requireDistinctAsset=true 时必须由不同物理资产满足(M2 校验) */ + distinctGroup: z.string().nullable(), + requireDistinctAsset: z.boolean(), + currentAssetId: Id.nullable(), + reservedPlanId: Id.nullable(), + statusReason: z.string().nullable(), + lastEvaluatedAt: Timestamp.nullable(), + nextEvaluateAt: Timestamp.nullable(), + revision: z.number().int().min(1), +}); +export type AcquisitionSlot = z.infer; +export const AcquisitionSlotCreate = AcquisitionSlot.pick({ + mediaEntityId: true, + contentUnitId: true, + subscriptionId: true, + policyRevisionId: true, + name: true, + hardConditions: true, + softPreferences: true, + fallback: true, + upgradeThreshold: true, + completion: true, + distinctGroup: true, + requireDistinctAsset: true, +}).partial({ + contentUnitId: true, + subscriptionId: true, + policyRevisionId: true, + hardConditions: true, + softPreferences: true, + fallback: true, + upgradeThreshold: true, + completion: true, + distinctGroup: true, + requireDistinctAsset: true, +}); +export type AcquisitionSlotCreate = z.infer; +/** 规则可改;status/currentAsset/reservedPlan 只由状态机推进,不经 Update */ +export const AcquisitionSlotUpdate = AcquisitionSlotCreate.omit({ + mediaEntityId: true, + contentUnitId: true, + name: true, +}) + .partial() + .extend({ expectedRevision: z.number().int().min(1) }); +export type AcquisitionSlotUpdate = z.infer; + +/** 偏好类型:soft 影响排序;hard 硬排除;version 版本偏好;target 独立目标配置(REQ §6.7) */ +export const PreferenceKind = z.enum(["soft", "hard", "version", "target"]); +export type PreferenceKind = z.infer; +export const PreferenceOrigin = z.enum(["manual", "conversation", "agent_inferred"]); +export type PreferenceOrigin = z.infer; +export const PreferenceStatus = z.enum(["active", "disabled", "deleted"]); +export type PreferenceStatus = z.infer; + +export const PreferenceTag = EntityBase.extend({ + label: z.string().min(1).max(80), + kind: PreferenceKind, + scope: JsonObject, + rule: JsonObject, + /** 语义指纹(kind+scope+rule 规范化哈希),用于抑制匹配 */ + fingerprint: z.string().min(1), + origin: PreferenceOrigin, + description: z.string().nullable(), + status: PreferenceStatus, + revision: z.number().int().min(1), + evidence: JsonObject, + policyRevisionId: Id.nullable(), +}); +export type PreferenceTag = z.infer; +/** fingerprint 由服务端计算 */ +export const PreferenceTagCreate = PreferenceTag.pick({ + label: true, + kind: true, + scope: true, + rule: true, + origin: true, + description: true, + evidence: true, +}).partial({ scope: true, rule: true, origin: true, description: true, evidence: true }); +export type PreferenceTagCreate = z.infer; +export const PreferenceTagUpdate = PreferenceTagCreate.omit({ origin: true }) + .partial() + .extend({ status: PreferenceStatus.optional() }); +export type PreferenceTagUpdate = z.infer; + +/** 手动取消标签留下的抑制记录:Agent 不得据同一旧证据自动加回 */ +export const PreferenceSuppression = EntityBase.extend({ + preferenceTagId: Id.nullable(), + fingerprint: z.string().min(1), + reason: z.string().nullable(), + evidence: JsonObject, + suppressedBy: z.string().min(1), + active: z.boolean(), + liftedAt: Timestamp.nullable(), + liftedBy: z.string().nullable(), +}); +export type PreferenceSuppression = z.infer; +export const PreferenceSuppressionCreate = PreferenceSuppression.omit({ + ...SERVER_MANAGED, + active: true, + liftedAt: true, + liftedBy: true, + suppressedBy: true, +}).partial({ preferenceTagId: true, reason: true, evidence: true }); +export type PreferenceSuppressionCreate = z.infer; +/** 唯一允许的更新是解除抑制 */ +export const PreferenceSuppressionUpdate = z.object({ active: z.literal(false), reason: z.string().optional() }); +export type PreferenceSuppressionUpdate = z.infer; diff --git a/spikes/deno/packages/contracts/src/settings.ts b/spikes/deno/packages/contracts/src/settings.ts new file mode 100644 index 00000000..6622de76 --- /dev/null +++ b/spikes/deno/packages/contracts/src/settings.ts @@ -0,0 +1,157 @@ +import { z } from "zod"; +import { Id, Timestamp } from "./common"; + +// 全局设置:存于 settings 表(key → jsonb)。每个顶层分组对应一个 settings.key。 +// schema 本身不带 .default()(zod 4 在 partial 里仍会套默认值,会让 PATCH 覆盖现值); +// 缺省值统一放在 DEFAULT_SETTINGS,服务端读取时 `{ ...DEFAULT_SETTINGS[key], ...stored }` 后再 parse。 + +const Percent = z.number().min(1).max(100); + +export const AutomationSettings = z.object({ + /** 自动化总开关:关闭后所有自动收录/下载/整理停止,只读查询与手动操作可用 */ + enabled: z.boolean(), + /** 恢复后的只读对账模式(REQ §8.5):开启时禁止自动写入 */ + readOnlyReconcile: z.boolean(), +}); +export type AutomationSettings = z.infer; + +const CacheShape = z.object({ + /** 缓存卷使用率硬阈值:达到即停止新增下载并暂停纳管下载中任务 */ + hardThresholdPercent: Percent, + /** 恢复水位:降到此值以下恢复因阈值暂停的任务 */ + resumePercent: Percent, + checkIntervalSec: z.number().int().min(10).max(3600), + /** 缓存清理的目标使用率(低水位) */ + cleanupTargetPercent: Percent, +}); +/** 下载缓存卷硬阈值与恢复水位(REQ §8.4.1) */ +export const CacheSettings = CacheShape.refine((v) => v.resumePercent < v.hardThresholdPercent, { + message: "恢复水位必须低于硬阈值", +}).refine((v) => v.cleanupTargetPercent <= v.resumePercent, { message: "清理目标不得高于恢复水位" }); +export type CacheSettings = z.infer; + +export const OrganizeMode = z.enum(["copy", "move"]); +export type OrganizeMode = z.infer; +export const OrganizeSettings = z.object({ + defaultMode: OrganizeMode, + /** 跨卷 Move 采用 Copy→核验→删源;EXDEV 不静默降级 */ + allowCrossVolumeMove: z.boolean(), +}); +export type OrganizeSettings = z.infer; + +export const RetentionSettings = z.object({ + /** Copy 入库后下载源在缓存中至少保留的天数(与 H&R 要求分开判断) */ + seedRetentionDays: z.number().int().min(0).max(365), + /** 观察/日志类数据保留天数 */ + logRetentionDays: z.number().int().min(1).max(3650), +}); +export type RetentionSettings = z.infer; + +export const ThinkingLevel = z.enum(["off", "minimal", "low", "medium", "high", "xhigh"]); +export type ThinkingLevel = z.infer; + +/** Pi Agent 设置(REQ §10.5):上游/模型/思考档位/上下文预算/压缩/子代理;密钥只存 secret 引用 */ +export const PiSettings = z.object({ + provider: z.string().min(1), + baseUrl: z.url().nullable(), + model: z.string().min(1).nullable(), + thinkingLevel: ThinkingLevel, + /** 上下文预算(token) */ + contextBudget: z.number().int().min(4096).max(2_000_000), + compaction: z.object({ + enabled: z.boolean(), + /** 达到上下文预算的该比例时触发压缩 */ + thresholdRatio: z.number().min(0.3).max(0.95), + }), + subagentsEnabled: z.boolean(), + apiKeySecretRef: Id.nullable(), +}); +export type PiSettings = z.infer; + +/** 每日预算与并发(REQ §6.4、§10.5) */ +export const BudgetSettings = z.object({ + maxNewTitlesPerDay: z.number().int().min(0), + maxDownloadBytesPerDay: z.number().int().min(0), + maxConcurrentDownloads: z.number().int().min(1).max(100), + agentMaxRunsPerDay: z.number().int().min(0), + agentMaxCostUsdPerDay: z.number().min(0), +}); +export type BudgetSettings = z.infer; + +export const GlobalSettings = z.object({ + automation: AutomationSettings, + cache: CacheSettings, + organize: OrganizeSettings, + retention: RetentionSettings, + pi: PiSettings, + budget: BudgetSettings, +}); +export type GlobalSettings = z.infer; + +/** settings 表的合法 key */ +export const SettingsKey = GlobalSettings.keyof(); +export type SettingsKey = z.infer; + +/** 缺省设置(settings 表缺 key 或缺字段时使用) */ +export const DEFAULT_SETTINGS: GlobalSettings = { + automation: { enabled: true, readOnlyReconcile: false }, + cache: { hardThresholdPercent: 90, resumePercent: 85, checkIntervalSec: 60, cleanupTargetPercent: 80 }, + organize: { defaultMode: "copy", allowCrossVolumeMove: true }, + retention: { seedRetentionDays: 7, logRetentionDays: 180 }, + pi: { + provider: "anthropic", + baseUrl: null, + model: null, + thinkingLevel: "medium", + contextBudget: 128_000, + compaction: { enabled: true, thresholdRatio: 0.8 }, + subagentsEnabled: false, + apiKeySecretRef: null, + }, + budget: { + maxNewTitlesPerDay: 20, + maxDownloadBytesPerDay: 500 * 1024 ** 3, + maxConcurrentDownloads: 5, + agentMaxRunsPerDay: 50, + agentMaxCostUsdPerDay: 5, + }, +}; + +/** PATCH /settings:按分组部分更新;服务端与现值合并后用 GlobalSettings 整体再校验 */ +export const GlobalSettingsUpdate = z.object({ + automation: AutomationSettings.partial().optional(), + cache: CacheShape.partial().optional(), + organize: OrganizeSettings.partial().optional(), + retention: RetentionSettings.partial().optional(), + pi: PiSettings.omit({ apiKeySecretRef: true }) + .partial() + .extend({ + compaction: PiSettings.shape.compaction.partial().optional(), + /** 只写:明文上游密钥,服务端存为 secret 并回填 apiKeySecretRef;传 null 表示清除 */ + apiKey: z.string().min(1).max(4096).nullable().optional(), + }) + .optional(), + budget: BudgetSettings.partial().optional(), +}); +export type GlobalSettingsUpdate = z.infer; + +/** 合并部分更新(纯函数,web 乐观更新与 server 共用) */ +export function mergeSettings(current: GlobalSettings, patch: GlobalSettingsUpdate): GlobalSettings { + const { apiKey: _apiKey, compaction, ...pi } = patch.pi ?? {}; + return { + automation: { ...current.automation, ...patch.automation }, + cache: { ...current.cache, ...patch.cache }, + organize: { ...current.organize, ...patch.organize }, + retention: { ...current.retention, ...patch.retention }, + pi: { ...current.pi, ...pi, compaction: { ...current.pi.compaction, ...compaction } }, + budget: { ...current.budget, ...patch.budget }, + }; +} + +/** settings 表行 */ +export const SettingEntry = z.object({ + key: SettingsKey, + value: z.unknown(), + updatedAt: Timestamp, +}); +export type SettingEntry = z.infer; diff --git a/spikes/deno/packages/contracts/src/sources.ts b/spikes/deno/packages/contracts/src/sources.ts new file mode 100644 index 00000000..f31a7c90 --- /dev/null +++ b/spikes/deno/packages/contracts/src/sources.ts @@ -0,0 +1,261 @@ +import { z } from "zod"; +import { EntityBase, Id, JsonObject, NonNegInt, SERVER_MANAGED, Timestamp } from "./common"; + +// 来源域:站点、站点账号、Feed、站点资源、观察(REQ §4) +// 含 passkey 的 Feed URL 只以 urlSecretRef 保存,对外只给脱敏的 urlDisplay。 + +export const SiteAdapter = z.enum(["nexusphp", "mteam_api", "generic_rss"]); +export type SiteAdapter = z.infer; +export const SiteHealth = z.enum(["unknown", "healthy", "degraded", "failing", "disabled"]); +export type SiteHealth = z.infer; +export const CircuitState = z.enum(["closed", "open", "half_open"]); +export type CircuitState = z.infer; + +/** 站点级限流(快照、实时搜索、浏览器共享) */ +export const SiteRateLimit = z.object({ + requestsPerMinute: z.number().int().positive().optional(), + minIntervalMs: z.number().int().nonnegative().optional(), + concurrency: z.number().int().positive().optional(), +}); +export type SiteRateLimit = z.infer; + +export const Site = EntityBase.extend({ + key: z.string().regex(/^[a-z][a-z0-9_-]*$/), + name: z.string().min(1), + adapter: SiteAdapter, + baseUrl: z.url(), + enabled: z.boolean(), + rateLimit: SiteRateLimit, + config: JsonObject, + healthStatus: SiteHealth, + circuitState: CircuitState, + circuitOpenUntil: Timestamp.nullable(), + snapshotEnabled: z.boolean(), + snapshotIntervalSec: z.number().int().positive().nullable(), + lastSnapshotAt: Timestamp.nullable(), + snapshotCursor: JsonObject, +}); +export type Site = z.infer; +export const SiteCreate = Site.pick({ + key: true, + name: true, + adapter: true, + baseUrl: true, + enabled: true, + rateLimit: true, + config: true, + snapshotEnabled: true, + snapshotIntervalSec: true, +}).partial({ enabled: true, rateLimit: true, config: true, snapshotEnabled: true, snapshotIntervalSec: true }); +export type SiteCreate = z.infer; +export const SiteUpdate = SiteCreate.omit({ key: true }).partial(); +export type SiteUpdate = z.infer; + +export const SiteAuthKind = z.enum(["cookie", "passkey", "api_key", "password"]); +export type SiteAuthKind = z.infer; +export const SiteAccountStatus = z.enum(["unknown", "active", "invalid", "expired"]); +export type SiteAccountStatus = z.infer; + +export const SiteAccount = EntityBase.extend({ + siteId: Id, + label: z.string().min(1), + authKind: SiteAuthKind, + secretRef: Id.nullable(), + enabled: z.boolean(), + status: SiteAccountStatus, + lastVerifiedAt: Timestamp.nullable(), + userInfo: JsonObject, +}); +export type SiteAccount = z.infer; +export const SiteAccountCreate = SiteAccount.pick({ siteId: true, label: true, authKind: true, enabled: true }) + .partial({ enabled: true }) + .extend({ credential: z.string().min(1).max(16384) }); +export type SiteAccountCreate = z.infer; +export const SiteAccountUpdate = SiteAccountCreate.omit({ siteId: true }).partial(); +export type SiteAccountUpdate = z.infer; + +export const FeedPurpose = z.enum(["movie", "series", "variety", "promotion", "search", "generic"]); +export type FeedPurpose = z.infer; +export const FeedKind = z.enum(["rss", "atom", "list", "query"]); +export type FeedKind = z.infer; +export const FeedHealth = z.enum(["unknown", "healthy", "degraded", "failing", "circuit_open", "disabled"]); +export type FeedHealth = z.infer; + +export const FeedSource = EntityBase.extend({ + siteId: Id.nullable(), + siteAccountId: Id.nullable(), + name: z.string().min(1), + purpose: FeedPurpose, + kind: FeedKind, + urlSecretRef: Id.nullable(), + /** 脱敏 URL(passkey 等参数替换为 [REDACTED]) */ + urlDisplay: z.string(), + parserVersion: z.string(), + enabled: z.boolean(), + pollIntervalSec: z.number().int().min(60), + filter: JsonObject, + cursor: JsonObject, + etag: z.string().nullable(), + lastModified: z.string().nullable(), + lastFetchAt: Timestamp.nullable(), + lastSuccessAt: Timestamp.nullable(), + nextFetchAt: Timestamp.nullable(), + consecutiveFailures: NonNegInt, + backoffUntil: Timestamp.nullable(), + healthStatus: FeedHealth, + lastError: JsonObject.nullable(), +}); +export type FeedSource = z.infer; +/** url 为只写明文(可能含 passkey),服务端加密存为 secret 并生成 urlDisplay */ +export const FeedSourceCreate = FeedSource.pick({ + siteId: true, + siteAccountId: true, + name: true, + purpose: true, + kind: true, + enabled: true, + pollIntervalSec: true, + filter: true, +}) + .partial({ + siteId: true, + siteAccountId: true, + purpose: true, + kind: true, + enabled: true, + pollIntervalSec: true, + filter: true, + }) + .extend({ url: z.url() }); +export type FeedSourceCreate = z.infer; +export const FeedSourceUpdate = FeedSourceCreate.partial(); +export type FeedSourceUpdate = z.infer; + +export const ResolutionStatus = z.enum(["unresolved", "resolved", "ambiguous", "unidentifiable", "rejected"]); +export type ResolutionStatus = z.infer; +export const ReleaseAvailability = z.enum(["active", "site_disabled", "expired", "unknown"]); +export type ReleaseAvailability = z.infer; + +/** 促销状态必须带观察时间与过期时间,下单前按策略复核(REQ §4.3) */ +export const Promotion = z.object({ + kind: z.string().optional(), + observedAt: Timestamp.optional(), + expiresAt: Timestamp.nullable().optional(), + raw: z.unknown().optional(), +}); +export type Promotion = z.infer; + +export const TorrentFileEntry = z.object({ + path: z.string(), + size: NonNegInt, +}); +export type TorrentFileEntry = z.infer; + +export const SourceRelease = EntityBase.extend({ + siteId: Id, + siteReleaseId: z.string().min(1), + title: z.string(), + subtitle: z.string().nullable(), + categoryRaw: z.string().nullable(), + tagsRaw: z.array(z.string()), + sizeBytes: NonNegInt.nullable(), + infohashV1: z.string().nullable(), + infohashV2: z.string().nullable(), + fileList: z.array(TorrentFileEntry).nullable(), + promotion: Promotion, + promotionExpiresAt: Timestamp.nullable(), + seeders: z.number().int().nullable(), + leechers: z.number().int().nullable(), + /** 站点发布时间;区别于 firstSeenAt(本系统首次发现) */ + sourcePublishedAt: Timestamp.nullable(), + firstSeenAt: Timestamp, + lastSeenAt: Timestamp, + detailFetchedAt: Timestamp.nullable(), + /** 标题解析结果(标题/年份/季集/分辨率/来源/编码/发布组…) */ + parsed: JsonObject, + parserVersion: z.string().nullable(), + mediaEntityId: Id.nullable(), + contentUnitId: Id.nullable(), + resolutionStatus: ResolutionStatus, + resolutionEvidence: JsonObject, + availability: ReleaseAvailability, + raw: JsonObject, +}); +export type SourceRelease = z.infer; +/** 由适配器/任务写入(按 siteId+siteReleaseId upsert) */ +export const SourceReleaseCreate = SourceRelease.omit({ + ...SERVER_MANAGED, + firstSeenAt: true, + lastSeenAt: true, +}).partial({ + subtitle: true, + categoryRaw: true, + tagsRaw: true, + sizeBytes: true, + infohashV1: true, + infohashV2: true, + fileList: true, + promotion: true, + promotionExpiresAt: true, + seeders: true, + leechers: true, + sourcePublishedAt: true, + detailFetchedAt: true, + parsed: true, + parserVersion: true, + mediaEntityId: true, + contentUnitId: true, + resolutionStatus: true, + resolutionEvidence: true, + availability: true, + raw: true, +}); +export type SourceReleaseCreate = z.infer; +export const SourceReleaseUpdate = SourceReleaseCreate.omit({ siteId: true, siteReleaseId: true }).partial(); +export type SourceReleaseUpdate = z.infer; + +export const ObservationStatus = z.enum(["new", "linked", "ignored", "error"]); +export type ObservationStatus = z.infer; + +export const Observation = EntityBase.extend({ + feedId: Id, + guid: z.string().min(1), + contentDigest: z.string().min(1), + sourceReleaseId: Id.nullable(), + title: z.string(), + /** 已脱敏的链接;原始下载链接不对外 */ + linkRedacted: z.string().nullable(), + raw: JsonObject, + sourcePublishedAt: Timestamp.nullable(), + fetchedAt: Timestamp, + firstSeenAt: Timestamp, + lastSeenAt: Timestamp, + status: ObservationStatus, + resolveAttempts: NonNegInt, + nextResolveAt: Timestamp.nullable(), + lastError: JsonObject.nullable(), +}); +export type Observation = z.infer; +export const ObservationCreate = Observation.pick({ + feedId: true, + guid: true, + contentDigest: true, + sourceReleaseId: true, + title: true, + linkRedacted: true, + raw: true, + sourcePublishedAt: true, + fetchedAt: true, +}).partial({ sourceReleaseId: true, linkRedacted: true, raw: true, sourcePublishedAt: true, fetchedAt: true }); +export type ObservationCreate = z.infer; +export const ObservationUpdate = Observation.pick({ + contentDigest: true, + sourceReleaseId: true, + title: true, + raw: true, + status: true, + resolveAttempts: true, + nextResolveAt: true, + lastError: true, +}).partial(); +export type ObservationUpdate = z.infer; diff --git a/spikes/deno/packages/contracts/tsconfig.json b/spikes/deno/packages/contracts/tsconfig.json new file mode 100644 index 00000000..7680f997 --- /dev/null +++ b/spikes/deno/packages/contracts/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "test"] +} diff --git a/spikes/deno/packages/x/deno.json b/spikes/deno/packages/x/deno.json new file mode 100644 index 00000000..37c49512 --- /dev/null +++ b/spikes/deno/packages/x/deno.json @@ -0,0 +1,5 @@ +{ + "name": "@spike/x", + "version": "0.0.0", + "exports": { ".": "./src/mod.ts" } +} diff --git a/spikes/deno/packages/x/src/mod.ts b/spikes/deno/packages/x/src/mod.ts new file mode 100644 index 00000000..ffdab141 --- /dev/null +++ b/spikes/deno/packages/x/src/mod.ts @@ -0,0 +1,20 @@ +export const REDACTED = "[REDACTED]"; +const SENSITIVE_KEY = /passkey|token|cookie|authorization|password|secret|api[-_]?key/i; +const SENSITIVE_QUERY = /([?&](?:passkey|token|access_token|api_key|apikey|auth|sign)=)[^&#\s"'<>]*/gi; +export function redactString(s: string): string { + return s.replace(SENSITIVE_QUERY, `$1${REDACTED}`); +} +export function redact(value: unknown, depth = 0): unknown { + if (depth > 8) return value; + if (typeof value === "string") return redactString(value); + if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1)); + if (value && typeof value === "object") { + const out: Record = {}; + for (const [k, v] of Object.entries(value)) { + if (SENSITIVE_KEY.test(k) && typeof v !== "number" && v != null) out[k] = REDACTED; + else out[k] = redact(v, depth + 1); + } + return out; + } + return value; +} diff --git a/spikes/deno/tsconfig.base.json b/spikes/deno/tsconfig.base.json new file mode 100644 index 00000000..ed724dc1 --- /dev/null +++ b/spikes/deno/tsconfig.base.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "resolveJsonModule": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": false, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + } +} diff --git a/spikes/deno/tsconfig.tsc.json b/spikes/deno/tsconfig.tsc.json new file mode 100644 index 00000000..ce3ef39e --- /dev/null +++ b/spikes/deno/tsconfig.tsc.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"], + "paths": { "@spike/x": ["./packages/x/src/mod.ts"] } + }, + "include": ["types/deno.d.ts", "packages/x/src", "apps/server/src", "apps/server/test/node-apis.test.ts", "apps/server/vitest.config.ts"] +} diff --git a/spikes/deno/web/README.md b/spikes/deno/web/README.md new file mode 100644 index 00000000..55884c78 --- /dev/null +++ b/spikes/deno/web/README.md @@ -0,0 +1,3 @@ +# @umt/web + +React Router 7 SPA 控制台(`ssr:false`),沿用 uhd_web 设计系统(token 见 `app/app.css`,规范见 `docs/design/`)。只通过 `/api/v1` 与 server 通信,请求/响应类型一律来自 `@umt/contracts`,不手写重复 DTO。开发端口 5180,`/api` 代理到 http://localhost:4380。 diff --git a/spikes/deno/web/app/api/health.ts b/spikes/deno/web/app/api/health.ts new file mode 100644 index 00000000..9a7fa3f2 --- /dev/null +++ b/spikes/deno/web/app/api/health.ts @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import type { HealthStatus } from "@umt/contracts"; +import { apiData, TTL_SECOND } from "~/lib/api"; + +/** 服务健康状态,15 秒轮询 */ +export function useHealth() { + return useQuery({ + queryKey: ["health"], + queryFn: ({ signal }) => apiData("/api/v1/health", { signal }), + refetchInterval: 15 * TTL_SECOND, + retry: false, + }); +} diff --git a/spikes/deno/web/app/app.css b/spikes/deno/web/app/app.css new file mode 100644 index 00000000..56a1ce3d --- /dev/null +++ b/spikes/deno/web/app/app.css @@ -0,0 +1,1616 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +/* ───────────────────────── Tailwind configuration ───────────────────────── */ + +@custom-variant dark (&:is(.dark *), &:is([data-force-dark] *)); + +@theme { + --font-sans: + "Inter Tight Variable", "Inter Tight", "SF Pro Display", "PingFang SC", + "Microsoft YaHei", sans-serif; + --font-display: + "Space Grotesk Variable", "Space Grotesk", "Inter Tight Variable", + "SF Pro Display", sans-serif; + + --color-ink-950: #040507; + --color-ink-900: #080b12; + --color-ink-800: #141925; + --color-ink-700: #202637; + + --color-bone-50: #fbfaf7; + --color-bone-100: #f1eee7; + --color-bone-200: #dcd5c8; + --color-bone-300: #bdb4a5; + --color-bone-400: #918879; + --color-bone-500: #6f675c; + + --color-gold-100: #fff0c4; + --color-gold-200: #f7d98d; + --color-gold-300: #e6b85c; + --color-gold-400: #d49a4a; + --color-gold-500: #af7133; + + --tracking-hero: 0; + + --shadow-card: 0 18px 40px -24px rgba(0, 0, 0, 0.9); + --shadow-card-hover: 0 26px 54px -24px rgba(0, 0, 0, 1); + + --animate-fade-in: fade-in 520ms ease-out both; + --animate-pg-pop: pg-pop 200ms var(--ease-pop) both; + /* 播放器加载提示的滚动字幕:新一行从下方滚入,上一行向上滚出 */ + --animate-ticker-in: ticker-in 360ms var(--ease-standard) both; + --animate-ticker-out: ticker-out 360ms var(--ease-standard) both; + --animate-blur-in: blur-in 560ms var(--ease-pop) backwards; + --animate-hero-fade: hero-fade 740ms ease-out both; + --animate-ken-burns: ken-burns 14s ease-out both; + + /* ─── 字号阶梯(见 docs/features/design-tokens.md)────────────────────── + 只定字号不定行高,行为等同原来的 text-[Npx];自带行高的 text-xs / text-sm 继续用。 + 组件按角色引用,不写像素。正文阶:10 / 11 / 12 / 13 / 14 / 15 / 16 / 17。 */ + --text-micro: 10px; + --text-label: 11px; + --text-meta: 12px; + --text-caption: 13px; + --text-body: 14px; + --text-control: 15px; + --text-lead: 16px; + --text-lead-lg: 17px; + /* 标题阶:18 / 20 / 22 / 24 / 26 / 28 / 30 / 32 / 34 / 42 / 44 / 52 / 56(现状值逐档保留,收敛另议) */ + --text-title-xs: 18px; + --text-title-sm: 20px; + --text-title: 22px; + --text-title-lg: 24px; + --text-headline-xs: 26px; + --text-headline-sm: 28px; + --text-headline: 30px; + --text-headline-lg: 32px; + --text-display-xs: 34px; + --text-display-sm: 42px; + --text-display: 44px; + --text-display-lg: 52px; + --text-display-xl: 56px; + /* 流式标题:每处 clamp 原值保留,按用途命名;要不要收成三档是设计决定 */ + --text-hero-stream: clamp(32px, 9vw, 46px); + --text-hero-stream-sm: clamp(40px, 7.5vw, 72px); + --text-hero-stream-md: clamp(44px, 7.5vw, 88px); + --text-detail-title: clamp(32px, 8vw, 82px); + --text-notfound-code: clamp(7rem, 18vw, 11rem); + --text-pricing-hero: clamp(2rem, 4.4vw, 4rem); + --text-pricing-title: clamp(1.6rem, 2.4vw, 2.2rem); + --text-auth-title: clamp(2rem, 3.4vw, 3.4rem); + --text-landing-hero: clamp(2.1rem, 4.1vw, 3.9rem); + --text-landing-platforms-title: clamp(1.8rem, 3.4vw, 2.8rem); + --text-landing-immersive-title: clamp(2.25rem, 4.8vw, 5.4rem); + --text-landing-service-hero: clamp(2rem, 4.4vw, 4.4rem); + --text-landing-service-title: clamp(1.6rem, 2.4vw, 2.4rem); + --text-landing-service-title-lg: clamp(1.6rem, 2.6vw, 2.6rem); + --text-landing-service-sub: clamp(1.5rem, 2vw, 2rem); + + /* ─── 缓动(见 docs/features/design-tokens.md)。standard 是绝大多数过渡;spring 给 + 滑块 / 按压回弹;sheet 给抽屉;pop 给弹层出现。组件用 ease-standard 等,不写 bezier */ + --ease-standard: cubic-bezier(0.4, 0, 0.2, 1); + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + --ease-spring-soft: cubic-bezier(0.34, 1.2, 0.64, 1); + --ease-sheet: cubic-bezier(0.32, 0.72, 0, 1); + --ease-pop: cubic-bezier(0.22, 1, 0.36, 1); +} + +/* 过渡时长四档 + 两档特例。Tailwind 没有时长的主题命名空间,用 @utility 定义, + 写法与内置 duration-N 完全一致(同时设 --tw-duration,tw-animate 的 animate-in 也读它)。 */ +:root { + --duration-micro: 150ms; + --duration-base: 200ms; + --duration-layout: 300ms; + --duration-slow: 500ms; + --duration-hero: 700ms; + --duration-cinematic: 1000ms; +} + +/* 横向滚动容器会连带裁掉纵向溢出(overflow-x:auto 时 overflow-y 不能是 visible), + 里面胶囊、分段滑块的阴影与描边会被切成直边。四周向外借 12px 再用内边距还回来, + 布局位置不变,阴影有地方画。放胶囊的横向滚动容器一律加它 */ +@utility scroll-bleed { + margin: -0.75rem; + padding: 0.75rem; +} + +@utility duration-micro { --tw-duration: var(--duration-micro); transition-duration: var(--duration-micro); } +@utility duration-base { --tw-duration: var(--duration-base); transition-duration: var(--duration-base); } +@utility duration-layout { --tw-duration: var(--duration-layout); transition-duration: var(--duration-layout); } +@utility duration-slow { --tw-duration: var(--duration-slow); transition-duration: var(--duration-slow); } +@utility duration-hero { --tw-duration: var(--duration-hero); transition-duration: var(--duration-hero); } +@utility duration-cinematic { --tw-duration: var(--duration-cinematic); transition-duration: var(--duration-cinematic); } + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + /* 角色型圆角:tight 给徽章 / 骨架屏这类小件(原来的裸 rounded),drawer 给手机端底部抽屉顶角, + slab 给门户大卡。控件 md、输入与小按钮 lg、卡片 xl、面板 2xl、抽屉与大卡 3xl、药丸 full, + 那些沿用 Tailwind 名,值随 --radius 一起缩放。 */ + --radius-tight: 0.25rem; + --radius-drawer: 28px; + --radius-slab: 2.5rem; + + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + /* ─── 主题 token(见 docs/features/design-tokens.md)─────────────────── + 组件只许用这些语义名,不许直接写 bone / ink / gold 原色、十六进制、rgba 或 + white/black 透明度。值在下方各壳的 token 块里定义,主题包只覆盖值。 */ + /* 文字层级(随日夜翻转) */ + --color-content-strong: var(--content-strong); + --color-content: var(--content); + --color-content-secondary: var(--content-secondary); + --color-content-tertiary: var(--content-tertiary); + --color-content-muted: var(--content-muted); + --color-content-faint: var(--content-faint); + /* 面(随日夜翻转):页面底、普通面、抬起的卡、更高一层 */ + --color-canvas: var(--canvas); + --color-surface: var(--surface); + --color-surface-raised: var(--surface-raised); + --color-surface-high: var(--surface-high); + /* 品牌色族(门户 = 墨水蓝族,流媒体 = 金,动漫版 = 品牌色) */ + --color-brand-soft: var(--brand-soft); + --color-brand-light: var(--brand-light); + --color-brand: var(--brand); + --color-brand-strong: var(--brand-strong); + --color-brand-deep: var(--brand-deep); + /* 透明"洗"层的底色:hover / 按压 / 分隔线等半透明层写成 `bg-wash/[0.07]`,透明度留在 + 调用处,底色随日夜翻转(暗底白洗,亮底黑洗)。 */ + --color-wash: var(--wash); + /* 压在视频 / 海报上的东西:不随日夜翻转,画面永远是暗的 */ + --color-on-media-strong: var(--on-media-strong); + --color-on-media: var(--on-media); + --color-on-media-secondary: var(--on-media-secondary); + --color-on-media-tertiary: var(--on-media-tertiary); + --color-on-media-muted: var(--on-media-muted); + --color-on-media-faint: var(--on-media-faint); + --color-on-media-wash: var(--on-media-wash); + /* 压暗画面的遮罩底色(黑),透明度留在调用处 */ + --color-scrim: var(--scrim); +} + +/* ─────────────────────────── Semantic tokens ─────────────────────────── */ + +:root { + --radius: 0.85rem; + + color-scheme: light; + + /* Core surfaces */ + --background: oklch(0.985 0.002 260); + --foreground: oklch(0.18 0.006 260); + --card: oklch(1 0 0 / 90%); + --card-solid: oklch(1 0 0); + --card-foreground: var(--foreground); + --popover: oklch(1 0 0); + --popover-foreground: var(--foreground); + + /* Actions and states */ + /* 无彩主色(2026-09-26 拍板):日间是墨,夜间是白,和落地页的白色胶囊按钮同一个语言; + 彩色只来自海报、剧照等内容,状态提示用下面的 success / warning / destructive */ + --primary: oklch(0.2 0.006 260); + --primary-foreground: oklch(0.99 0.002 260); + --secondary: oklch(0.95 0.003 260); + --secondary-foreground: oklch(0.26 0.006 260); + --muted: oklch(0.955 0.003 260); + --muted-foreground: oklch(0.47 0.008 260); + --accent: oklch(0.935 0.004 260); + --accent-foreground: oklch(0.2 0.006 260); + --destructive: oklch(0.54 0.2 25); + --destructive-foreground: oklch(0.99 0.005 25); + /* 状态色:只用于状态提示,饱和度压低 */ + --success: oklch(0.52 0.1 155); + --success-foreground: oklch(0.99 0.002 260); + --warning: oklch(0.6 0.12 70); + --warning-foreground: oklch(0.99 0.002 260); + + /* Controls */ + --border: oklch(0.88 0.003 260 / 85%); + --input: oklch(0.93 0.003 260); + --ring: var(--primary); + + /* App shell */ + --sidebar: oklch(0.975 0.002 260); + --sidebar-foreground: var(--foreground); + --sidebar-primary: var(--primary); + --sidebar-primary-foreground: var(--primary-foreground); + --sidebar-accent: oklch(0.93 0.003 260); + --sidebar-accent-foreground: var(--foreground); + --sidebar-border: var(--border); + --sidebar-ring: var(--primary); + + --shell-border: color-mix(in oklch, var(--border) 86%, transparent); + --shell-hover: color-mix(in oklch, var(--accent) 68%, transparent); + --shell-subtle: color-mix(in oklch, var(--muted) 72%, transparent); + + /* 主题 token:浅色门户。文字层级从 foreground 往淡推,面从 background 往上抬 */ + --content-strong: oklch(0.13 0.006 260); + --content: var(--foreground); + --content-secondary: oklch(0.32 0.006 260); + --content-tertiary: var(--muted-foreground); + --content-muted: oklch(0.58 0.006 260); + --content-faint: oklch(0.7 0.005 260); + --canvas: var(--background); + --surface: oklch(0.98 0.002 260); + --surface-raised: var(--card-solid); + --surface-high: oklch(0.955 0.003 260); + --brand-soft: oklch(0.93 0.003 260); + --brand-light: oklch(0.45 0.006 260); + --brand: var(--primary); + --brand-strong: oklch(0.1 0.004 260); + --brand-deep: oklch(0.05 0.002 260); + --wash: #000000; + /* on-media 不分日夜:与流媒体壳的暗色文字层级同值(bone 阶) */ + --on-media-strong: #fbfaf7; + --on-media: #f1eee7; + --on-media-secondary: #dcd5c8; + --on-media-tertiary: #bdb4a5; + --on-media-muted: #918879; + --on-media-faint: #6f675c; + --on-media-wash: #ffffff; + --scrim: #000000; + + /* ─── 材质参数(见 docs/features/interface-materials.md)──────────────────── + 一个材质 = 一份固定配方(底色 / 光泽 / 模糊 / 边 / 内高光 / 内暗线 / 外投影 / 辉光)+ 这里的参数。 + class 只引用参数;浅色 / 深色 / 流媒体壳的差别只改参数,不再各自重写声明。 + 值与参数化前逐字相同。`--pg-surface-alpha` 是唯一给组件用的公开旋钮。 */ + /* surface:贴边控制条 / header */ + --mat-surface-rgb: 12, 13, 17; + --mat-surface-blur: blur(24px) saturate(160%); + --mat-surface-hairline: rgba(255, 255, 255, 0.08); + --mat-surface-highlight: rgba(255, 255, 255, 0.07); + --mat-surface-solid: rgba(12, 13, 17, 0.92); + /* pill:药丸 HUD */ + --mat-pill-bg: rgba(16, 17, 23, 0.68); + --mat-pill-sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.04) 40%, rgba(255, 255, 255, 0) 70%); + --mat-pill-blur: blur(16px) saturate(160%); + --mat-pill-border: rgba(255, 255, 255, 0.16); + --mat-pill-highlight: rgba(255, 255, 255, 0.18); + --mat-pill-shade: rgba(0, 0, 0, 0.25); + --mat-pill-shadow: 0 2px 6px -2px rgba(0, 0, 0, 0.2), 0 12px 32px -14px rgba(0, 0, 0, 0.42); + --mat-pill-solid: rgba(16, 17, 23, 0.88); + /* slab:门户外层内容卡(浅色值;深色在 .dark 块) */ + --mat-slab-bg: linear-gradient(155deg, rgba(255, 255, 255, 0.74) 0%, rgba(255, 255, 255, 0.58) 40%, rgba(255, 255, 255, 0.52) 100%); + --mat-slab-border: rgba(255, 255, 255, 0.7); + --mat-slab-blur: blur(20px) saturate(160%); + --mat-slab-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85), 0 0 0 1px rgba(20, 22, 28, 0.04), 0 1px 2px rgba(20, 22, 28, 0.05), 0 12px 28px -18px rgba(20, 22, 28, 0.16); + --mat-slab-reflection: linear-gradient(105deg, transparent 0%, rgba(255, 255, 255, 0.14) 36%, rgba(255, 255, 255, 0.3) 44%, rgba(255, 255, 255, 0.07) 51%, transparent 62%); + --mat-spotlight: rgba(255, 255, 255, 0.6); + /* 卡片投影:日间轻,夜间沿用 @theme 里的深投影 */ + --shadow-card: 0 1px 2px rgba(20, 22, 28, 0.06), 0 14px 32px -18px rgba(20, 22, 28, 0.28); + --shadow-card-hover: 0 1px 2px rgba(20, 22, 28, 0.08), 0 20px 40px -20px rgba(20, 22, 28, 0.34); + --mat-leak-opacity: 0.3; + --mat-leak-blend: multiply; + /* chrome:门户侧栏 / 顶栏 */ + --mat-chrome-bg: rgba(255, 255, 255, 0.52); + --mat-chrome-blur: blur(24px) saturate(160%); + --mat-chrome-border: color-mix(in oklch, var(--primary) 11%, transparent); + --mat-chrome-shadow: 0 8px 24px -20px rgba(20, 22, 28, 0.25); + --mat-chrome-side-shadow: 8px 0 24px -20px rgba(20, 22, 28, 0.25); + --mat-chrome-solid: rgba(255, 255, 255, 0.95); + /* float:门户小型悬浮面板 */ + --mat-float-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.82) 100%); + --mat-float-blur: blur(28px) saturate(170%); + --mat-float-border: rgba(255, 255, 255, 0.75); + --mat-float-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9), 0 0 0 1px rgba(20, 22, 28, 0.05), 0 2px 6px -2px rgba(20, 22, 28, 0.08), 0 20px 40px -20px rgba(20, 22, 28, 0.2); + --mat-float-solid: rgb(251, 251, 252); + /* 胶囊按钮:主操作是有填充的薄玻璃,次操作只描边。日夜对称——夜间白色薄玻璃配白字, + 日间墨色薄玻璃配深字;不做实心黑 / 实心白 */ + --mat-pill-primary-bg: linear-gradient(180deg, rgba(20, 22, 28, 0.02), rgba(20, 22, 28, 0.05)); + --mat-pill-primary-bg-hover: linear-gradient(180deg, rgba(20, 22, 28, 0.04), rgba(20, 22, 28, 0.08)); + --mat-pill-primary-fg: var(--content-strong); + --mat-pill-primary-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7), 0 1px 2px rgba(20, 22, 28, 0.06); + /* 主按钮的棱镜环(方案 A):亮度、彩度、环与内光的强度 */ + --prism-l: 0.66; + --prism-c: 0.15; + --prism-ring: 0.9; + --prism-glow: 0.16; + /* 选中态(分页当前页、角标、选中的选项):静态半透明 + 稍深描边,不转动、不实心 */ + --mat-pill-selected-bg: rgba(20, 22, 28, 0.07); + --mat-pill-selected-border: rgba(20, 22, 28, 0.28); + --mat-pill-secondary-bg: rgba(20, 22, 28, 0.03); + --mat-pill-secondary-bg-hover: rgba(20, 22, 28, 0.07); + --mat-pill-secondary-border: rgba(20, 22, 28, 0.12); + /* 分段控件的滑块(侧栏当前项、SegmentedTabs):日间是托起的白片,夜间是薄白玻璃 */ + --mat-seg-thumb-bg: rgba(255, 255, 255, 0.8); + --mat-seg-thumb-border: rgba(20, 22, 28, 0.06); + --mat-seg-thumb-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.95), 0 3px 12px -3px rgba(20, 22, 28, 0.18); + /* notice:更新提示胶囊 */ + --mat-notice-fg: var(--foreground); + --mat-notice-bg: rgba(255, 255, 255, 0.78); + --mat-notice-blur: blur(24px) saturate(160%); + --mat-notice-border: rgba(255, 255, 255, 0.85); + --mat-notice-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.95), 0 0 0 1px color-mix(in oklch, var(--primary) 12%, transparent), 0 14px 36px -14px rgba(20, 22, 28, 0.35), 0 3px 10px -4px rgba(20, 22, 28, 0.12); + --mat-notice-sub-fg: var(--muted-foreground); + --mat-notice-icon-fg: var(--primary); + --mat-notice-icon-bg: color-mix(in oklch, var(--primary) 12%, transparent); + --mat-notice-ring: 0 0 0 0 color-mix(in oklch, var(--primary) 45%, transparent); + --mat-notice-cta-fg: var(--primary-foreground); + --mat-notice-cta-bg: var(--primary); + --mat-notice-cta-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 6px 16px -8px color-mix(in oklch, var(--primary) 70%, transparent); + --mat-notice-close-fg: var(--muted-foreground); + --mat-notice-close-hover-fg: var(--foreground); + --mat-notice-close-hover-bg: rgba(0, 0, 0, 0.06); + + /* Scrollbars */ + --scrollbar-thumb: color-mix( + in oklch, + var(--muted-foreground) 26%, + transparent + ); + --scrollbar-thumb-hover: color-mix( + in oklch, + var(--muted-foreground) 42%, + transparent + ); + --scrollbar-thumb-active: color-mix( + in oklch, + var(--muted-foreground) 58%, + transparent + ); +} + +.dark, +:root:has([data-force-dark]) { + color-scheme: dark; + + --background: oklch(0.15 0.004 265); + --foreground: oklch(0.96 0.003 260); + + --card: oklch(0.21 0.004 265 / 88%); + --card-solid: oklch(0.21 0.004 265); + --card-foreground: var(--foreground); + --popover: oklch(0.2 0.004 265); + --popover-foreground: var(--foreground); + + /* 深色档主色是白(见 :root 处注释) */ + --primary: oklch(0.97 0.002 260); + --primary-foreground: oklch(0.16 0.004 265); + + --secondary: oklch(0.27 0.004 265); + --secondary-foreground: var(--foreground); + + --muted: oklch(0.26 0.004 265); + --muted-foreground: oklch(0.7 0.004 260); + + --accent: oklch(0.29 0.004 265); + --accent-foreground: var(--foreground); + + --destructive: oklch(0.7 0.15 25); + --destructive-foreground: oklch(0.16 0.004 265); + --success: oklch(0.78 0.1 158); + --success-foreground: oklch(0.2 0.03 158); + --warning: oklch(0.82 0.1 78); + --warning-foreground: oklch(0.22 0.03 78); + --border: oklch(0.36 0.004 265 / 70%); + --input: oklch(0.3 0.004 265); + --ring: var(--primary); + + --sidebar: oklch(0.17 0.004 265); + --sidebar-foreground: var(--foreground); + --sidebar-primary: var(--primary); + --sidebar-primary-foreground: var(--primary-foreground); + --sidebar-accent: oklch(0.27 0.004 265); + --sidebar-accent-foreground: var(--foreground); + --sidebar-border: var(--border); + --sidebar-ring: var(--primary); + + --shell-border: rgba(255, 255, 255, 0.08); + --shell-hover: rgba(255, 255, 255, 0.08); + --shell-subtle: rgba(255, 255, 255, 0.05); + + /* 主题 token:深色门户 */ + --content-strong: oklch(0.99 0.002 260); + --content: var(--foreground); + --content-secondary: oklch(0.85 0.004 260); + --content-tertiary: var(--muted-foreground); + --content-muted: oklch(0.58 0.004 260); + --content-faint: oklch(0.46 0.004 260); + --canvas: var(--background); + --surface: oklch(0.18 0.004 265); + --surface-raised: var(--card-solid); + --surface-high: oklch(0.26 0.004 265); + --brand-soft: oklch(0.27 0.004 265); + --brand-light: oklch(1 0 0); + --brand: var(--primary); + --brand-strong: oklch(0.88 0.003 260); + --brand-deep: oklch(0.78 0.003 260); + --wash: #ffffff; + + /* 材质参数:深色门户 */ + --mat-slab-bg: linear-gradient(155deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 30%, rgba(255, 255, 255, 0.03) 75%, rgba(255, 255, 255, 0.04) 100%); + --mat-slab-border: rgba(255, 255, 255, 0.1); + --mat-slab-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), inset 0 -1px 0 rgba(255, 255, 255, 0.03), 0 1px 2px rgba(0, 0, 0, 0.2), 0 12px 28px -18px rgba(0, 0, 0, 0.4); + --mat-slab-reflection: linear-gradient(105deg, transparent 0%, rgba(255, 255, 255, 0.006) 36%, rgba(255, 255, 255, 0.013) 44%, rgba(255, 255, 255, 0.003) 51%, transparent 62%); + --mat-spotlight: rgba(255, 255, 255, 0.07); + --shadow-card: 0 18px 40px -24px rgba(0, 0, 0, 0.9); + --shadow-card-hover: 0 26px 54px -24px rgba(0, 0, 0, 1); + --mat-leak-opacity: 0.3; + --mat-leak-blend: normal; + --mat-chrome-bg: color-mix(in oklch, var(--card-solid) 48%, transparent); + --mat-chrome-border: color-mix(in oklch, var(--primary) 13%, transparent); + --mat-chrome-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); + --mat-chrome-side-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.04); + --mat-chrome-solid: rgba(12, 13, 17, 0.92); + --mat-float-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.035) 32%, rgba(255, 255, 255, 0) 62%), rgba(20, 21, 25, 0.9); + --mat-float-solid: rgb(22, 23, 27); + --mat-float-blur: blur(36px) saturate(180%); + --mat-float-border: rgba(255, 255, 255, 0.14); + /* 胶囊按钮:夜间主操作是白色玻璃 */ + --mat-pill-primary-bg: rgba(255, 255, 255, 0.05); + --mat-pill-primary-bg-hover: rgba(255, 255, 255, 0.09); + --mat-pill-primary-fg: #ffffff; + --mat-pill-primary-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 1px 2px rgba(0, 0, 0, 0.2); + --prism-l: 0.82; + --prism-c: 0.13; + --prism-ring: 0.85; + --prism-glow: 0.22; + --mat-pill-selected-bg: rgba(255, 255, 255, 0.1); + --mat-pill-selected-border: rgba(255, 255, 255, 0.32); + --mat-pill-secondary-bg: rgba(255, 255, 255, 0.04); + --mat-pill-secondary-bg-hover: rgba(255, 255, 255, 0.1); + --mat-pill-secondary-border: rgba(255, 255, 255, 0.15); + --mat-seg-thumb-bg: rgba(255, 255, 255, 0.08); + --mat-seg-thumb-border: rgba(255, 255, 255, 0.15); + --mat-seg-thumb-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 4px 18px -6px rgba(0, 0, 0, 0.4); + --mat-float-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16), inset 0 -1px 0 rgba(255, 255, 255, 0.03), 0 2px 6px -2px rgba(0, 0, 0, 0.22), 0 18px 40px -20px rgba(0, 0, 0, 0.4); + --mat-notice-bg: rgba(255, 255, 255, 0.08); + --mat-notice-border: rgba(255, 255, 255, 0.14); + --mat-notice-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16), inset 0 -1px 0 rgba(0, 0, 0, 0.25), 0 16px 40px -16px rgba(0, 0, 0, 0.55); + --mat-notice-close-hover-bg: rgba(255, 255, 255, 0.08); +} + +/* ───────────────────────────── Base styles ───────────────────────────── */ + +@layer base { + * { + @apply border-border outline-ring/50; + } + + html, + body { + min-height: 100%; + } + + /* Light app shell */ + body { + @apply bg-background text-foreground antialiased; + background-image: linear-gradient(180deg, oklch(1 0 0), oklch(0.97 0.002 260)); + } + + /* Dark app shell, including fixed-dark public and stream routes */ + .dark body, + :root:has([data-force-dark]) body { + background-image: linear-gradient(180deg, oklch(0.16 0.004 265), oklch(0.12 0.004 265)); + } + + /* 流媒体壳:html / body 整块铺成壳的 canvas,不用上面门户的渐变底。 + 桌面上永远看不到 body,但手机上地址栏收起、橡皮筋回弹、内容不够一屏、崩溃页这些 + "壳没铺满"的时刻会把 body 露出来。夜间 html 也一并铺:iOS 回弹露出来的是 html 不是 body。 */ + :root.dark:has([data-stream-shell]), + :root:has([data-stream-shell="play"]) { + background: var(--canvas); + } + + :root.dark:has([data-stream-shell]) body, + :root:has([data-stream-shell="play"]) body, + :root:not(.dark):has([data-stream-shell="page"]) body { + background-image: none; + background-color: var(--canvas); + } + + + /* 触屏点按时不要系统自带的半透明高亮(Android 是一层蓝灰),按压反馈由 + 卡片 / 按钮自己的 active:scale 给。该属性可继承,挂在壳上即可。 */ + [data-stream-shell] { + -webkit-tap-highlight-color: transparent; + } + + /* 流媒体手机端有 3.5rem 的底部 Tab Bar:全局右下角浮层(更新提示)按这个 + 变量再垫高,免得压在 Tab Bar 上。播放页不渲染 Tab Bar,不设。 */ + @media (max-width: 767px) { + :root:has([data-stream-shell="page"]) { + --uhd-bottom-dock: 3.5rem; + } + } + + a { + @apply transition-colors; + } +} + +/* ────────────────────────────── Utilities ────────────────────────────── */ + +@layer utilities { + /* Typography */ + .font-display { + font-family: var(--font-display); + letter-spacing: 0.01em; + } + + /* Scrolling */ + .no-scrollbar { + scrollbar-width: none; + } + + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + :root:has(.app-scrollbars), + :root:has(.app-scrollbars) *:not(.no-scrollbar):not(.scrollbar-none) { + scrollbar-color: var(--scrollbar-thumb) transparent; + scrollbar-width: thin; + } + + :root:has(.app-scrollbars) + *:not(.no-scrollbar):not(.scrollbar-none):hover { + scrollbar-color: var(--scrollbar-thumb-hover) transparent; + } + + :root:has(.app-scrollbars)::-webkit-scrollbar, + :root:has(.app-scrollbars) + *:not(.no-scrollbar):not(.scrollbar-none)::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + :root:has(.app-scrollbars)::-webkit-scrollbar-track, + :root:has(.app-scrollbars) + *:not(.no-scrollbar):not(.scrollbar-none)::-webkit-scrollbar-track { + background: transparent; + } + + :root:has(.app-scrollbars)::-webkit-scrollbar-thumb, + :root:has(.app-scrollbars) + *:not(.no-scrollbar):not(.scrollbar-none)::-webkit-scrollbar-thumb { + min-height: 36px; + border: 1px solid transparent; + border-radius: 999px; + background-color: var(--scrollbar-thumb); + background-clip: padding-box; + } + + :root:has(.app-scrollbars) + *:not(.no-scrollbar):not(.scrollbar-none):hover::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb-hover); + } + + :root:has(.app-scrollbars)::-webkit-scrollbar-thumb:hover, + :root:has(.app-scrollbars) + *:not(.no-scrollbar):not(.scrollbar-none)::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-thumb-active); + } + + /* Player controls */ + .volume-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + border-radius: 999px; + outline: none; + cursor: pointer; + background: linear-gradient( + to right, + rgba(255, 255, 255, 0.9) 0%, + rgba(255, 255, 255, 0.9) var(--val, 100%), + rgba(255, 255, 255, 0.12) var(--val, 100%), + rgba(255, 255, 255, 0.12) 100% + ); + } + + .volume-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 13px; + height: 13px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45); + cursor: pointer; + border: 0; + } + + .volume-slider::-moz-range-thumb { + width: 13px; + height: 13px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45); + cursor: pointer; + border: 0; + } + + /* App shell backgrounds */ + /* 门户 shell 背景光斑(user/unsubscribe 共用)。2026-09-26 主色改为无彩后,光斑也去掉色相, + 只留明暗层次:日间顶部一层柔和的白与一抹极淡的灰,夜间顶部一抹微光。 + 玻璃卡背后要有明暗变化才读得出是玻璃,不能是一整块平涂。 */ + /* 门户页背景:无彩的明暗光斑铺满整页;棱镜漏光是单独一层(.prism-leak)。 + 卡片是磨砂面,背景只需给它柔和的明暗与淡彩,不需要纹理 */ + .bg-page-glow { + background: + radial-gradient(ellipse 60% 50% at 12% 4%, rgba(20, 22, 28, 0.09) 0%, transparent 70%), + radial-gradient(ellipse 45% 40% at 88% 18%, rgba(255, 255, 255, 0.9) 0%, transparent 70%), + radial-gradient(ellipse 50% 45% at 70% 62%, rgba(20, 22, 28, 0.07) 0%, transparent 70%), + radial-gradient(ellipse 45% 40% at 20% 88%, rgba(20, 22, 28, 0.06) 0%, transparent 70%), + linear-gradient(180deg, rgba(255, 255, 255, 0.42), rgba(246, 246, 248, 0.72)); + } + + /* 棱镜漏光(全站,落地页与播放页除外):左上角斜进来一束已分好色的光,大幅虚化、只占 + 一角,与落地页的棱镜叙事呼应;卡片压在上面时透出淡彩。日间用 multiply 叠在浅底上, + 避免发灰;浓度见 --mat-leak-opacity。由 渲染,固定在视口上 */ + .prism-leak { + position: fixed; + inset: -10%; + pointer-events: none; + background: linear-gradient( + 118deg, + transparent 6%, + oklch(0.75 0.15 25) 13%, + oklch(0.8 0.14 65) 17%, + oklch(0.87 0.14 100) 21%, + oklch(0.8 0.14 150) 25%, + oklch(0.78 0.12 205) 29%, + oklch(0.68 0.15 265) 33%, + oklch(0.62 0.16 305) 37%, + transparent 44% + ); + filter: blur(48px); + opacity: var(--mat-leak-opacity); + mix-blend-mode: var(--mat-leak-blend); + -webkit-mask-image: radial-gradient(ellipse 75% 70% at 8% 0%, #000 0%, rgba(0, 0, 0, 0.5) 45%, transparent 78%); + mask-image: radial-gradient(ellipse 75% 70% at 8% 0%, #000 0%, rgba(0, 0, 0, 0.5) 45%, transparent 78%); + } + + .dark .bg-page-glow, + [data-force-dark] .bg-page-glow { + background: + radial-gradient(ellipse 60% 50% at 12% 4%, rgba(255, 255, 255, 0.075) 0%, transparent 70%), + radial-gradient(ellipse 45% 40% at 88% 22%, rgba(255, 255, 255, 0.05) 0%, transparent 70%), + radial-gradient(ellipse 50% 45% at 68% 64%, rgba(255, 255, 255, 0.055) 0%, transparent 70%), + radial-gradient(ellipse 45% 40% at 18% 90%, rgba(255, 255, 255, 0.04) 0%, transparent 70%), + linear-gradient(180deg, rgba(16, 17, 20, 0.76), rgba(17, 18, 21, 0.86)); + } +} + +/* ───────────────────────────── Components ───────────────────────────── */ + +@layer components { + /* Player subtitles */ + [data-subtitles-hidden] .ASS-box { + display: none !important; + } + + .ASS-box { + transition: transform 200ms ease; + } + + /* 抬多少由 Player 按字幕层与控件常驻面的实际重叠量写进 --uhd-sub-lift(负值 px); + 量不到时退回固定 5.5rem(约等于控件常驻面高度)。 */ + [data-controls-visible] .ASS-box { + transform: translate3d(0, var(--uhd-sub-lift, -5.5rem), 0); + } + + .ASS-box .ASS-dialogue [data-text] { + paint-order: stroke fill; + -webkit-text-stroke: 1.5px rgba(0, 0, 0, 0.95); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); + } + + /* 字幕字号档位:Player 在根节点上写 --uhd-sub-scale(见 subtitleScale.ts), + 在 ass.js 自己算出的字号上再乘一个倍率。ass.js 的样式表是不分层的 + (#ASS-global-style),这里在 @layer 里按普通优先级会输给它,所以三条都加 + !important —— 分层内的 !important 高于未分层的普通声明。只动字号 / 行高 / + 字距,定位与描边仍由 ass.js 的 --ass-scale 控制。 */ + .ASS-box .ASS-dialogue [data-text] { + font-size: calc(var(--ass-scale) * var(--ass-real-fs) * var(--uhd-sub-scale, 1) * 1px) !important; + line-height: calc(var(--ass-scale) * var(--ass-tag-fs) * var(--uhd-sub-scale, 1) * 1px) !important; + letter-spacing: calc(var(--ass-scale) * var(--ass-tag-fsp) * var(--uhd-sub-scale, 1) * 1px) !important; + } + + /* ─── 玻璃三档 tokens(全站 dark chrome 统一配方)────────────────── + 配方来源是 btn-glass 的语言 + Glass UI 的思路:① 底色不透明度压到 + 0.55~0.70,blur 真正透出后面的内容才有玻璃感(旧 .glass/.glass-panel + 的 0.86+ 基本是实心板,已删除并全量迁移);② inset 顶部高光,玻璃 + 上沿"接光"。边框与投影一并收进 class,组件侧不再各写各的。 + 使用范围:播放器全部浮层 + 媒体区 header/筛选栏/弹层/卡片徽章。 */ + /* 两族分治(2026-08-01 实测校准): + - 贴边 chrome(header/筛选栏/播放器底栏)= pg-surface,融入页面: + 只有 blur + 底色,不加泛光和接光线 —— 贴屏幕边缘的面没有"上方 + 光源",加亮线会把它从页面上切下来,恰好破坏融为一体的感觉。 + - 悬浮物(弹层/对话框)= pg-float,画面中央的药丸 HUD = pg-pill,材质发光: + 四周有留白,光学上浮在页面上方,泛光层 + 双内高光 + 亮边框成立。 + 暗底上"更透"读不出来,能读出来的是这些材质要素;底色反而要实, + 内容才好读。 */ + .pg-surface { + /* 常驻贴边大面:播放器底部控制条(0.6)。站点 header/筛选栏压在 + 高亮海报上,文字对比要求更高,用 [--pg-surface-alpha:0.7] 收紧 */ + background: rgba(var(--mat-surface-rgb), var(--pg-surface-alpha, 0.6)); + backdrop-filter: var(--mat-surface-blur); + -webkit-backdrop-filter: var(--mat-surface-blur); + border-color: var(--mat-surface-hairline); + box-shadow: inset 0 1px 0 var(--mat-surface-highlight); + } + + .pg-pill { + /* 画面中央短暂浮层:seek 进度环、播放/暂停 flash、toast、手势 HUD */ + background: + var(--mat-pill-sheen), + var(--mat-pill-bg); + backdrop-filter: var(--mat-pill-blur); + -webkit-backdrop-filter: var(--mat-pill-blur); + border: 1px solid var(--mat-pill-border); + box-shadow: + inset 0 1px 0 var(--mat-pill-highlight), + inset 0 -1px 0 var(--mat-pill-shade), + var(--mat-pill-shadow); + } + + + + /* ─── 更新提示胶囊(app/components/update-notice.tsx)────────────── + 三处材质各归各:浅色门户 = 白磨砂玻璃 + 蓝调投影(pg-slab 浅色配方的 + 简化版);深色门户 = 白透玻璃(侧栏药丸同款);流媒体壳 = 同深色配方, + 动作钮换金色 —— 那边的强调色是金不是蓝。 */ + .update-pill { + color: var(--mat-notice-fg); + background: var(--mat-notice-bg); + backdrop-filter: var(--mat-notice-blur); + -webkit-backdrop-filter: var(--mat-notice-blur); + border: 1px solid var(--mat-notice-border); + box-shadow: var(--mat-notice-shadow); + } + + .update-pill__sub { + color: var(--mat-notice-sub-fg); + } + + .update-pill__icon { + color: var(--mat-notice-icon-fg); + background: var(--mat-notice-icon-bg); + } + + .update-pill__icon--stale { + color: var(--warning); + background: color-mix(in oklch, var(--warning) 14%, transparent); + } + + /* 一圈慢呼吸的光环,提醒但不催 */ + .update-pill__ring { + box-shadow: var(--mat-notice-ring); + animation: update-pill-ring 2.4s ease-out infinite; + } + + .update-pill__cta { + color: var(--mat-notice-cta-fg); + background: var(--mat-notice-cta-bg); + box-shadow: var(--mat-notice-cta-shadow); + } + + .update-pill__close { + color: var(--mat-notice-close-fg); + } + + .update-pill__close:hover { + color: var(--mat-notice-close-hover-fg); + background: var(--mat-notice-close-hover-bg); + } + + + @media (prefers-reduced-motion: reduce) { + .update-pill__ring { + animation: none; + } + } + + /* 第四档:内容卡片,双主题(门户与 /speed 共用)。薄白/薄亮底 + 亮棱边 + + 顶部内高光 + 斜反光,背景模糊由 --mat-slab-blur 给;/speed 的卡片贴着 + 高频重绘的实时区,在 .spd-bg 里把它关掉,零合成开销优先。 */ + .pg-slab { + position: relative; + /* 建立层叠上下文,把 ::before 的 z-index:-1 关在自己内部 */ + isolation: isolate; + /* 磨砂面,不追求透明:内容卡以好读为先,背景模糊只让漏光与明暗柔和地渗进来。 + 不做折射——背后没有细节可弯折,Chromium 专属的 SVG 折射也不值得 */ + background: var(--mat-slab-bg); + backdrop-filter: var(--mat-slab-blur); + -webkit-backdrop-filter: var(--mat-slab-blur); + /* 立体感靠亮棱边与顶部内高光,不靠投影:投影只有贴边一层 + 很淡的环境一层, + 越透的东西影子越淡,重投影会让玻璃读成实心板 */ + border: 1px solid var(--mat-slab-border); + box-shadow: var(--mat-slab-shadow); + } + + .pg-slab::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + pointer-events: none; + /* 斜掠反光,收窄且不对称 —— 对称高光读成"渐变背景"而不是"反射"。 + 浅色下卡片本身大面积偏白,反光峰值可以略高;作用仍是"察觉有一层 + 膜",能被明确看成光带就过了 */ + background: var(--mat-slab-reflection); + } + + + /* 贴边 chrome 的双主题版(/speed 页头)。浅色:白磨砂 + 蓝调底缘投影; + 深色:等同 pg-surface [--pg-surface-alpha:0.7] 的配方。只设边框颜色, + 宽度由 border-b utility 给。 */ + /* 2026-08-02 统一校准:① 暗色底从硬编码墨黑(rgba 12,13,17,流媒体 + 继承)改为 token 混色 —— 门户暗色是 271° 蓝紫调,chrome 必须同色相 + 否则读成另一种材质;② 边框掺 primary,与卡片"蓝棱线"同语言; + ③ 竖直侧栏用 .pg-chrome-side 把投影转向右侧(向下投影是横栏的光学)。 */ + .pg-chrome { + background: var(--mat-chrome-bg); + backdrop-filter: var(--mat-chrome-blur); + -webkit-backdrop-filter: var(--mat-chrome-blur); + border-color: var(--mat-chrome-border); + box-shadow: var(--mat-chrome-shadow); + } + + .pg-chrome-side { + box-shadow: var(--mat-chrome-side-shadow); + } + + + /* 全站唯一的悬浮层材质(2026-09-26 起):下拉菜单、弹出框、下拉选择、对话框、抽屉, + 门户与流媒体、播放器共用;日夜跟随,播放页恒为夜间。圆角分两档:菜单类 rounded-2xl, + 对话框 rounded-3xl。悬浮物打开时是静态的,可以放心用 backdrop-filter。 */ + .pg-float { + background: var(--mat-float-bg); + backdrop-filter: var(--mat-float-blur); + -webkit-backdrop-filter: var(--mat-float-blur); + border: 1px solid var(--mat-float-border); + box-shadow: var(--mat-float-shadow); + } + + /* 嵌在另一层磨砂里的菜单(页头、筛选栏、卡片内的下拉):外层磨砂是 backdrop 根, + 里面的菜单糊不到页面,半透明底会透出没糊过的内容 —— 这类一律用近实底(动漫站同一做法)。 + 挂在 body 上的对话框、shadcn 弹出框不需要 */ + .pg-float-solid { + --mat-float-bg: var(--mat-float-solid); + } + + + /* /speed 页面背景:玻璃要"有东西可透"才成立,这几团光斑就是卡片背后的内容。 + 纯静态渐变、零重绘成本。2026-09-26 主色改为无彩后光斑也去掉色相,只留明暗: + 日间是几团极淡的墨,夜间是几团微光。强度照原来校准过的档位,玻璃仍读得出来。 */ + .spd-bg { + /* 测速页的卡片贴着高频重绘的实时区,不开背景模糊 */ + --mat-slab-blur: none; + background-color: var(--background); + background-image: + radial-gradient( + 120% 80% at 12% -8%, + rgba(20, 22, 28, 0.08) 0%, + transparent 55% + ), + radial-gradient( + 90% 65% at 96% -6%, + rgba(20, 22, 28, 0.06) 0%, + transparent 52% + ), + radial-gradient( + 56% 44% at 38% 52%, + rgba(20, 22, 28, 0.04) 0%, + transparent 62% + ), + radial-gradient( + 80% 60% at 50% 112%, + rgba(20, 22, 28, 0.05) 0%, + transparent 60% + ); + } + + .dark .spd-bg, + [data-force-dark] .spd-bg { + background-image: + radial-gradient( + 120% 80% at 15% -10%, + rgba(255, 255, 255, 0.06) 0%, + transparent 55% + ), + radial-gradient( + 100% 70% at 95% 0%, + rgba(255, 255, 255, 0.04) 0%, + transparent 50% + ); + } + + + /* blur 不可用(老内核/省电模式)时透明底会让文字压在画面上没法读, + 退回近实心 */ + @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .pg-surface { + background: var(--mat-surface-solid); + } + + .pg-pill { + background: var(--mat-pill-solid); + } + + .pg-chrome { + background: var(--mat-chrome-solid); + } + + .pg-float { + background: var(--mat-float-solid); + } + + + .dark .pg-float, + [data-force-dark] .pg-float { + background: rgba(20, 21, 25, 0.96); + } + } + + /* 吸顶筛选条与顶栏之间的接缝:一条无彩细线,两端淡出,随日夜取 --wash */ + .chrome-seam-top { + position: relative; + } + + .chrome-seam-top::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 1px; + background: linear-gradient( + 90deg, + transparent 0%, + color-mix(in oklch, var(--wash) 8%, transparent) 12%, + color-mix(in oklch, var(--wash) 12%, transparent) 50%, + color-mix(in oklch, var(--wash) 8%, transparent) 88%, + transparent 100% + ); + pointer-events: none; + } + + /* Stream library layout */ + /* 手机三列:两列时单张海报 190px,信息密度只有主流客户端的一半;三列 + 约 120px,一屏能看 9 张,标题字号在 MovieCard 里随之收到 15px */ + .lib-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + column-gap: 0.5rem; + row-gap: 1.25rem; + } + + @media (min-width: 480px) { + .lib-grid { + column-gap: 0.75rem; + row-gap: 2rem; + } + } + + @media (min-width: 768px) { + .lib-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + column-gap: 1.25rem; + row-gap: 2.5rem; + } + } + + @media (min-width: 1024px) { + .lib-grid { + /* 定宽 220 + 左对齐会在平板/宽屏右侧留大片空白(横屏 iPad 只排 4 列、 + 右边空 ~200px)。改成弹性列:每列 200px 起、用 1fr 拉伸铺满整行, + 列数随宽度自适应,不再有右侧留白。 */ + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + } + } + + /* 玻璃族的动效成员:沿元素边框匀速流动的一段高亮弧。用法:宿主已有 + 圆角与边框,挂 .pg-beam 即可;弧色取 var(--primary) 自动双主题。 + 克制档:1.5px 细、弧长约 15% 周长、5.5s 一圈;动画的是注册过的 + 自定义属性(角度),仅小面积重绘。reduced-motion 时静止为常亮弧。 */ + .pg-beam { + position: relative; + } + + .pg-beam::after { + content: ""; + position: absolute; + inset: -2px; + padding: 1.5px; + border-radius: inherit; + pointer-events: none; + /* 弧的亮度与巡边周期可在使用处经 CSS 变量收弱(常驻元素建议弱档, + 一次性 CTA 用默认档) */ + background: conic-gradient( + from var(--pg-beam-angle), + transparent 0%, + transparent 74%, + color-mix(in oklch, var(--primary) var(--pg-beam-strength, 88%), transparent) + 87%, + transparent 100% + ); + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask-composite: exclude; + animation: pg-beam-spin var(--pg-beam-duration, 5.5s) linear infinite; + } + + @media (prefers-reduced-motion: reduce) { + .pg-beam::after { + animation: none; + } + } + + /* Segmented tabs */ + .tab-slider { + transition: + transform 380ms var(--ease-spring-soft), + width 380ms var(--ease-spring-soft), + top 380ms var(--ease-spring-soft), + height 380ms var(--ease-spring-soft); + will-change: transform, width, top, height; + } + + /* 分段控件滑块的材质,位置与尺寸由组件量测后写进 style */ + .seg-thumb { + background: var(--mat-seg-thumb-bg); + border: 1px solid var(--mat-seg-thumb-border); + box-shadow: var(--mat-seg-thumb-shadow); + backdrop-filter: blur(12px); + } + + .tab-color { + transition: color 320ms var(--ease-standard); + } + + /* Glass buttons */ + /* 门户胶囊按钮(2026-09-26):主操作 pill-primary 是带棱镜环的半透明胶囊,次操作 pill-secondary 是描边胶囊。 + 只管底、边、字色与投影,形状尺寸(rounded-full、内边距、字号)由调用处给。 + 日夜差别全在 --mat-pill-* 参数里;以后要换回实底或调透明度只改参数。 */ + .pill-secondary { + background: var(--mat-pill-secondary-bg); + border: 1px solid var(--mat-pill-secondary-border); + color: var(--content); + -webkit-backdrop-filter: blur(20px) saturate(160%); + backdrop-filter: blur(20px) saturate(160%); + transition: + background-color var(--duration-base) var(--ease-standard), + transform 220ms var(--ease-spring); + } + + .pill-secondary:hover { + background: var(--mat-pill-secondary-bg-hover); + } + + /* 主操作(方案 A,2026-09-26):和其他按钮一样是半透明胶囊,不做实心;靠沿边框缓慢流转的 + 一整圈棱镜光谱突出,内侧再透一层很淡的同色光。门户 .pill-primary 与流媒体 + .btn-glass-primary 共用;减少动态效果时停转,只留彩色边框 */ + .pill-primary, + .btn-glass.btn-glass-primary { + position: relative; + isolation: isolate; + overflow: visible; + background: var(--mat-pill-primary-bg); + border: 1px solid transparent; + color: var(--mat-pill-primary-fg); + box-shadow: var(--mat-pill-primary-shadow); + -webkit-backdrop-filter: blur(20px) saturate(160%); + backdrop-filter: blur(20px) saturate(160%); + transition: + background-color var(--duration-base) var(--ease-standard), + transform 220ms var(--ease-spring); + } + + .pill-primary:hover, + .btn-glass.btn-glass-primary:hover { + background: var(--mat-pill-primary-bg-hover); + } + + .pill-primary::before, + .btn-glass.btn-glass-primary::before, + .prism-ring::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1.5px; + pointer-events: none; + background: conic-gradient( + from var(--prism-angle), + oklch(var(--prism-l) var(--prism-c) 25), + oklch(var(--prism-l) var(--prism-c) 70), + oklch(calc(var(--prism-l) + 0.06) var(--prism-c) 105), + oklch(var(--prism-l) var(--prism-c) 155), + oklch(var(--prism-l) var(--prism-c) 205), + oklch(calc(var(--prism-l) - 0.04) var(--prism-c) 265), + oklch(calc(var(--prism-l) - 0.04) var(--prism-c) 305), + oklch(var(--prism-l) var(--prism-c) 25) + ); + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask-composite: exclude; + opacity: var(--prism-ring); + transition: opacity var(--duration-base) var(--ease-standard); + animation: prism-spin 6s linear infinite; + } + + .pill-primary::after, + .btn-glass.btn-glass-primary::after, + .prism-ring::after { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + pointer-events: none; + background: conic-gradient( + from var(--prism-angle), + oklch(0.8 0.14 25), + oklch(0.85 0.13 105), + oklch(0.8 0.13 205), + oklch(0.72 0.15 305), + oklch(0.8 0.14 25) + ); + filter: blur(10px); + opacity: var(--prism-glow); + transition: opacity var(--duration-base) var(--ease-standard); + animation: prism-spin 6s linear infinite; + } + + .pill-primary:hover::before, + .btn-glass.btn-glass-primary:hover::before, + .group:hover .prism-ring::before, + .prism-ring:hover::before { + opacity: 1; + } + + .pill-primary:hover::after, + .btn-glass.btn-glass-primary:hover::after, + .group:hover .prism-ring::after, + .prism-ring:hover::after { + opacity: calc(var(--prism-glow) * 1.6); + } + + .pill-primary:disabled::before, + .pill-primary:disabled::after, + .btn-glass.btn-glass-primary:disabled::before, + .btn-glass.btn-glass-primary:disabled::after { + animation: none; + } + + @media (prefers-reduced-motion: reduce) { + .pill-primary::before, + .pill-primary::after, + .btn-glass.btn-glass-primary::before, + .btn-glass.btn-glass-primary::after, + .prism-ring::before, + .prism-ring::after { + animation: none; + } + } + + /* 只要棱镜彩环与内侧淡光、不要胶囊底的宿主(如侧栏"前往流媒体"的播放键):彩环画在 + 宿主外沿,宿主本身不能 overflow:hidden;放在 .group 里时悬停整组也会点亮 */ + .prism-ring { + position: relative; + isolation: isolate; + } + + /* 侧栏"前往流媒体":点下去从播放键往外扩一圈七彩光再跳转(动漫站同一做法,光换成棱镜色) */ + .stream-entry__burst { + opacity: 0; + transform: scale(1); + background: conic-gradient( + oklch(0.8 0.14 25), + oklch(0.85 0.13 105), + oklch(0.8 0.13 205), + oklch(0.72 0.15 305), + oklch(0.8 0.14 25) + ); + } + + .stream-entry--leaving .stream-entry__play { + transform: scale(0.92); + } + + .stream-entry--leaving .stream-entry__burst { + animation: stream-entry-burst 360ms var(--ease-standard) both; + } + + /* 流光图标:图标形状做遮罩,里面填和主按钮同一套的旋转光谱(如账户菜单里"流媒体"的三角)。 + 形状由 --prism-icon 给(SVG data URL),尺寸由调用处给 */ + .prism-icon { + display: block; + flex-shrink: 0; + background: conic-gradient( + from var(--prism-angle), + oklch(var(--prism-l) var(--prism-c) 25), + oklch(var(--prism-l) var(--prism-c) 70), + oklch(calc(var(--prism-l) + 0.06) var(--prism-c) 105), + oklch(var(--prism-l) var(--prism-c) 155), + oklch(var(--prism-l) var(--prism-c) 205), + oklch(calc(var(--prism-l) - 0.04) var(--prism-c) 265), + oklch(calc(var(--prism-l) - 0.04) var(--prism-c) 305), + oklch(var(--prism-l) var(--prism-c) 25) + ); + -webkit-mask: var(--prism-icon) center / contain no-repeat; + mask: var(--prism-icon) center / contain no-repeat; + animation: prism-spin 6s linear infinite; + } + + @media (prefers-reduced-motion: reduce) { + .prism-icon { + animation: none; + } + } + + /* 选中态:分页当前页、角标、选中的选项等"不是按钮但要标出来"的地方。静态、不实心 */ + .pill-selected { + background: var(--mat-pill-selected-bg); + border: 1px solid var(--mat-pill-selected-border); + color: var(--content-strong); + } + + /* 按下回弹,与 .btn-glass 同一手感 */ + .pill-primary:active, + .pill-secondary:active { + transform: scale(0.97); + } + + /* 卡片聚光:鼠标所在处的卡面微微提亮(借鉴 Aceternity Card Spotlight)。 + 宿主挂 .pg-spotlight 并用 rippleMove 写入 --mx/--my;只在能悬停的设备上出现。 + 宿主的 ::after 归它用,亮度见 --mat-spotlight */ + .pg-spotlight { + isolation: isolate; + } + + /* z-index:-1 + isolate:光铺在卡面底色之上、文字之下,不洗淡内容 */ + .pg-spotlight::after { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + pointer-events: none; + background: radial-gradient(320px circle at var(--mx, 50%) var(--my, 50%), var(--mat-spotlight), transparent 70%); + opacity: 0; + transition: opacity var(--duration-layout) var(--ease-standard); + } + + @media (hover: hover) { + .pg-spotlight:hover::after { + opacity: 1; + } + } + + /* 流媒体按钮(2026-09-26 起与门户胶囊同一外观):.btn-glass = 次操作(描边胶囊), + .btn-glass-primary = 主操作(棱镜胶囊),.btn-glass-icon = 圆形图标钮。只保留各自的 + 尺寸与排版,底、边、字色、投影全部引用 --mat-pill-*,改外观只改那组参数 */ + .btn-glass { + @apply relative isolate inline-flex items-center gap-2 rounded-full px-6 py-2.5 text-[15px] font-medium tracking-tight cursor-pointer select-none overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-content-strong/40; + } + + .btn-glass, + .btn-glass-icon { + background: var(--mat-pill-secondary-bg); + border: 1px solid var(--mat-pill-secondary-border); + color: var(--content-strong); + -webkit-backdrop-filter: blur(20px) saturate(160%); + backdrop-filter: blur(20px) saturate(160%); + transition: + background-color var(--duration-base) var(--ease-standard), + transform 220ms var(--ease-spring); + } + + .btn-glass:hover, + .btn-glass-icon:hover { + background: var(--mat-pill-secondary-bg-hover); + } + + .btn-glass:active, + .btn-glass-icon:active { + transform: scale(0.97); + } + + .btn-glass-icon { + @apply relative isolate grid place-items-center rounded-full w-11 h-11 cursor-pointer overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-content-strong/40; + } + + /* Stream image overlays */ + /* 大图压暗层,取页面底色:夜间压暗、日间提亮(文字随主题深 / 浅) */ + .hero-vignette { + background: + radial-gradient( + circle at 70% 42%, + transparent 0%, + color-mix(in oklch, var(--canvas) 8%, transparent) 36%, + color-mix(in oklch, var(--canvas) 92%, transparent) 100% + ), + linear-gradient( + 90deg, + color-mix(in oklch, var(--canvas) 92%, transparent) 0%, + color-mix(in oklch, var(--canvas) 56%, transparent) 32%, + color-mix(in oklch, var(--canvas) 10%, transparent) 66%, + color-mix(in oklch, var(--canvas) 46%, transparent) 100% + ), + linear-gradient( + 180deg, + color-mix(in oklch, var(--canvas) 18%, transparent) 0%, + transparent 36%, + color-mix(in oklch, var(--canvas) 96%, transparent) 100% + ); + } + + /* 横向滚动行两侧的渐隐,取页面底色(日间原来是一道黑条) */ + .row-fade-left { + background: linear-gradient(90deg, var(--canvas) 0%, transparent 100%); + } + + .row-fade-right { + background: linear-gradient(270deg, var(--canvas) 0%, transparent 100%); + } +} + +/* ───────────────────────────── Animations ────────────────────────────── */ + +/* .pg-beam 的旋转角:注册为 才能被动画插值(普通自定义属性 + 只能离散跳变);@property 不允许出现在 @layer 内,故放此处 */ +@property --pg-beam-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + +/* 主按钮棱镜环的旋转角,同理需注册 */ +@property --prism-angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + +@keyframes prism-spin { + to { + --prism-angle: 360deg; + } +} + +@keyframes stream-entry-burst { + from { + opacity: 0.6; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(2.6); + } +} + +@keyframes pg-beam-spin { + to { + --pg-beam-angle: 360deg; + } +} + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(8px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 播放器弹出层:比 fade-in 快得多(200ms)并带轻微 scale,从触发按钮方向 + "弹"出来(配 origin-bottom-right)。菜单是高频操作,520ms 的入场太拖 */ +@keyframes ticker-in { + from { + opacity: 0; + transform: translateY(100%); + } + + to { + opacity: 1; + transform: none; + } +} + +@keyframes ticker-out { + from { + opacity: 1; + transform: none; + } + + to { + opacity: 0; + transform: translateY(-100%); + } +} + +@keyframes pg-pop { + from { + opacity: 0; + transform: translateY(6px) scale(0.97); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +/* 页面入场:从轻微失焦到清晰,只播一次(借鉴 Arc 的 blur-in;调用处用 + motion-safe: 前缀,减少动态效果时直接显示)。填充模式用 backwards 而不是 both: + 播完不能留下 filter/transform,否则页面里 position:fixed 的元素会改以这层为参照 */ +@keyframes blur-in { + from { + opacity: 0; + filter: blur(6px); + transform: translateY(8px); + } + + to { + opacity: 1; + filter: blur(0); + transform: translateY(0); + } +} + +@keyframes hero-fade { + from { + opacity: 0; + transform: translateY(16px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes ken-burns { + from { + transform: translateY(var(--parallax, 0)) scale(1.02); + } + + to { + transform: translateY(var(--parallax, 0)) scale(1.08); + } +} + +@keyframes shiny-text { + 0%, + 90%, + 100% { + background-position: calc(-100% - var(--shiny-width)) 0; + } + 30%, + 60% { + background-position: calc(100% + var(--shiny-width)) 0; + } +} + +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(24px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes player-flash { + 0% { + opacity: 0; + transform: scale(0.55); + } + + 12% { + opacity: 1; + transform: scale(1.06); + } + + 18%, + 75% { + opacity: 1; + transform: scale(1); + } + + 100% { + opacity: 0; + transform: scale(1.15); + } +} + +.animate-shiny-text { + animation: shiny-text 8s infinite; +} + +.animate-player-flash { + animation: player-flash var(--player-flash-dur, 1500ms) + var(--ease-standard) forwards; +} + +::view-transition-old(play-card), +::view-transition-new(play-card) { + animation-duration: 360ms; + animation-timing-function: var(--ease-sheet); +} + +/* 主题切换的渐进过渡(theme-toggle.tsx):走 View Transitions 默认的 + 交叉淡化(plus-lighter 混合),这里只把 UA 默认的 250ms 放缓到 420ms + 并换成标准减速曲线,让明暗互换从容一些。scoped 在 .theme-vt 下, + 不影响其它 view transition(play-card 的导航过渡仍走默认时长)。 */ +:root.theme-vt::view-transition-old(root), +:root.theme-vt::view-transition-new(root) { + animation-duration: 420ms; + animation-timing-function: var(--ease-standard); +} + +@keyframes update-pill-ring { + 0% { + box-shadow: 0 0 0 0 color-mix(in oklch, var(--primary) 45%, transparent); + opacity: 1; + } + + 70% { + box-shadow: 0 0 0 10px color-mix(in oklch, var(--primary) 0%, transparent); + opacity: 0; + } + + 100% { + box-shadow: 0 0 0 0 color-mix(in oklch, var(--primary) 0%, transparent); + opacity: 0; + } +} diff --git a/spikes/deno/web/app/components/console/api-key-dialog.tsx b/spikes/deno/web/app/components/console/api-key-dialog.tsx new file mode 100644 index 00000000..70357ca6 --- /dev/null +++ b/spikes/deno/web/app/components/console/api-key-dialog.tsx @@ -0,0 +1,80 @@ +import { KeyRound } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { Button } from "~/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; +import { Input } from "~/components/ui/input"; +import { clearAuth, getToken, setToken } from "~/lib/api"; + +/** 设置本机使用的 API Key(存 localStorage,首版单管理员) */ +export function ApiKeyDialog() { + const [open, setOpen] = useState(false); + const [value, setValue] = useState(""); + const hasKey = typeof window !== "undefined" && !!getToken(); + + return ( +

{ + setOpen(o); + if (o) setValue(""); + }} + > + + + + + + API Key + 控制台用它访问服务端接口,只保存在本浏览器。 + + setValue(e.target.value)} + /> + + {hasKey ? ( + + ) : null} + + + + + ); +} diff --git a/spikes/deno/web/app/components/console/empty-state.tsx b/spikes/deno/web/app/components/console/empty-state.tsx new file mode 100644 index 00000000..c196440d --- /dev/null +++ b/spikes/deno/web/app/components/console/empty-state.tsx @@ -0,0 +1,35 @@ +import type { LucideIcon } from "lucide-react"; +import { Inbox } from "lucide-react"; +import type { ReactNode } from "react"; +import { cn } from "~/lib/utils"; + +/** 空态:图标 + 标题 + 说明 + 可选操作。用于"尚无数据"与"功能建设中"占位 */ +export function EmptyState({ + icon: Icon = Inbox, + title, + description, + action, + plain = false, + className, +}: { + icon?: LucideIcon; + title: string; + description?: ReactNode; + action?: ReactNode; + /** 放在已有卡片内时用 plain,避免玻璃叠玻璃 */ + plain?: boolean; + className?: string; +}) { + return ( +
+ + + +

{title}

+ {description ?

{description}

: null} + {action ?
{action}
: null} +
+ ); +} diff --git a/spikes/deno/web/app/components/console/nav.test.ts b/spikes/deno/web/app/components/console/nav.test.ts new file mode 100644 index 00000000..d9b33494 --- /dev/null +++ b/spikes/deno/web/app/components/console/nav.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { findNavItem, NAV_ITEMS } from "./nav"; + +describe("控制台导航", () => { + it("12 个工作区,路径唯一", () => { + expect(NAV_ITEMS).toHaveLength(12); + expect(new Set(NAV_ITEMS.map((i) => i.to)).size).toBe(12); + }); + + it("按最长前缀匹配当前项", () => { + expect(findNavItem("/")?.label).toBe("首页"); + expect(findNavItem("/library/abc")?.label).toBe("媒体库"); + expect(findNavItem("/nope")).toBeUndefined(); + }); +}); diff --git a/spikes/deno/web/app/components/console/nav.ts b/spikes/deno/web/app/components/console/nav.ts new file mode 100644 index 00000000..78e61653 --- /dev/null +++ b/spikes/deno/web/app/components/console/nav.ts @@ -0,0 +1,86 @@ +import { + Bot, + CalendarClock, + Download, + FileStack, + Fingerprint, + Globe, + House, + Inbox, + Library, + type LucideIcon, + Rss, + Settings, + Tags, +} from "lucide-react"; + +export type NavItem = { to: string; label: string; icon: LucideIcon; description: string }; +export type NavGroup = { title: string; items: NavItem[] }; + +/** 控制台 12 个工作区(任务书 §3.1 第 10 条 / REQ §13.1) */ +export const NAV_GROUPS: NavGroup[] = [ + { + title: "概览", + items: [ + { + to: "/", + label: "首页", + icon: House, + description: "自动化概览、当前工作、需要我处理、缓存与保留、来源健康、最近产出", + }, + { to: "/inbox", label: "待处理中心", icon: Inbox, description: "按作品与根因合并的待决定事项,Agent 已先行调查" }, + ], + }, + { + title: "媒体", + items: [ + { to: "/library", label: "媒体库", icon: Library, description: "作品、内容单元、版本目标与实际路径" }, + { to: "/identity", label: "身份与元数据", icon: Fingerprint, description: "外部身份映射、待识别池与元数据来源" }, + { to: "/organize", label: "整理记录", icon: FileStack, description: "Copy/Move 入库前后路径与逐文件结果" }, + ], + }, + { + title: "获取", + items: [ + { + to: "/subscriptions", + label: "订阅与收录规则", + icon: CalendarClock, + description: "单次收录与持续追踪,版本目标与预算", + }, + { + to: "/preferences", + label: "偏好标签", + icon: Tags, + description: "软偏好与硬规则,手动取消的偏好不会被自动加回", + }, + { to: "/sites", label: "PT 站点工作台", icon: Globe, description: "统一格式浏览、搜索站点资源并关联本地库存" }, + { to: "/sources", label: "来源 / Feed / 快照", icon: Rss, description: "RSS 与站点快照的抓取状态、新鲜度与健康" }, + { to: "/tasks", label: "下载与任务", icon: Download, description: "下载、整理、核验的业务时间线与子任务" }, + ], + }, + { + title: "助手与系统", + items: [ + { to: "/agent", label: "Agent 会话", icon: Bot, description: "用自然语言交代目标,Agent 在授权内执行并汇报" }, + { + to: "/settings", + label: "系统设置", + icon: Settings, + description: "下载器与媒体服务器实例、路径映射、API Key、Pi 上游与自动化开关", + }, + ], + }, +]; + +export const NAV_ITEMS = NAV_GROUPS.flatMap((g) => g.items); + +/** 按路径找当前导航项(最长前缀匹配) */ +export function findNavItem(pathname: string): NavItem | undefined { + return ( + NAV_ITEMS.find((i) => i.to === pathname) ?? + NAV_ITEMS.filter((i) => i.to !== "/" && pathname.startsWith(`${i.to}/`)).sort( + (a, b) => b.to.length - a.to.length, + )[0] + ); +} diff --git a/spikes/deno/web/app/components/console/page-header.tsx b/spikes/deno/web/app/components/console/page-header.tsx new file mode 100644 index 00000000..61db1d6e --- /dev/null +++ b/spikes/deno/web/app/components/console/page-header.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; +import { cn } from "~/lib/utils"; + +/** 控制台页面标题:标题 + 一句说明 + 右侧操作区 */ +export function PageHeader({ + title, + description, + actions, + className, +}: { + title: string; + description?: ReactNode; + actions?: ReactNode; + className?: string; +}) { + return ( +
+
+

{title}

+ {description ?

{description}

: null} +
+ {actions ?
{actions}
: null} +
+ ); +} diff --git a/spikes/deno/web/app/components/console/status-badge.tsx b/spikes/deno/web/app/components/console/status-badge.tsx new file mode 100644 index 00000000..9d3b5715 --- /dev/null +++ b/spikes/deno/web/app/components/console/status-badge.tsx @@ -0,0 +1,37 @@ +import type { ReactNode } from "react"; +import { cn } from "~/lib/utils"; + +export type StatusTone = "success" | "warning" | "destructive" | "neutral"; + +// 状态色只表示状态,不做装饰(docs/design/interface-materials.md) +const TONE: Record = { + success: { box: "border-success/30 bg-success/10 text-success", dot: "bg-success" }, + warning: { box: "border-warning/30 bg-warning/10 text-warning", dot: "bg-warning" }, + destructive: { box: "border-destructive/30 bg-destructive/10 text-destructive", dot: "bg-destructive" }, + neutral: { box: "border-border bg-foreground/[0.04] text-muted-foreground", dot: "bg-muted-foreground" }, +}; + +/** 状态徽章:文字必填,不只靠颜色传达状态 */ +export function StatusBadge({ + tone = "neutral", + children, + className, +}: { + tone?: StatusTone; + children: ReactNode; + className?: string; +}) { + const t = TONE[tone]; + return ( + + + {children} + + ); +} diff --git a/spikes/deno/web/app/components/console/workspace-placeholder.tsx b/spikes/deno/web/app/components/console/workspace-placeholder.tsx new file mode 100644 index 00000000..1a76fe03 --- /dev/null +++ b/spikes/deno/web/app/components/console/workspace-placeholder.tsx @@ -0,0 +1,21 @@ +import { Construction } from "lucide-react"; +import { useLocation } from "react-router"; +import { EmptyState } from "./empty-state"; +import { findNavItem } from "./nav"; +import { PageHeader } from "./page-header"; + +/** 工作区占位页:标题与说明取自导航表,正文为"建设中"空态 */ +export function WorkspacePlaceholder({ milestone = "W3" }: { milestone?: string }) { + const { pathname } = useLocation(); + const item = findNavItem(pathname); + return ( + <> + + + + ); +} diff --git a/spikes/deno/web/app/components/prism-leak.tsx b/spikes/deno/web/app/components/prism-leak.tsx new file mode 100644 index 00000000..6e19cbd7 --- /dev/null +++ b/spikes/deno/web/app/components/prism-leak.tsx @@ -0,0 +1,10 @@ +import { cn } from "~/lib/utils"; + +/** + * 全站背景上左上角那束淡淡的棱镜漏光(样式见 app.css 的 .prism-leak)。 + * 各页面外壳各放一个:用户中心、流媒体浏览页、公共页(落地页除外)、测速、404、退订。 + * 放在背景层之后、内容之前;宿主是层叠上下文时用 -z-10 垫在内容下面。 + */ +export default function PrismLeak({ className }: { className?: string }) { + return
; +} diff --git a/spikes/deno/web/app/components/theme-toggle.tsx b/spikes/deno/web/app/components/theme-toggle.tsx new file mode 100644 index 00000000..7007f654 --- /dev/null +++ b/spikes/deno/web/app/components/theme-toggle.tsx @@ -0,0 +1,57 @@ +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; +import { flushSync } from "react-dom"; + +import { Button } from "~/components/ui/button"; +import { cn } from "~/lib/utils"; + +export function ThemeToggle({ className }: { className?: string }) { + const [mounted, setMounted] = useState(false); + const { resolvedTheme, setTheme } = useTheme(); + + useEffect(() => { + setMounted(true); + }, []); + + const isDark = mounted && resolvedTheme === "dark"; + const nextTheme = isDark ? "light" : "dark"; + + // 渐进式主题过渡:整页新旧快照交叉淡化(View Transitions 默认行为, + // plus-lighter 混合,中途无灰闪)。.theme-vt 只在本次过渡期间挂在 html + // 上,给 app.css 一个作用域去放缓时长/缓动,不影响其它 view transition + // (如 play-card)。不支持或用户偏好减弱动效时,退回直接切换。 + function handleToggle() { + const prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (!document.startViewTransition || prefersReduced) { + setTheme(nextTheme); + return; + } + + const root = document.documentElement; + root.classList.add("theme-vt"); + // flushSync:startViewTransition 的回调返回后浏览器就拍新快照, + // 必须让 React 同步把主题类落到 DOM,不能等下一轮调度 + const vt = document.startViewTransition(() => { + flushSync(() => setTheme(nextTheme)); + }); + void vt.finished.finally(() => root.classList.remove("theme-vt")); + } + + return ( + + ); +} diff --git a/spikes/deno/web/app/components/ui/alert-dialog.tsx b/spikes/deno/web/app/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..c33977e7 --- /dev/null +++ b/spikes/deno/web/app/components/ui/alert-dialog.tsx @@ -0,0 +1,160 @@ +import { AlertDialog as AlertDialogPrimitive } from "radix-ui"; +import type * as React from "react"; +import { Button } from "~/components/ui/button"; +import { cn } from "~/lib/utils"; + +function AlertDialog({ ...props }: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return ; +} + +function AlertDialogOverlay({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm"; +}) { + return ( + + + + + ); +} + +function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ); +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +}; diff --git a/spikes/deno/web/app/components/ui/avatar.tsx b/spikes/deno/web/app/components/ui/avatar.tsx new file mode 100644 index 00000000..b6c790d1 --- /dev/null +++ b/spikes/deno/web/app/components/ui/avatar.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { Avatar as AvatarPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Avatar({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" | "lg"; +}) { + return ( + + ); +} + +function AvatarImage({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className, + )} + {...props} + /> + ); +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className, + )} + {...props} + /> + ); +} + +export { Avatar, AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage }; diff --git a/spikes/deno/web/app/components/ui/badge.tsx b/spikes/deno/web/app/components/ui/badge.tsx new file mode 100644 index 00000000..6741bb20 --- /dev/null +++ b/spikes/deno/web/app/components/ui/badge.tsx @@ -0,0 +1,40 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: "pill-selected", + secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/25 dark:focus-visible:ring-destructive/40", + outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span"; + + return ( + + ); +} + +export { Badge, badgeVariants }; diff --git a/spikes/deno/web/app/components/ui/breadcrumb.tsx b/spikes/deno/web/app/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..68b1f1ae --- /dev/null +++ b/spikes/deno/web/app/components/ui/breadcrumb.tsx @@ -0,0 +1,92 @@ +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import { Slot } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return
+ ); +} + +function DialogTitle({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/spikes/deno/web/app/components/ui/input.tsx b/spikes/deno/web/app/components/ui/input.tsx new file mode 100644 index 00000000..de935f70 --- /dev/null +++ b/spikes/deno/web/app/components/ui/input.tsx @@ -0,0 +1,21 @@ +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/spikes/deno/web/app/components/ui/label.tsx b/spikes/deno/web/app/components/ui/label.tsx new file mode 100644 index 00000000..20d6f055 --- /dev/null +++ b/spikes/deno/web/app/components/ui/label.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { Label as LabelPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Label({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/spikes/deno/web/app/components/ui/pagination.tsx b/spikes/deno/web/app/components/ui/pagination.tsx new file mode 100644 index 00000000..26f4fb1d --- /dev/null +++ b/spikes/deno/web/app/components/ui/pagination.tsx @@ -0,0 +1,154 @@ +import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react"; +import { cn } from "~/lib/utils"; + +const PAGE_SIZE_OPTIONS = [10, 15, 20, 50, 100]; + +type PaginationProps = { + page: number; + pageSize: number; + total: number; + onPageChange: (page: number) => void; + onPageSizeChange?: (size: number) => void; + className?: string; +}; + +export function Pagination({ page, pageSize, total, onPageChange, onPageSizeChange, className }: PaginationProps) { + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + const from = total === 0 ? 0 : (page - 1) * pageSize + 1; + const to = Math.min(page * pageSize, total); + + const isFirst = page <= 1; + const isLast = page >= totalPages; + + // 生成页码列表(最多显示 7 个按钮) + function buildPages(): (number | "...")[] { + if (totalPages <= 7) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + if (page <= 4) { + return [1, 2, 3, 4, 5, "...", totalPages]; + } + if (page >= totalPages - 3) { + return [1, "...", totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages]; + } + return [1, "...", page - 1, page, page + 1, "...", totalPages]; + } + + const pages = buildPages(); + + return ( +
+ {/* 左侧:总数 + 每页显示 */} +
+ + 共 {total} 条 · 第{" "} + {from} + {" 至 "} + {to} 条 + + + {/* 每页条数 */} + {onPageSizeChange && ( +
+ 每页 + +
+ )} +
+ + {/* 右侧:页码导航 */} +
+ {/* 首页 */} + onPageChange(1)} disabled={isFirst} title="第一页"> + + + + {/* 上一页 */} + onPageChange(page - 1)} disabled={isFirst} title="上一页"> + + + + {/* 页码按钮 */} +
+ {pages.map((p, i) => + p === "..." ? ( + + ··· + + ) : ( + + ), + )} +
+ + {/* 下一页 */} + onPageChange(page + 1)} disabled={isLast} title="下一页"> + + + + {/* 末页 */} + onPageChange(totalPages)} disabled={isLast} title="最后一页"> + + +
+
+ ); +} + +function NavBtn({ + children, + onClick, + disabled, + title, +}: { + children: React.ReactNode; + onClick: () => void; + disabled: boolean; + title?: string; +}) { + return ( + + ); +} diff --git a/spikes/deno/web/app/components/ui/popover.tsx b/spikes/deno/web/app/components/ui/popover.tsx new file mode 100644 index 00000000..f7e0a61d --- /dev/null +++ b/spikes/deno/web/app/components/ui/popover.tsx @@ -0,0 +1,52 @@ +import { Popover as PopoverPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Popover({ ...props }: React.ComponentProps) { + return ; +} + +function PopoverTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function PopoverAnchor({ ...props }: React.ComponentProps) { + return ; +} + +function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) { + return
; +} + +function PopoverDescription({ className, ...props }: React.ComponentProps<"p">) { + return

; +} + +export { Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger }; diff --git a/spikes/deno/web/app/components/ui/select.tsx b/spikes/deno/web/app/components/ui/select.tsx new file mode 100644 index 00000000..1e62d01d --- /dev/null +++ b/spikes/deno/web/app/components/ui/select.tsx @@ -0,0 +1,160 @@ +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import { Select as SelectPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Select({ ...props }: React.ComponentProps) { + return ; +} + +function SelectGroup({ ...props }: React.ComponentProps) { + return ; +} + +function SelectValue({ ...props }: React.ComponentProps) { + return ; +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default"; +}) { + return ( + + {children} + + + + + ); +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SelectItem({ className, children, ...props }: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function SelectSeparator({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SelectScrollUpButton({ className, ...props }: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/spikes/deno/web/app/components/ui/separator.tsx b/spikes/deno/web/app/components/ui/separator.tsx new file mode 100644 index 00000000..c656ba5a --- /dev/null +++ b/spikes/deno/web/app/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import { Separator as SeparatorPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator }; diff --git a/spikes/deno/web/app/components/ui/sheet.tsx b/spikes/deno/web/app/components/ui/sheet.tsx new file mode 100644 index 00000000..64fe012d --- /dev/null +++ b/spikes/deno/web/app/components/ui/sheet.tsx @@ -0,0 +1,105 @@ +import { XIcon } from "lucide-react"; +import { Dialog as SheetPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Sheet({ ...props }: React.ComponentProps) { + return ; +} + +function SheetTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function SheetClose({ ...props }: React.ComponentProps) { + return ; +} + +function SheetPortal({ ...props }: React.ComponentProps) { + return ; +} + +function SheetOverlay({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SheetContent({ + className, + children, + side = "right", + showCloseButton = true, + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left"; + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return

; +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function SheetTitle({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SheetDescription({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger }; diff --git a/spikes/deno/web/app/components/ui/slider.tsx b/spikes/deno/web/app/components/ui/slider.tsx new file mode 100644 index 00000000..c95a89b5 --- /dev/null +++ b/spikes/deno/web/app/components/ui/slider.tsx @@ -0,0 +1,54 @@ +import { Slider as SliderPrimitive } from "radix-ui"; +import * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Slider({ + className, + defaultValue, + value, + min = 0, + max = 100, + ...props +}: React.ComponentProps) { + const _values = React.useMemo( + () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]), + [value, defaultValue, min, max], + ); + + return ( + + + + + {Array.from({ length: _values.length }, (_, index) => ( + + ))} + + ); +} + +export { Slider }; diff --git a/spikes/deno/web/app/components/ui/sonner.tsx b/spikes/deno/web/app/components/ui/sonner.tsx new file mode 100644 index 00000000..f489bf63 --- /dev/null +++ b/spikes/deno/web/app/components/ui/sonner.tsx @@ -0,0 +1,36 @@ +import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon } from "lucide-react"; +import { useTheme } from "next-themes"; +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + pointerEvents: "none", + } as React.CSSProperties + } + toastOptions={{ + style: { pointerEvents: "auto" }, + }} + {...props} + /> + ); +}; + +export { Toaster }; diff --git a/spikes/deno/web/app/components/ui/switch.tsx b/spikes/deno/web/app/components/ui/switch.tsx new file mode 100644 index 00000000..1ed3b5a8 --- /dev/null +++ b/spikes/deno/web/app/components/ui/switch.tsx @@ -0,0 +1,24 @@ +import { Switch as SwitchPrimitive } from "radix-ui"; +import type * as React from "react"; + +import { cn } from "~/lib/utils"; + +function Switch({ className, ...props }: React.ComponentProps) { + return ( + + + + ); +} + +export { Switch }; diff --git a/spikes/deno/web/app/globals.d.ts b/spikes/deno/web/app/globals.d.ts new file mode 100644 index 00000000..1f87c4b4 --- /dev/null +++ b/spikes/deno/web/app/globals.d.ts @@ -0,0 +1,2 @@ +/** 构建时由 vite.config.ts 的 appVersion 插件注入 */ +declare const __APP_VERSION__: string; diff --git a/spikes/deno/web/app/lib/api.ts b/spikes/deno/web/app/lib/api.ts new file mode 100644 index 00000000..ee53403d --- /dev/null +++ b/spikes/deno/web/app/lib/api.ts @@ -0,0 +1,107 @@ +import type { ApiError, ApiResponse } from "@umt/contracts"; + +export type { ApiResponse }; + +// ─── API Key 存储 ─────────────────────────────────────────────── +const TOKEN_KEY = "umt-api-key"; + +export function getToken(): string { + if (typeof localStorage === "undefined") return ""; + return localStorage.getItem(TOKEN_KEY) || ""; +} + +export function setToken(token: string) { + localStorage.setItem(TOKEN_KEY, token); +} + +export function clearAuth() { + localStorage.removeItem(TOKEN_KEY); +} + +// ─── 请求参数 ──────────────────────────────────────────────────── +export type FetcherArg = { + /** 路径变量,如 { id: "..." } 替换 /api/v1/media/:id */ + pathParams?: Record; + params?: Record; + method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + body?: B; + headers?: Record; + signal?: AbortSignal; +}; + +export const API_BASE = import.meta.env.VITE_API_BASE_URL ?? ""; + +const fail = (code: ApiError["code"], message: string): ApiResponse => ({ + ok: false, + error: { code, message }, +}); + +/** + * 核心 fetcher:始终返回 ApiResponse 信封,不抛出网络/解析错误。 + * queryFn: () => apiFetch("/api/v1/health") + * mutationFn: (body) => apiFetch("/api/v1/xxx", { method: "POST", body }) + */ +export async function apiFetch(path: string, arg: FetcherArg = {}): Promise> { + const { method = "GET", headers = {}, body, params, pathParams, signal } = arg; + + let url = `${API_BASE.replace(/\/+$/, "")}${path}`; + if (pathParams) { + for (const [key, value] of Object.entries(pathParams)) { + url = url.replace(`:${key}`, encodeURIComponent(String(value))); + } + } + if (params) { + const qs = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) qs.append(key, String(value)); + } + const s = qs.toString(); + if (s) url += (url.includes("?") ? "&" : "?") + s; + } + + const reqHeaders: Record = { ...headers }; + const token = getToken(); + if (token) reqHeaders.Authorization = `Bearer ${token}`; + + let reqBody: BodyInit | undefined; + if (body instanceof FormData) { + reqBody = body; + } else if (body !== undefined && body !== null) { + reqHeaders["Content-Type"] ??= "application/json"; + reqBody = JSON.stringify(body); + } + + let res: Response; + try { + res = await fetch(url, { method, headers: reqHeaders, body: reqBody, signal }); + } catch (err) { + if ((err as Error).name === "AbortError") throw err; + return fail("upstream_unavailable", "无法连接服务"); + } + + if (res.status === 401 && typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("unauthorized")); + } + + try { + return (await res.json()) as ApiResponse; + } catch { + return fail("internal", `响应不是 JSON(HTTP ${res.status})`); + } +} + +/** 取出 data,失败时抛出(给 react-query 的 queryFn 用,错误进入 error 状态) */ +export async function apiData(path: string, arg?: FetcherArg): Promise { + const r = await apiFetch(path, arg); + if (!r.ok) throw Object.assign(new Error(r.error.message), { code: r.error.code }); + return r.data; +} + +/** TanStack Query mutationFn 包装 */ +export function mutateFetcher(path: string, base?: Omit, "body">) { + return (body: B) => apiFetch(path, { method: "POST", ...base, body }); +} + +export const TTL_SECOND = 1_000; +export const TTL_MINUTE = TTL_SECOND * 60; +export const TTL_HOUR = TTL_MINUTE * 60; diff --git a/spikes/deno/web/app/lib/design-tokens.generated.ts b/spikes/deno/web/app/lib/design-tokens.generated.ts new file mode 100644 index 00000000..c0988b12 --- /dev/null +++ b/spikes/deno/web/app/lib/design-tokens.generated.ts @@ -0,0 +1,68 @@ +// 由 scripts/gen-design-tokens.mjs 从 app/app.css 生成,不要手改;改了 app.css 跑 pnpm gen:tokens +export const DESIGN_TOKENS = { + "fontSizes": [ + "micro", + "label", + "meta", + "caption", + "body", + "control", + "lead", + "lead-lg", + "title-xs", + "title-sm", + "title", + "title-lg", + "headline-xs", + "headline-sm", + "headline", + "headline-lg", + "display-xs", + "display-sm", + "display", + "display-lg", + "display-xl", + "hero-stream", + "hero-stream-sm", + "hero-stream-md", + "detail-title", + "notfound-code", + "pricing-hero", + "pricing-title", + "auth-title", + "landing-hero", + "landing-platforms-title", + "landing-immersive-title", + "landing-service-hero", + "landing-service-title", + "landing-service-title-lg", + "landing-service-sub" + ], + "eases": [ + "standard", + "spring", + "spring-soft", + "sheet", + "pop" + ], + "radii": [ + "sm", + "md", + "lg", + "xl", + "2xl", + "3xl", + "4xl", + "tight", + "drawer", + "slab" + ], + "durations": [ + "micro", + "base", + "layout", + "slow", + "hero", + "cinematic" + ] +} as const; diff --git a/spikes/deno/web/app/lib/utils.ts b/spikes/deno/web/app/lib/utils.ts new file mode 100644 index 00000000..8752c5fb --- /dev/null +++ b/spikes/deno/web/app/lib/utils.ts @@ -0,0 +1,57 @@ +import { type ClassValue, clsx } from "clsx"; +import { extendTailwindMerge } from "tailwind-merge"; + +import { DESIGN_TOKENS } from "./design-tokens.generated"; + +/** + * 自定义工具类的 token(字号 / 圆角 / 缓动 / 时长)要告诉 tailwind-merge 它们属于哪一组: + * 不登记的话 `text-control text-muted-foreground` 会被当成两个颜色类冲突,字号那个被丢掉。 + * 登记表由 scripts/gen-design-tokens.mjs 从 app.css 生成,CI 校验它是最新的。 + */ +const twMerge = extendTailwindMerge({ + extend: { + theme: { + radius: [...DESIGN_TOKENS.radii], + ease: [...DESIGN_TOKENS.eases], + }, + classGroups: { + "font-size": [{ text: [...DESIGN_TOKENS.fontSizes] }], + duration: [{ duration: [...DESIGN_TOKENS.durations] }], + }, + }, +}); + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatBytes(bytes: number, decimals = 2) { + if (!+bytes) return "0 B"; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); + return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; +} + +export function formatDateTime(value: string | null | undefined) { + if (!value) return "—"; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return value; + return d.toLocaleString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); +} + +export function formatDate(value: string | null | undefined) { + if (!value) return "—"; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return value; + return d.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" }); +} diff --git a/spikes/deno/web/app/root.tsx b/spikes/deno/web/app/root.tsx new file mode 100644 index 00000000..9af3af9f --- /dev/null +++ b/spikes/deno/web/app/root.tsx @@ -0,0 +1,85 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ThemeProvider } from "next-themes"; +import { useState } from "react"; +import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; +import { Toaster } from "~/components/ui/sonner"; + +import type { Route } from "./+types/root"; +import "@fontsource-variable/inter-tight"; +import "@fontsource-variable/space-grotesk"; +import "./app.css"; + +export const meta: Route.MetaFunction = () => [{ title: "UHD 媒体自动化控制台" }]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + + {children} + + + + + + + ); +} + +export default function App() { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { staleTime: 30_000, refetchOnWindowFocus: false }, + }, + }), + ); + return ( + + + + ); +} + +export function HydrateFallback() { + return null; +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = "出错了"; + let details = "发生了未预期的错误。"; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "错误"; + details = error.status === 404 ? "页面不存在。" : error.statusText || details; + } else if (import.meta.env.DEV && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/spikes/deno/web/app/routes.ts b/spikes/deno/web/app/routes.ts new file mode 100644 index 00000000..e835043d --- /dev/null +++ b/spikes/deno/web/app/routes.ts @@ -0,0 +1,20 @@ +import { index, layout, type RouteConfig, route } from "@react-router/dev/routes"; + +// 显式路由表。新增工作区:在这里加一行 + components/console/nav.ts 加导航项 +export default [ + layout("routes/console/layout.tsx", [ + index("routes/console/home.tsx"), + route("inbox", "routes/console/inbox.tsx"), + route("library", "routes/console/library.tsx"), + route("identity", "routes/console/identity.tsx"), + route("organize", "routes/console/organize.tsx"), + route("subscriptions", "routes/console/subscriptions.tsx"), + route("preferences", "routes/console/preferences.tsx"), + route("sites", "routes/console/sites.tsx"), + route("sources", "routes/console/sources.tsx"), + route("tasks", "routes/console/tasks.tsx"), + route("agent", "routes/console/agent.tsx"), + route("settings", "routes/console/settings.tsx"), + ]), + route("*", "routes/not-found.tsx"), +] satisfies RouteConfig; diff --git a/spikes/deno/web/app/routes/console/agent.tsx b/spikes/deno/web/app/routes/console/agent.tsx new file mode 100644 index 00000000..43a48816 --- /dev/null +++ b/spikes/deno/web/app/routes/console/agent.tsx @@ -0,0 +1,31 @@ +import { Bot, Send } from "lucide-react"; +import { EmptyState } from "~/components/console/empty-state"; +import { PageHeader } from "~/components/console/page-header"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; + +/** Agent 会话页占位:消息流 + 输入框骨架,W2/W3 接入 agent_run 与 SSE 增量 */ +export default function AgentSession() { + return ( + <> + +
+ +
e.preventDefault()}> + + +
+
+ + ); +} diff --git a/spikes/deno/web/app/routes/console/home.tsx b/spikes/deno/web/app/routes/console/home.tsx new file mode 100644 index 00000000..409f2a4b --- /dev/null +++ b/spikes/deno/web/app/routes/console/home.tsx @@ -0,0 +1,58 @@ +import { Activity, AlertTriangle, CircleCheck, HardDrive, ListTodo, Radio, Sparkles } from "lucide-react"; +import { useHealth } from "~/api/health"; +import { EmptyState } from "~/components/console/empty-state"; +import { PageHeader } from "~/components/console/page-header"; +import { StatusBadge } from "~/components/console/status-badge"; +import { formatDateTime } from "~/lib/utils"; + +// 首页六区块(REQ §13.4.3),W3 接入正式状态投影 +const SECTIONS = [ + { title: "自动化概览", icon: Sparkles, hint: "今日新增、升级与已核验入库" }, + { title: "当前工作", icon: Activity, hint: "正在搜索/下载/整理/核验的目标" }, + { title: "需要我处理", icon: AlertTriangle, hint: "根因级事项与剩余决策" }, + { title: "缓存与保留", icon: HardDrive, hint: "使用率、在途预留与可清理量" }, + { title: "来源健康", icon: Radio, hint: "站点、qB、Emby、模型上游" }, + { title: "最近产出", icon: ListTodo, hint: "新增的集与版本、整理前后路径" }, +]; + +function ServiceStatus() { + const { data, isPending, isError, error } = useHealth(); + if (isPending) return 检查中; + if (isError) return 服务不可达:{error.message}; + return ( +
+ + 服务{data.status === "ok" ? "正常" : "降级"} + + + 数据库 {data.db === "ok" ? "正常" : data.db === "down" ? "不可用" : "未知"} + + + v{data.version} · {data.role} · {formatDateTime(data.time)} + +
+ ); +} + +export default function Home() { + return ( + <> + } + /> +
+ {SECTIONS.map((s) => ( +
+
+ +

{s.title}

+
+ +
+ ))} +
+ + ); +} diff --git a/spikes/deno/web/app/routes/console/identity.tsx b/spikes/deno/web/app/routes/console/identity.tsx new file mode 100644 index 00000000..60e4e0bc --- /dev/null +++ b/spikes/deno/web/app/routes/console/identity.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function IdentityPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/inbox.tsx b/spikes/deno/web/app/routes/console/inbox.tsx new file mode 100644 index 00000000..ef5141fd --- /dev/null +++ b/spikes/deno/web/app/routes/console/inbox.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function InboxPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/layout.tsx b/spikes/deno/web/app/routes/console/layout.tsx new file mode 100644 index 00000000..f47b0d45 --- /dev/null +++ b/spikes/deno/web/app/routes/console/layout.tsx @@ -0,0 +1,208 @@ +import { ChevronLeft, ChevronRight, Clapperboard, Menu } from "lucide-react"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { Link, NavLink, Outlet, useLocation } from "react-router"; +import { ApiKeyDialog } from "~/components/console/api-key-dialog"; +import { findNavItem, NAV_GROUPS, NAV_ITEMS, type NavGroup } from "~/components/console/nav"; +import PrismLeak from "~/components/prism-leak"; +import { ThemeToggle } from "~/components/theme-toggle"; +import { Button } from "~/components/ui/button"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "~/components/ui/sheet"; +import { cn } from "~/lib/utils"; + +const PORTAL_NAME = "媒体自动化"; + +// ─── 展开态导航:选中项由一枚玻璃滑块(seg-thumb)滑到激活链接背后 ─── +function ExpandedNav({ groups, onNavigate }: { groups: NavGroup[]; onNavigate?: () => void }) { + const location = useLocation(); + const navRef = useRef(null); + const [pill, setPill] = useState<{ x: number; y: number; w: number; h: number } | null>(null); + + const measure = useCallback(() => { + const nav = navRef.current; + if (!nav) return; + const active = nav.querySelector('[aria-current="page"]'); + if (!active) { + setPill(null); + return; + } + const navRect = nav.getBoundingClientRect(); + const rect = active.getBoundingClientRect(); + setPill({ x: rect.left - navRect.left, y: rect.top - navRect.top + nav.scrollTop, w: rect.width, h: rect.height }); + }, []); + + // biome-ignore lint/correctness/useExhaustiveDependencies: 路由变化时重新量测 + useLayoutEffect(() => { + measure(); + }, [measure, location.pathname]); + + useEffect(() => { + window.addEventListener("resize", measure); + return () => window.removeEventListener("resize", measure); + }, [measure]); + + return ( + + ); +} + +// ─── 折叠态:只显示图标 ─── +function CollapsedNav() { + return ( + + ); +} + +function Brand({ collapsed = false }: { collapsed?: boolean }) { + return ( + + + + + {!collapsed &&

UHD Media Tool

} + + ); +} + +export default function ConsoleLayout() { + const location = useLocation(); + const [collapsed, setCollapsed] = useState(false); + const [sheetOpen, setSheetOpen] = useState(false); + const activeLabel = findNavItem(location.pathname)?.label ?? PORTAL_NAME; + + return ( +
+
+ + + {/* 桌面端侧栏 */} + + + {/* 主内容区 */} +
+
+
+ + + + + + + + + + + setSheetOpen(false)} /> + + + {activeLabel} +
+
+ + +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/spikes/deno/web/app/routes/console/library.tsx b/spikes/deno/web/app/routes/console/library.tsx new file mode 100644 index 00000000..0e1cdad9 --- /dev/null +++ b/spikes/deno/web/app/routes/console/library.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function LibraryPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/organize.tsx b/spikes/deno/web/app/routes/console/organize.tsx new file mode 100644 index 00000000..551099e2 --- /dev/null +++ b/spikes/deno/web/app/routes/console/organize.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function OrganizePage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/preferences.tsx b/spikes/deno/web/app/routes/console/preferences.tsx new file mode 100644 index 00000000..b96339f6 --- /dev/null +++ b/spikes/deno/web/app/routes/console/preferences.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function PreferencesPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/settings.tsx b/spikes/deno/web/app/routes/console/settings.tsx new file mode 100644 index 00000000..93150ed5 --- /dev/null +++ b/spikes/deno/web/app/routes/console/settings.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function SettingsPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/sites.tsx b/spikes/deno/web/app/routes/console/sites.tsx new file mode 100644 index 00000000..d3ddd00e --- /dev/null +++ b/spikes/deno/web/app/routes/console/sites.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function SitesPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/sources.tsx b/spikes/deno/web/app/routes/console/sources.tsx new file mode 100644 index 00000000..88cbbf75 --- /dev/null +++ b/spikes/deno/web/app/routes/console/sources.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function SourcesPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/subscriptions.tsx b/spikes/deno/web/app/routes/console/subscriptions.tsx new file mode 100644 index 00000000..0f12daab --- /dev/null +++ b/spikes/deno/web/app/routes/console/subscriptions.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function SubscriptionsPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/console/tasks.tsx b/spikes/deno/web/app/routes/console/tasks.tsx new file mode 100644 index 00000000..41d09cda --- /dev/null +++ b/spikes/deno/web/app/routes/console/tasks.tsx @@ -0,0 +1,6 @@ +import { WorkspacePlaceholder } from "~/components/console/workspace-placeholder"; + +// 占位:标题与说明取自 components/console/nav.ts +export default function TasksPage() { + return ; +} diff --git a/spikes/deno/web/app/routes/not-found.tsx b/spikes/deno/web/app/routes/not-found.tsx new file mode 100644 index 00000000..48af9ea9 --- /dev/null +++ b/spikes/deno/web/app/routes/not-found.tsx @@ -0,0 +1,19 @@ +import { Link } from "react-router"; +import { EmptyState } from "~/components/console/empty-state"; +import { buttonVariants } from "~/components/ui/button"; + +export default function NotFound() { + return ( +
+ + 回到首页 + + } + /> +
+ ); +} diff --git a/spikes/deno/web/components.json b/spikes/deno/web/components.json new file mode 100644 index 00000000..adece9fb --- /dev/null +++ b/spikes/deno/web/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/app.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "registries": {} +} diff --git a/spikes/deno/web/package.json b/spikes/deno/web/package.json new file mode 100644 index 00000000..d61b7616 --- /dev/null +++ b/spikes/deno/web/package.json @@ -0,0 +1,47 @@ +{ + "name": "@umt/web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "react-router dev --host 0.0.0.0 --port 5180", + "build": "react-router build", + "preview": "vite preview --host 0.0.0.0 --port 5180", + "typecheck": "react-router typegen && tsc", + "test": "vitest run", + "gen:tokens": "node scripts/gen-design-tokens.mjs", + "check:tokens": "node scripts/gen-design-tokens.mjs --check && sh scripts/check-design-tokens.sh" + }, + "dependencies": { + "@fontsource-variable/inter-tight": "^5.2.7", + "@fontsource-variable/space-grotesk": "^5.2.10", + "@react-router/node": "7.12.0", + "@tanstack/react-query": "^5.90.21", + "@umt/contracts": "workspace:*", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "isbot": "^5.1.31", + "lucide-react": "^0.576.0", + "next-themes": "^0.4.6", + "radix-ui": "^1.4.3", + "react": "^19.2.4", + "react-day-picker": "^10.0.1", + "react-dom": "^19.2.4", + "react-router": "7.12.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@react-router/dev": "7.12.0", + "@tailwindcss/vite": "^4.1.13", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "shadcn": "^3.8.5", + "tailwindcss": "^4.1.13", + "tw-animate-css": "^1.4.0", + "vite": "^7.1.7", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/spikes/deno/web/react-router.config.ts b/spikes/deno/web/react-router.config.ts new file mode 100644 index 00000000..51cf9985 --- /dev/null +++ b/spikes/deno/web/react-router.config.ts @@ -0,0 +1,6 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // SPA 模式,静态部署 + ssr: false, +} satisfies Config; diff --git a/spikes/deno/web/scripts/check-design-tokens.sh b/spikes/deno/web/scripts/check-design-tokens.sh new file mode 100755 index 00000000..69f85cb5 --- /dev/null +++ b/spikes/deno/web/scripts/check-design-tokens.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# 设计 token 守卫:组件里不许直接写原色、白黑透明度、十六进制或 Tailwind 调色板颜色,一律走语义 token。 +# 规范见仓库根 docs/design/(design-tokens.md、interface-materials.md、ui-controls.md)。 +# 在 apps/web 目录下运行;/dev/* 陈列页不在范围内。 +# 确实需要例外(第三方库的颜色参数等)时,在该行或上一行写 `token-allow: 原因`。 +set -eu +fail=0 +EXCLUDE='app/routes/dev/' +report() { + # $1 范围,$2 说明,$3 正则(PCRE);跳过注释行、token-allow 标记的行和它的下一行 + hits=$(grep -rnP "$3" $1 --include='*.tsx' --include='*.ts' | grep -v "$EXCLUDE" | grep -vP ':\d+:\s*(\*|//|/\*|\{/\*)' || true) + hits=$(printf '%s\n' "$hits" | while IFS= read -r hit; do + [ -z "$hit" ] && continue + file=${hit%%:*}; rest=${hit#*:}; line=${rest%%:*} + prev=$((line > 1 ? line - 1 : 1)) + sed -n "${prev},${line}p" "$file" | grep -q 'token-allow' || printf '%s\n' "$hit" + done) + if [ -n "$hits" ]; then + echo "✗ $2"; echo "$hits" | head -20; fail=1 + fi +} + +# 颜色:全站 +report app "原色类(bone / ink / gold)只能在 app.css 的 token 定义里出现" '(?<=-)(bone|ink|gold)-\d+\b' +report app "white / black 透明度请改写成 wash / on-media-wash / scrim / canvas(透明度留在调用处)" '(?<=-)(? 默认样式(pill-primary)" 'hover:bg-(primary|foreground|brand-strong)(?![\w/-])' +report app "浮层(下拉、菜单、弹窗面板)请用 pg-float,不要手写 shadow-2xl 之类的投影" '(? [ + ...new Set([...css.matchAll(new RegExp(`^\\s*--${ns}-([a-z0-9-]+)\\s*:`, "gm"))].map((m) => m[1])), +]; +const tokens = { + fontSizes: pick("text").filter((n) => !/^(xs|sm|base|lg|xl|\dxl)$/.test(n)), + eases: pick("ease"), + radii: pick("radius"), + durations: [...new Set([...css.matchAll(/^@utility duration-([a-z0-9-]+)/gm)].map((m) => m[1]))], +}; +const out = `// 由 scripts/gen-design-tokens.mjs 从 app/app.css 生成,不要手改;改了 app.css 跑 pnpm gen:tokens +export const DESIGN_TOKENS = ${JSON.stringify(tokens, null, 2)} as const; +`; +const target = new URL("../app/lib/design-tokens.generated.ts", import.meta.url); +if (process.argv.includes("--check")) { + let current = ""; + try { + current = readFileSync(target, "utf8"); + } catch {} + if (current !== out) { + console.error("app/lib/design-tokens.generated.ts 已过期,运行 pnpm gen:tokens"); + process.exit(1); + } + console.log( + `design token 登记表是最新的(字号 ${tokens.fontSizes.length} / 缓动 ${tokens.eases.length} / 圆角 ${tokens.radii.length} / 时长 ${tokens.durations.length})`, + ); +} else { + writeFileSync(target, out); + console.log( + `已生成 app/lib/design-tokens.generated.ts(字号 ${tokens.fontSizes.length} / 缓动 ${tokens.eases.length} / 圆角 ${tokens.radii.length} / 时长 ${tokens.durations.length})`, + ); +} diff --git a/spikes/deno/web/tsconfig.json b/spikes/deno/web/tsconfig.json new file mode 100644 index 00000000..88027084 --- /dev/null +++ b/spikes/deno/web/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"], + "exclude": ["node_modules", "build"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2023"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/spikes/deno/web/vite.config.ts b/spikes/deno/web/vite.config.ts new file mode 100644 index 00000000..84302408 --- /dev/null +++ b/spikes/deno/web/vite.config.ts @@ -0,0 +1,65 @@ +import fs from "node:fs"; +import path from "node:path"; +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig, loadEnv, type Plugin } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +/** 读取 git 短哈希(不依赖 git 命令) */ +function readGitShortSha(cwd: string): string | null { + try { + const gitDir = path.join(cwd, ".git"); + const head = fs.readFileSync(path.join(gitDir, "HEAD"), "utf8").trim(); + let sha = head; + if (head.startsWith("ref: ")) { + const ref = head.slice(5); + const refFile = path.join(gitDir, ref); + if (fs.existsSync(refFile)) { + sha = fs.readFileSync(refFile, "utf8").trim(); + } else { + const packed = fs.readFileSync(path.join(gitDir, "packed-refs"), "utf8"); + const line = packed.split("\n").find((l) => l.endsWith(` ${ref}`)); + if (!line) return null; + sha = line.split(" ")[0] ?? ""; + } + } + return /^[0-9a-f]{40}$/.test(sha) ? sha.slice(0, 7) : null; + } catch { + return null; + } +} + +/** 构建时注入 __APP_VERSION__ 并输出 version.json;优先 APP_VERSION,其次 git 短哈希,最后时间戳 */ +function appVersion(): Plugin { + const repoRoot = path.resolve(process.cwd(), "../.."); + const version = process.env.APP_VERSION?.trim() || readGitShortSha(repoRoot) || `build-${Date.now().toString(36)}`; + return { + name: "app-version", + config() { + return { define: { __APP_VERSION__: JSON.stringify(version) } }; + }, + generateBundle() { + this.emitFile({ + type: "asset", + fileName: "version.json", + source: JSON.stringify({ version, builtAt: new Date().toISOString() }), + }); + }, + }; +} + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + const apiTarget = env.VITE_API_TARGET || "http://localhost:4380"; + + return { + plugins: [tailwindcss(), reactRouter(), tsconfigPaths(), appVersion()], + server: { + port: 5180, + strictPort: true, + proxy: { + "/api": { target: apiTarget, changeOrigin: true }, + }, + }, + }; +}); diff --git a/spikes/deno/web/vitest.config.ts b/spikes/deno/web/vitest.config.ts new file mode 100644 index 00000000..4fe9313c --- /dev/null +++ b/spikes/deno/web/vitest.config.ts @@ -0,0 +1,8 @@ +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +// 单测不加载 react-router / tailwind 插件 +export default defineConfig({ + plugins: [tsconfigPaths()], + test: { include: ["app/**/*.test.ts"], environment: "node" }, +});