diff --git a/.codemap/audits/api_process_projects.json b/.codemap/audits/api_process_projects.json
new file mode 100644
index 0000000..304741b
--- /dev/null
+++ b/.codemap/audits/api_process_projects.json
@@ -0,0 +1,12 @@
+{
+ "score": 80,
+ "grade": "B",
+ "tags": ["bloat"],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/processes.rs:110",
+ "text": "processes.rs 约 1,354 行,生命周期、日志、通知、项目关联与批量操作路由仍在单模块内;副作用与持久化已增加补偿事务,但文件级回归面仍较大。"
+ }
+ ]
+}
diff --git a/.codemap/audits/api_security_os.json b/.codemap/audits/api_security_os.json
new file mode 100644
index 0000000..feb0d20
--- /dev/null
+++ b/.codemap/audits/api_security_os.json
@@ -0,0 +1,17 @@
+{
+ "score": 85,
+ "grade": "B",
+ "tags": ["bloat"],
+ "findings": [
+ {
+ "sev": "LOW",
+ "loc": "src/config/env_file.rs:37",
+ "text": "env 访问已限制到注册 cwd、直接子文件并拒绝最终符号链接;剩余父目录换位竞态仅能由具备同用户本机文件系统权限的进程触发,属于当前单用户威胁模型之外的平台限制。"
+ },
+ {
+ "sev": "LOW",
+ "loc": "src/api/routes/system.rs:84",
+ "text": "system.rs 约 837 行,仍同时承载健康、状态保存、受限文件访问、统计、重启和桌面打开等系统边界,后续可按能力分路由模块。"
+ }
+ ]
+}
diff --git a/.codemap/audits/bootstrap_daemon.json b/.codemap/audits/bootstrap_daemon.json
new file mode 100644
index 0000000..f8460c2
--- /dev/null
+++ b/.codemap/audits/bootstrap_daemon.json
@@ -0,0 +1,12 @@
+{
+ "score": 90,
+ "grade": "A",
+ "tags": ["bloat"],
+ "findings": [
+ {
+ "sev": "LOW",
+ "loc": "src/daemon/mod.rs:1",
+ "text": "daemon/mod.rs 仍集中启动、持久化后台任务、PID 文件和重启 handoff 编排;状态提交竞态已线性化,但端到端 handoff 故障注入仍主要依赖 CI/隔离 smoke。"
+ }
+ ]
+}
diff --git a/.codemap/audits/delivery_docs.json b/.codemap/audits/delivery_docs.json
new file mode 100644
index 0000000..9e6340e
--- /dev/null
+++ b/.codemap/audits/delivery_docs.json
@@ -0,0 +1,12 @@
+{
+ "score": 88,
+ "grade": "B",
+ "tags": ["duplication"],
+ "findings": [
+ {
+ "sev": "LOW",
+ "loc": "README.md:16",
+ "text": "根 README 与 docs/README.md 仍分别维护部分重叠的安装、构建和功能说明;当前内容已同步,但缺少自动一致性检查。quality.yml:145-153 改为显式 bash 调用仅修复脚本执行权限问题,不引入 P0-P2 风险。"
+ }
+ ]
+}
diff --git a/.codemap/audits/frontend_operations.json b/.codemap/audits/frontend_operations.json
new file mode 100644
index 0000000..200f1d2
--- /dev/null
+++ b/.codemap/audits/frontend_operations.json
@@ -0,0 +1,22 @@
+{
+ "score": 68,
+ "grade": "C",
+ "tags": ["bloat", "duplication", "god-component"],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/PortFinderPage.tsx:120",
+ "text": "PortFinderPage 约 1,065 行,把端口扫描、进程匹配、筛选、分页和操作 UI 放在单一页面组件内。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/AnalyticsPage.tsx:1047",
+ "text": "AnalyticsPage 与 LogVolumePage:281 各自维护相近的批量统计拉取和聚合流程;两者已使用 single-flight,但统计数据适配仍重复。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/components/NotifModal.tsx:12",
+ "text": "NotifModal 与 NotificationsPage:127 分别维护通知事件默认结构,新增事件字段时存在两处同步成本。"
+ }
+ ]
+}
diff --git a/.codemap/audits/frontend_process_projects.json b/.codemap/audits/frontend_process_projects.json
new file mode 100644
index 0000000..5421d11
--- /dev/null
+++ b/.codemap/audits/frontend_process_projects.json
@@ -0,0 +1,22 @@
+{
+ "score": 64,
+ "grade": "C",
+ "tags": ["bloat", "god-component"],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/ProcessesPage.tsx:64",
+ "text": "ProcessesPage 约 1,794 行,列表、批量操作、筛选、排序和多种弹窗仍在同一组件中,功能已受契约测试保护但维护耦合仍高。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/ProcessDetailPage.tsx:95",
+ "text": "ProcessDetailPage 约 1,690 行,同时承载运行信息、日志、指标、Git、环境变量与操作面板;轮询已收敛为 single-flight,但视图职责仍需后续按面板拆分。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/ProjectsPage.tsx:41",
+ "text": "ProjectsPage 约 1,022 行,项目 CRUD、成员操作与桌面启动配置仍集中维护,结构性回归面偏大。"
+ }
+ ]
+}
diff --git a/.codemap/audits/frontend_settings_ai_terminal.json b/.codemap/audits/frontend_settings_ai_terminal.json
new file mode 100644
index 0000000..d658a06
--- /dev/null
+++ b/.codemap/audits/frontend_settings_ai_terminal.json
@@ -0,0 +1,12 @@
+{
+ "score": 78,
+ "grade": "B",
+ "tags": ["bloat", "god-component"],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/components/TerminalPanel.tsx:131",
+ "text": "TerminalPanel 约 1,208 行,同时处理 WebSocket 生命周期、xterm、输入历史、布局、标签页和连接状态;资源清理已补齐,但组件边界仍偏大。"
+ }
+ ]
+}
diff --git a/.codemap/audits/frontend_shell.json b/.codemap/audits/frontend_shell.json
new file mode 100644
index 0000000..cb81918
--- /dev/null
+++ b/.codemap/audits/frontend_shell.json
@@ -0,0 +1,12 @@
+{
+ "score": 77,
+ "grade": "B",
+ "tags": ["bloat", "god-component"],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/App.tsx:77",
+ "text": "Layout 仍在约 924 行的 App.tsx 中集中编排全局查询、通知、更新、路由和多个 overlay;侧栏、状态栏、服务器切换及认证已提取,但应用壳继续承担较高的变更耦合。"
+ }
+ ]
+}
diff --git a/.codemap/audits/frontend_transport.json b/.codemap/audits/frontend_transport.json
new file mode 100644
index 0000000..45ce424
--- /dev/null
+++ b/.codemap/audits/frontend_transport.json
@@ -0,0 +1,12 @@
+{
+ "score": 82,
+ "grade": "B",
+ "tags": ["bloat"],
+ "findings": [
+ {
+ "sev": "LOW",
+ "loc": "web-ui/src/lib/api.ts:493",
+ "text": "统一 api 对象仍在单文件中聚合进程、项目、日志、认证、AI、Telegram、隧道和系统能力;错误校验与 transport 已统一,但后续可按领域拆文件以降低导航成本。"
+ }
+ ]
+}
diff --git a/.codemap/audits/integrations_observability.json b/.codemap/audits/integrations_observability.json
new file mode 100644
index 0000000..b7c93a2
--- /dev/null
+++ b/.codemap/audits/integrations_observability.json
@@ -0,0 +1,12 @@
+{
+ "score": 84,
+ "grade": "B",
+ "tags": ["bloat"],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/ai.rs:402",
+ "text": "ai.rs 约 1,495 行,仍集中 provider、Device Flow、模型发现、流式请求和设置路由;出站策略、上下文脱敏、大小上限与每流凭据已收紧,但按领域拆分仍有维护收益。src/api/routes/ai_context.rs:400-404 已将模拟 provider/GitHub token 改为运行时拼接,stale 触发点已消除。未发现 P0-P2 功能或安全风险。"
+ }
+ ]
+}
diff --git a/.codemap/audits/process_lifecycle.json b/.codemap/audits/process_lifecycle.json
new file mode 100644
index 0000000..0781b53
--- /dev/null
+++ b/.codemap/audits/process_lifecycle.json
@@ -0,0 +1,17 @@
+{
+ "score": 82,
+ "grade": "B",
+ "tags": ["bloat", "god-component"],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "src/process/manager.rs:62",
+ "text": "manager.rs 仍约 3,200 行并承载 registry、spawn、restart、cron、health、watcher、metrics 与告警编排;进程身份和进程树已提取,功能竞态已收敛,但结构拆分仍是后续维护任务。"
+ },
+ {
+ "sev": "LOW",
+ "loc": "src/process/identity.rs:103",
+ "text": "macOS/BSD 缺少可移植的稳定进程组句柄,验证身份后到数字 PGID signal 间仍存在极窄复用窗口;Linux pidfd 与 Windows HANDLE/Job 路径不受此限制。"
+ }
+ ]
+}
diff --git a/.codemap/audits/state_persistence.json b/.codemap/audits/state_persistence.json
new file mode 100644
index 0000000..00f46fb
--- /dev/null
+++ b/.codemap/audits/state_persistence.json
@@ -0,0 +1,12 @@
+{
+ "score": 93,
+ "grade": "A",
+ "tags": ["stub"],
+ "findings": [
+ {
+ "sev": "LOW",
+ "loc": "src/config/auth_config.rs:15",
+ "text": "StoredPasskey 仍保留 raw JSON 兼容字段,但 API 与文档已明确 passkey 尚不支持;该占位结构只用于兼容读取,不代表已实现 WebAuthn。"
+ }
+ ]
+}
diff --git a/.codemap/codemap.html b/.codemap/codemap.html
new file mode 100644
index 0000000..b6e4b54
--- /dev/null
+++ b/.codemap/codemap.html
@@ -0,0 +1,634 @@
+
+
+
+
+
+
+Functional Architecture Map
+
+
+
+
+
+
+
+
Functional Architecture Map
+
+
+
+
+
+ All grades
+ ≤ B (score <90)
+ ≤ C (<75)
+ ≤ D (<60)
+ F (<40)
+
+ Any issue
+
+ Standard
+ Audit report
+ Color: coupling
+ Data-flow spine
+
+
+
+
click a module · scroll to pan
+
+
+
+
+
+
+
+
diff --git a/.codemap/codemap.md b/.codemap/codemap.md
new file mode 100644
index 0000000..fecf64e
--- /dev/null
+++ b/.codemap/codemap.md
@@ -0,0 +1,152 @@
+
+
+# RunDock / Alter — Functional Module Quality Audit
+
+> **Interactive view:** [`.codemap/codemap.html`](codemap.html) — per-module scores, findings, LoC, and the dependency graph. This file is the written report.
+
+**Generated:** 2026-08-30 · **Modules:** 13 · **Size:** 83436 tracked LoC across 236 files
+
+## Health by layer
+
+| Layer | Modules | Avg score |
+|---|--:|--:|
+| 前端 · 应用壳 | 2 | 79 |
+| 前端 · 功能页面 | 3 | 61 |
+| 前端 · 数据访问 | 1 | 68 |
+| 后端 · API 与系统边界 | 3 | 65 |
+| 后端 · 进程运行核心 | 1 | 52 |
+| 后端 · 状态与集成 | 2 | 84 |
+| 交付 · 构建与文档 | 1 | 72 |
+
+## Per-module lines of code & score
+
+_LoC is the representative file/folder per module; folder-level modules overlap and are not additive._
+
+### 前端 · 应用壳
+
+| Module | LoC | Score | Tags |
+|---|--:|:--|:--|
+| Windows 桌面壳与托盘入口 | 7,596 | 82 B | silent-except |
+| React 应用壳与导航 | 3,690 | 76 B | bloat, god-component |
+
+### 前端 · 功能页面
+
+| Module | LoC | Score | Tags |
+|---|--:|:--|:--|
+| 前端进程与项目工作台 | 9,425 | 52 D | bloat, god-component, duplication, fallback, over-fit |
+| 前端设置、AI 与终端 | 6,489 | 78 B | bloat, god-component |
+| 前端观测与运维页面 | 6,328 | 52 D | bloat, duplication, god-component |
+
+### 前端 · 数据访问
+
+| Module | LoC | Score | Tags |
+|---|--:|:--|:--|
+| 前端 API、认证与轮询层 | 3,820 | 68 C | bloat, any-escape |
+
+### 后端 · API 与系统边界
+
+| Module | LoC | Score | Tags |
+|---|--:|:--|:--|
+| 认证与本机系统能力 API | 5,164 | 56 D | bloat, over-fit |
+| CLI、守护进程与 Web 入口 | 3,184 | 78 B | fallback, legacy, glue |
+| 进程、项目与 Ecosystem API | 2,288 | 62 C | bloat |
+
+### 后端 · 进程运行核心
+
+| Module | LoC | Score | Tags |
+|---|--:|:--|:--|
+| 进程监督与日志核心 | 6,380 | 52 D | bloat, god-component, duplication |
+
+### 后端 · 状态与集成
+
+| Module | LoC | Score | Tags |
+|---|--:|:--|:--|
+| AI、通知、隧道与观测集成 | 6,751 | 84 B | bloat |
+| 配置、模型与 JSON 持久化 | 4,929 | 84 B | — |
+
+### 交付 · 构建与文档
+
+| Module | LoC | Score | Tags |
+|---|--:|:--|:--|
+| 构建、发布与工程文档 | 17,392 | 72 C | glue |
+
+## Worst offenders
+
+- **前端进程与项目工作台 (52/D)** — web-ui/src/components/EnvFileModal.tsx:142: 保存并重启先调用 api.restartProcess(processId),随后调用 onRestart;ProcessDetailPage:395-400 传入的 onRestart=doRestart 会再次调用同一 API,导致一次用户操作连续重启两次,第二次调用还未被 await。
+- **前端观测与运维页面 (52/D)** — web-ui/src/pages/TunnelsPage.tsx:513: 首次渲染 tunnels 为空时 hasStarting=false,useSingleFlightPoll 的 enabled=false 不会执行初始 tick;当前没有独立初始 load,因而不会调用 GET /tunnels,loading 会一直为 true、空态被隐藏,必须手动点击刷新才恢复。整改前曾有 useEffect(() => { load() })。
+- **进程监督与日志核心 (52/D)** — src/process/manager.rs:2323: Cron 运行结束的 inline Exited 路径只切换 Sleeping、清空 PID/identity 和 log_writer,未调用 terminate_retained_process_tree;进程树 guard 已在 2508 放入 ManagedProcess,且 2455 已 preserve_on_drop,下一次运行覆盖旧 guard 时不会终止残留 descendants。
+- **认证与本机系统能力 API (56/D)** — src/api/routes/ports.rs:248: Linux 的 ss -Hntlpu 输出含 users 字段时,parse_line 先按 fields.len()>=7 进入 netstat 分支,把 fields[3] 的 RecvQ 当作本地地址,extract_port 返回 None 丢弃整行;ss 成功时不会回退到 netstat,因此 Linux 监听端口会被漏报,破坏端口页及项目 WebPort 关联。
+- **进程、项目与 Ecosystem API (62/C)** — src/api/routes/processes.rs:730: PATCH 更新将 req.cron、req.cwd、req.notify 和 req.log_alert 的 None 回填 existing_config;serde Option 无法区分字段省略与显式 null,因此无法清除已有 cron、CWD、通知覆盖或日志告警配置。
+- **前端 API、认证与轮询层 (68/C)** — web-ui/src/hooks/useSingleFlightPoll.ts:71: enabled=false 时调度器完全跳过首次 tick;useProcesses:31 与 useProjects:30 将用户的 autoRefresh 直接作为 enabled,因此关闭自动刷新或设置加载竞态下可能没有首次进程/项目请求,只能依赖手动 reload 才获得数据。
+- **构建、发布与工程文档 (72/C)** — web-ui/vite.config.ts:74: Vite 开发服务器绑定 0.0.0.0,并在 76-80 行将 /api(含 WebSocket)代理到本机 127.0.0.1:2999。LAN 客户端可访问 5173 的同源代理,导致 passwordless daemon 的本地控制面暴露;这与 docs/API.md:928、docs/CLI.md:388 的 loopback-only 假设不一致。
+- **React 应用壳与导航 (76/B)** — web-ui/src/App.tsx:81: Layout 从第81行延伸到第841行(约761行),同时持有 settings/processes/projects/health/通知查询、AI/终端/统计/开发工具状态、移动端焦点陷阱与侧栏副作用,并内嵌第722-817行的完整路由表及多个 overlay;本轮虽删除 StatusBar,应用壳仍承担过高的 core 变更耦合。
+- **前端设置、AI 与终端 (78/B)** — web-ui/src/components/TerminalPanel.tsx:131: TerminalPanel 当前约 1,136 行,仍在同一组件内处理 xterm 生命周期、WebSocket 建连/重试、输入与命令历史持久化、标签页/分屏、布局和快捷键;本轮虽删除 TerminalStatusBarBtn 并修正底部偏移,未见新增功能回归,但核心边界和维护回归面仍偏大。
+- **CLI、守护进程与 Web 入口 (78/B)** — src/utils/pid.rs:174: capture_process_identity(...)=None 会被 is_some_and 直接解释为 daemon 不在运行;write_pid_file 随后可能删除仍存活 daemon 的 PID 文件,旧进程继续监听而新实例因端口冲突启动失败,违背身份未知时 fail-closed 的不变量。
+
+## All findings
+
+### HIGH (6)
+
+- **前端进程与项目工作台** · `web-ui/src/components/EnvFileModal.tsx:142` — 保存并重启先调用 api.restartProcess(processId),随后调用 onRestart;ProcessDetailPage:395-400 传入的 onRestart=doRestart 会再次调用同一 API,导致一次用户操作连续重启两次,第二次调用还未被 await。
+- **前端观测与运维页面** · `web-ui/src/pages/TunnelsPage.tsx:513` — 首次渲染 tunnels 为空时 hasStarting=false,useSingleFlightPoll 的 enabled=false 不会执行初始 tick;当前没有独立初始 load,因而不会调用 GET /tunnels,loading 会一直为 true、空态被隐藏,必须手动点击刷新才恢复。整改前曾有 useEffect(() => { load() })。
+- **认证与本机系统能力 API** · `src/api/routes/ports.rs:248` — Linux 的 ss -Hntlpu 输出含 users 字段时,parse_line 先按 fields.len()>=7 进入 netstat 分支,把 fields[3] 的 RecvQ 当作本地地址,extract_port 返回 None 丢弃整行;ss 成功时不会回退到 netstat,因此 Linux 监听端口会被漏报,破坏端口页及项目 WebPort 关联。
+- **进程监督与日志核心** · `src/process/manager.rs:2323` — Cron 运行结束的 inline Exited 路径只切换 Sleeping、清空 PID/identity 和 log_writer,未调用 terminate_retained_process_tree;进程树 guard 已在 2508 放入 ManagedProcess,且 2455 已 preserve_on_drop,下一次运行覆盖旧 guard 时不会终止残留 descendants。
+- **进程监督与日志核心** · `src/process/runner.rs:280` — ProcessTreeGuard::new 失败时仅 child.kill()+wait 根进程,未按 Unix process group/Windows job 清理 descendants;hooks.rs:60-65 存在同样的根进程单独清理路径,树所有权建立失败可能留下失控子进程。
+- **构建、发布与工程文档** · `web-ui/vite.config.ts:74` — Vite 开发服务器绑定 0.0.0.0,并在 76-80 行将 /api(含 WebSocket)代理到本机 127.0.0.1:2999。LAN 客户端可访问 5173 的同源代理,导致 passwordless daemon 的本地控制面暴露;这与 docs/API.md:928、docs/CLI.md:388 的 loopback-only 假设不一致。
+
+### MED (33)
+
+- **React 应用壳与导航** · `web-ui/src/App.tsx:81` — Layout 从第81行延伸到第841行(约761行),同时持有 settings/processes/projects/health/通知查询、AI/终端/统计/开发工具状态、移动端焦点陷阱与侧栏副作用,并内嵌第722-817行的完整路由表及多个 overlay;本轮虽删除 StatusBar,应用壳仍承担过高的 core 变更耦合。
+- **前端进程与项目工作台** · `web-ui/src/pages/EditPage.tsx:165` — 编辑表单把空 cron、args、cwd、notify 转成省略字段;后端 update_process 对省略字段保留旧值,因此界面留空无法真正清除已有定时任务、参数、目录或通知配置。
+- **前端进程与项目工作台** · `web-ui/src/components/EnvFileModal.tsx:402` — 底部取消按钮直接执行 onClose,绕过 requestClose 的 dirty 确认与 mutationRef 保护;用户可在未保存时直接丢弃修改,保存进行中也可关闭弹窗。
+- **前端进程与项目工作台** · `web-ui/src/pages/ProcessesPage.tsx:1023` — 终端动作设置 disabled: !p.cwd,但两套菜单均未处理该字段;无 cwd 时按钮仍可点击,回调仅短路且无错误提示。
+- **前端进程与项目工作台** · `web-ui/src/pages/StartPage.tsx:143` — 环境文件保存期间仍允许切换和编辑,保存完成无 request/generation 校验就标记已保存;并发编辑会被错误标记为已保存,启动表单也未等待 envSaving。
+- **前端进程与项目工作台** · `web-ui/src/pages/ProcessesPage.tsx:64` — ProcessesPage、ProcessDetailPage、ProjectsPage 仍集中多类职责并复制动作/视图逻辑;EnvFileModal 与 EnvFilePanel 也重复加载、编辑、保存、同步状态流程,持续存在漂移风险。
+- **前端观测与运维页面** · `web-ui/src/pages/PortFinderPage.tsx:120` — 约 1,065 行页面同时承担端口扫描、进程匹配、过滤、分页、杀进程、隧道创建及多个内嵌渲染组件,仍是明显的 bloat/god-component,维护和回归风险较高。
+- **前端观测与运维页面** · `web-ui/src/pages/AnalyticsPage.tsx:1042` — AnalyticsPage 与 LogVolumePage:278 各自维护相近的批量日志统计拉取、并发限制、增量 map 合并、错误处理及 single-flight 轮询流程,日志统计适配和策略变更仍需改两份。
+- **前端观测与运维页面** · `web-ui/src/pages/NotifModal.tsx:10` — NotifModal:10 与 NotificationsPage:125 重复维护相同的 notification event 默认配置,新增或调整事件开关时存在漂移风险。
+- **前端观测与运维页面** · `web-ui/src/pages/PortFinderPage.tsx:232` — 状态过滤只匹配 p.state.toUpperCase() === LISTENING(快速隧道按钮在 :364 也如此),但非 Windows 后端直接返回 ss/netstat 的标准状态 LISTEN;Linux 监听端口因此无法被“监听中”筛选,也不会出现隧道按钮。
+- **前端观测与运维页面** · `web-ui/src/pages/LogLibraryPage.tsx:401` — 后端 list_log_dates 按 newest-first 返回日期,但这里使用 dates.slice(-3).reverse(),超过 3 天时显示最旧三天而不是最新三天;同时日期按钮在 :410 不传日期,:247 的 onView 仅导航到 /processes/:id,点击日期不会打开所选日期日志。
+- **前端观测与运维页面** · `web-ui/src/pages/AnalyticsPage.tsx:681` — 上游 :129-132 将 sleeping/starting 计入 NsRow.other,但 statusLabel 只判断 crashed/running,否则固定显示“全部已停止”;纯 sleeping/starting 命名空间会被错误标记为已停止。
+- **前端设置、AI 与终端** · `web-ui/src/components/TerminalPanel.tsx:131` — TerminalPanel 当前约 1,136 行,仍在同一组件内处理 xterm 生命周期、WebSocket 建连/重试、输入与命令历史持久化、标签页/分屏、布局和快捷键;本轮虽删除 TerminalStatusBarBtn 并修正底部偏移,未见新增功能回归,但核心边界和维护回归面仍偏大。
+- **前端 API、认证与轮询层** · `web-ui/src/hooks/useSingleFlightPoll.ts:71` — enabled=false 时调度器完全跳过首次 tick;useProcesses:31 与 useProjects:30 将用户的 autoRefresh 直接作为 enabled,因此关闭自动刷新或设置加载竞态下可能没有首次进程/项目请求,只能依赖手动 reload 才获得数据。
+- **前端 API、认证与轮询层** · `web-ui/src/hooks/useNotificationTray.ts:30` — INACTIVE 集合遗漏 stopping;当轮询观测到 running/watching → stopping → stopped 时,:86 的 ACTIVE.has(prevStatus) 条件不会成立,停止事件不会进入通知托盘。
+- **前端 API、认证与轮询层** · `web-ui/src/lib/api.ts:401` — 通用 request 直接将 JSON.parse 结果强制转换为 T;多个实际端点仍绕过运行时 schema,异常 2xx 响应可被当作合法成功数据继续进入页面。
+- **CLI、守护进程与 Web 入口** · `src/utils/pid.rs:174` — capture_process_identity(...)=None 会被 is_some_and 直接解释为 daemon 不在运行;write_pid_file 随后可能删除仍存活 daemon 的 PID 文件,旧进程继续监听而新实例因端口冲突启动失败,违背身份未知时 fail-closed 的不变量。
+- **CLI、守护进程与 Web 入口** · `src/utils/pid.rs:141` — 兼容 numeric PID 时将 start_time_secs 置零、executable 置空;daemon_record_matches_identity 会跳过启动时间并退回 current_exe。旧 PID 被复用给同路径普通 CLI 进程时可能误判为 daemon,阻塞启动/重启并削弱 PID reuse 防护。
+- **进程、项目与 Ecosystem API** · `src/api/routes/processes.rs:730` — PATCH 更新将 req.cron、req.cwd、req.notify 和 req.log_alert 的 None 回填 existing_config;serde Option 无法区分字段省略与显式 null,因此无法清除已有 cron、CWD、通知覆盖或日志告警配置。
+- **进程、项目与 Ecosystem API** · `src/api/routes/projects.rs:351` — 项目更新仅在 patch.web_port 为 Some 时写入;显式 null 与省略都落到 None,无法清除已有 web_port。effective_launch_uri 同样会把 None 回填旧值。
+- **进程、项目与 Ecosystem API** · `src/api/routes/ecosystem.rs:84` — Ecosystem 导入中项目关联失败且 delete 清理也失败时,仅尝试 save_to_disk 并拼接错误;若持久化也失败,未设置 background_persistence_error 或明确 persistence failed,可能留下运行中但未持久化的孤儿进程。
+- **进程、项目与 Ecosystem API** · `src/api/routes/ecosystem.rs:77` — 导入总时限只在每个 app 开始前检查,manager.start(app).await 未受 import_deadline/timeout 约束;单个启动阻塞时,批量接口可能超过声明的 5 分钟并长期持有 state_mutation_lock。
+- **进程、项目与 Ecosystem API** · `src/api/routes/processes.rs:110` — processes.rs 仍同时承载生命周期、日志、环境变量、通知、项目关联、克隆和命名空间批量操作;补偿事务虽完善,但文件级职责和回归面仍高。
+- **认证与本机系统能力 API** · `src/api/routes/ports.rs:180` — Windows netstat 返回所有连接而 Unix -l 仅返回监听 socket;API 注释和前端状态筛选宣称支持全部状态,跨平台语义不一致,且 Windows 可对非监听连接进入受管理进程停止流程。
+- **进程监督与日志核心** · `src/process/manager.rs:437` — 恢复已运行进程时先将 watch 配置映射为 Watching,FileWatcher::start 失败却只记录日志并继续,留下 status=Watching 但 file_watcher=None 的假监督状态;正常 do_spawn 路径则会清理并返回错误,恢复路径语义不一致。
+- **进程监督与日志核心** · `src/process/manager.rs:2183` — manager.rs 当前约 3431 行;cron_trigger_loop 在 2200-2669 重新复制 do_spawn_with_event_inner:1338 的 hooks、env、PID/tree、health 和提交流程,已与普通生命周期路径发生漂移并直接造成 Cron 树清理遗漏,维护回归面较大。
+- **配置、模型与 JSON 持久化** · `src/config/telegram_checkpoint.rs:57` — save 先读取当前 offset、执行单调性校验,再独立写入;没有覆盖检查到写入的锁。并发调用可同时读到旧 offset,较小 offset 后写覆盖较大 offset,导致 Telegram 更新重复消费。atomic_file 锁只覆盖单次文件操作。
+- **配置、模型与 JSON 持久化** · `src/models/project.rs:75` — ProjectPatch 的 web_port、launch_uri 等字段均为 Option;serde 会把字段缺失和显式 null 都解码为 None,模型没有 presence/clear 标记,PATCH 无法同时表达保持原值和清除值,项目端口或启动 URI 存在无法清除的状态风险。
+- **AI、通知、隧道与观测集成** · `src/api/routes/ai.rs:161` — ai.rs 仍约 1495 行,集中承载 AI 设置 CRUD、GitHub Device Flow、模型发现、多个 Provider 出站请求及 SSE/NDJSON 流式解析;当前未核实到新的 P0-P2 安全或功能回归,但该 god-file 仍有明确维护风险。
+- **Windows 桌面壳与托盘入口** · `desktop-shell/src/windows_app.rs:173` — 自启动初始化与托盘切换都静默丢弃错误:initialize_autostart 在 :181 仅判断 is_ok(),失败无提示;托盘 :296 用 unwrap_or(false),:298-301 对 enable/disable 仅判断结果,失败不更新勾选也不给用户诊断。登录自启动可能未生效而界面无可操作反馈。
+- **构建、发布与工程文档** · `sonar-project.properties:4` — Sonar 仅扫描 src 与 web-ui/src,完全不纳入 desktop-shell;quality.yml:190-193 只对桌面壳执行 cargo test/build,没有 fmt、clippy 或覆盖率,托盘、单实例和升级入口不受 Sonar quality gate 约束。
+- **构建、发布与工程文档** · `.github/workflows/quality.yml:143` — Linux package smoke 仅执行脚本语法检查、构建 deb 和 payload grep,没有 dpkg 安装、postinst/prerm/postrm、systemd 启停或健康探针;文档宣传的 Linux 服务安装生命周期仍未被 CI 验证。
+- **构建、发布与工程文档** · `.github/workflows/release.yml:234` — 正式 Windows release 的 package-installer 只编译、签名、哈希并上传安装包,没有对最终 release artifacts 执行安装、启动、升级、卸载 smoke;quality.yml 的安装测试使用另一套构建产物,不能完全证明实际发布包可用。
+
+### LOW (6)
+
+- **React 应用壳与导航** · `web-ui/src/components/ServerSwitcher.tsx:84` — ServerSwitcher 从第84行延伸到第704行(约621行),在单组件内同时处理本地/远程/SSH 配置、存储恢复与保存、活动服务器切换、隧道命令复制、弹层焦点和全部表单 UI;职责仍属同一领域且未见新增功能错误,但维护回归面偏大。
+- **前端进程与项目工作台** · `web-ui/src/components/FolderBrowser.tsx:94` — 面向跨平台项目的面包屑始终用反斜杠拼接,Unix 路径点击后会生成错误目录。
+- **前端 API、认证与轮询层** · `web-ui/src/lib/api.ts:493` — api 对象仍在约 1,342 行单文件中聚合多个领域端点;统一 transport 已存在,但按领域拆分可降低维护成本。
+- **CLI、守护进程与 Web 入口** · `src/lib.rs:204` — Web 入口直接拼接 host;--host ::1 会生成缺少 IPv6 authority 方括号的 URL,浏览器启动失败。
+- **认证与本机系统能力 API** · `src/api/routes/system.rs:84` — system.rs 仍同时承载健康、状态保存恢复、受限 env 文件、统计、重启和打开目录等多个系统边界。
+- **认证与本机系统能力 API** · `src/config/env_file.rs:37` — env 路径已限制到注册 cwd、直接子文件并拒绝最终符号链接;canonicalize 与后续 I/O 间仍有同用户恶意替换父目录的 TOCTOU 窗口。
+
+## Cross-cutting themes
+
+- **P0 安全边界总体保持,但开发代理存在旁路.** 生产 daemon 的回环、鉴权、CORS 与出站策略仍有效;Vite 开发服务绑定 0.0.0.0 并代理本机 API,密码关闭时会把控制面间接暴露给局域网。
+- **进程生命周期仍有未合流的 Cron 分支.** Cron 路径复制了普通 spawn/commit 流程,退出与树所有权失败时可能只处理根进程或遗留 descendants;恢复 watcher 也可能显示假 Watching 状态。
+- **前端整改引入了可复现的交互回归.** 条件化 single-flight 轮询可让页面永远不做首次加载,env 保存并重启会重复请求;Linux 端口解析还会丢弃常见 ss 输出。
+- **PATCH 与检查点的一致性边界仍不完整.** 进程和项目补丁无法区分字段缺失与显式清空,Telegram checkpoint 的单调性检查与写入没有同一把锁,极端并发下可能回退。
+- **桌面壳已成为正式入口但质量门禁未完全覆盖.** 桌面壳有独立测试和 Windows 构建 smoke,但未进入 Sonar、覆盖率与独立 clippy/fmt 门禁;Linux 包和正式 release artifact 的安装生命周期也缺少端到端验证。
+- **停止大范围洁癖重构,优先关闭少量运行语义缺陷.** 单体 Rust daemon + React 控制台仍适合产品规模;大文件和重复代码只在已经造成具体缺陷的生命周期与轮询路径上做窄修,不建议全面拆分或引入微服务。
diff --git a/.codemap/config.json b/.codemap/config.json
new file mode 100644
index 0000000..d63b8e7
--- /dev/null
+++ b/.codemap/config.json
@@ -0,0 +1 @@
+{"lang":"zh","project":"RunDock / Alter","subtitle":"Rust + React 本地开发进程管理器工程地图与质量审计","outputDir":".codemap","htmlFile":"codemap.html","mdFile":"codemap.md"}
diff --git a/.codemap/modules.json b/.codemap/modules.json
new file mode 100644
index 0000000..79b7613
--- /dev/null
+++ b/.codemap/modules.json
@@ -0,0 +1,822 @@
+{
+ "meta": {
+ "project": "RunDock / Alter",
+ "lang": "zh",
+ "subtitle": "Rust + React 本地开发进程管理器工程地图与质量审计",
+ "generatedAt": "2026-08-30",
+ "htmlPath": ".codemap/codemap.html",
+ "mdPath": ".codemap/codemap.md",
+ "spineDesc": "用户可从 Windows 桌面壳或浏览器进入 React 控制台,经统一 API 客户端和 Axum 鉴权路由进入进程监督核心,再把运行状态与项目元数据持久化为本地 JSON 快照。",
+ "tracked_loc": 83436,
+ "tracked_files": 236,
+ "rev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ "bands": [
+ {
+ "id": "ui-shell",
+ "tier": "fe",
+ "t": "前端 · 应用壳",
+ "d": "导航、认证入口与全局界面状态"
+ },
+ {
+ "id": "ui-features",
+ "tier": "fe",
+ "t": "前端 · 功能页面",
+ "d": "进程、项目、观测与设置交互"
+ },
+ {
+ "id": "ui-infra",
+ "tier": "fe",
+ "t": "前端 · 数据访问",
+ "d": "HTTP、认证令牌、服务器选择与轮询 hooks"
+ },
+ {
+ "id": "wire",
+ "wire": true,
+ "t": "◀ HTTP · SSE · WebSocket ▶"
+ },
+ {
+ "id": "api",
+ "tier": "be",
+ "t": "后端 · API 与系统边界",
+ "d": "Axum 路由、认证、文件系统和外部能力"
+ },
+ {
+ "id": "core",
+ "tier": "be",
+ "t": "后端 · 进程运行核心",
+ "d": "启动、停止、重启、健康检查、计划任务与日志"
+ },
+ {
+ "id": "state",
+ "tier": "be",
+ "t": "后端 · 状态与集成",
+ "d": "JSON 持久化、数据模型、通知与第三方服务"
+ },
+ {
+ "id": "delivery",
+ "tier": "ops",
+ "t": "交付 · 构建与文档",
+ "d": "依赖、CI、安装、发布和工程文档"
+ }
+ ],
+ "spine": [
+ "desktop_shell",
+ "frontend_shell",
+ "frontend_process_projects",
+ "frontend_transport",
+ "api_process_projects",
+ "process_lifecycle",
+ "state_persistence"
+ ],
+ "reportThemes": [
+ [
+ "P0 安全边界总体保持,但开发代理存在旁路",
+ "生产 daemon 的回环、鉴权、CORS 与出站策略仍有效;Vite 开发服务绑定 0.0.0.0 并代理本机 API,密码关闭时会把控制面间接暴露给局域网。"
+ ],
+ [
+ "进程生命周期仍有未合流的 Cron 分支",
+ "Cron 路径复制了普通 spawn/commit 流程,退出与树所有权失败时可能只处理根进程或遗留 descendants;恢复 watcher 也可能显示假 Watching 状态。"
+ ],
+ [
+ "前端整改引入了可复现的交互回归",
+ "条件化 single-flight 轮询可让页面永远不做首次加载,env 保存并重启会重复请求;Linux 端口解析还会丢弃常见 ss 输出。"
+ ],
+ [
+ "PATCH 与检查点的一致性边界仍不完整",
+ "进程和项目补丁无法区分字段缺失与显式清空,Telegram checkpoint 的单调性检查与写入没有同一把锁,极端并发下可能回退。"
+ ],
+ [
+ "桌面壳已成为正式入口但质量门禁未完全覆盖",
+ "桌面壳有独立测试和 Windows 构建 smoke,但未进入 Sonar、覆盖率与独立 clippy/fmt 门禁;Linux 包和正式 release artifact 的安装生命周期也缺少端到端验证。"
+ ],
+ [
+ "停止大范围洁癖重构,优先关闭少量运行语义缺陷",
+ "单体 Rust daemon + React 控制台仍适合产品规模;大文件和重复代码只在已经造成具体缺陷的生命周期与轮询路径上做窄修,不建议全面拆分或引入微服务。"
+ ]
+ ],
+ "modules": [
+ {
+ "id": "frontend_shell",
+ "label": "React 应用壳与导航",
+ "band": "ui-shell",
+ "path": "web-ui/src/App.tsx",
+ "paths": [
+ "web-ui/src/App.tsx",
+ "web-ui/src/App.test.tsx",
+ "web-ui/src/main.tsx",
+ "web-ui/src/index.css",
+ "web-ui/src/components/GitHubStarBanner.tsx",
+ "web-ui/src/components/AppErrorBoundary.tsx",
+ "web-ui/src/components/AppSidebar.tsx",
+ "web-ui/src/components/AppSidebar.test.tsx",
+ "web-ui/src/components/AuthGuard.tsx",
+ "web-ui/src/components/ServerSwitcher.tsx",
+ "web-ui/src/components/ServerSwitcher.test.tsx",
+ "web-ui/src/components/StatusBar.tsx",
+ "web-ui/src/components/SystemStatsWidget.tsx"
+ ],
+ "coupling": "core",
+ "deps": [
+ "frontend_process_projects",
+ "frontend_operations",
+ "frontend_settings_ai_terminal",
+ "frontend_transport"
+ ],
+ "desc": "承载路由、侧栏、服务器切换、认证门禁、全局状态和页面装配。",
+ "loc": 3690,
+ "contentHash": "f8427001d9958f2eb15d7aeff13408631df36bc5760aa1ccc02b19f2c7cb1702",
+ "score": 76,
+ "grade": "B",
+ "tags": [
+ "bloat",
+ "god-component"
+ ],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/App.tsx:81",
+ "text": "Layout 从第81行延伸到第841行(约761行),同时持有 settings/processes/projects/health/通知查询、AI/终端/统计/开发工具状态、移动端焦点陷阱与侧栏副作用,并内嵌第722-817行的完整路由表及多个 overlay;本轮虽删除 StatusBar,应用壳仍承担过高的 core 变更耦合。"
+ },
+ {
+ "sev": "LOW",
+ "loc": "web-ui/src/components/ServerSwitcher.tsx:84",
+ "text": "ServerSwitcher 从第84行延伸到第704行(约621行),在单组件内同时处理本地/远程/SSH 配置、存储恢复与保存、活动服务器切换、隧道命令复制、弹层焦点和全部表单 UI;职责仍属同一领域且未见新增功能错误,但维护回归面偏大。"
+ }
+ ],
+ "auditedHash": "f8427001d9958f2eb15d7aeff13408631df36bc5760aa1ccc02b19f2c7cb1702",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "frontend_process_projects",
+ "label": "前端进程与项目工作台",
+ "band": "ui-features",
+ "path": "web-ui/src/pages/ProcessesPage.tsx 等",
+ "paths": [
+ "web-ui/src/pages/ProcessesPage.tsx",
+ "web-ui/src/pages/ProcessDetailPage.tsx",
+ "web-ui/src/pages/ProjectsPage.tsx",
+ "web-ui/src/pages/StartPage.tsx",
+ "web-ui/src/pages/EditPage.tsx",
+ "web-ui/src/pages/CronJobsPage.tsx",
+ "web-ui/src/pages/CreateCronJobPage.tsx",
+ "web-ui/src/components/CronExpressionInput.tsx",
+ "web-ui/src/components/DesktopLaunchButton.tsx",
+ "web-ui/src/components/WebPortButton.tsx",
+ "web-ui/src/components/NamespaceInput.tsx",
+ "web-ui/src/components/RunOutput.tsx",
+ "web-ui/src/components/FolderBrowser.tsx",
+ "web-ui/src/components/EnvEditor.tsx",
+ "web-ui/src/components/EnvFileModal.tsx",
+ "web-ui/src/components/EnvFilePanel.tsx",
+ "web-ui/src/components/CodeEditor.tsx",
+ "web-ui/src/components/FormLayout.tsx",
+ "web-ui/src/pages/formStyles.ts"
+ ],
+ "coupling": "high",
+ "deps": [
+ "frontend_transport",
+ "api_process_projects"
+ ],
+ "desc": "实现进程、逻辑项目、计划任务、日志详情和环境文件的主要用户操作流程。",
+ "tests": [
+ "web-ui/src/pages/ProjectsPage.test.tsx",
+ "web-ui/src/components/WebPortButton.test.tsx",
+ "web-ui/src/components/DesktopLaunchButton.test.tsx"
+ ],
+ "loc": 9425,
+ "contentHash": "4f1cacce35a382137c13ff820ef7d7f5977383cb4e706aedfbfab7a3496850ce",
+ "score": 52,
+ "grade": "D",
+ "tags": [
+ "bloat",
+ "god-component",
+ "duplication",
+ "fallback",
+ "over-fit"
+ ],
+ "findings": [
+ {
+ "sev": "HIGH",
+ "loc": "web-ui/src/components/EnvFileModal.tsx:142",
+ "text": "保存并重启先调用 api.restartProcess(processId),随后调用 onRestart;ProcessDetailPage:395-400 传入的 onRestart=doRestart 会再次调用同一 API,导致一次用户操作连续重启两次,第二次调用还未被 await。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/EditPage.tsx:165",
+ "text": "编辑表单把空 cron、args、cwd、notify 转成省略字段;后端 update_process 对省略字段保留旧值,因此界面留空无法真正清除已有定时任务、参数、目录或通知配置。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/components/EnvFileModal.tsx:402",
+ "text": "底部取消按钮直接执行 onClose,绕过 requestClose 的 dirty 确认与 mutationRef 保护;用户可在未保存时直接丢弃修改,保存进行中也可关闭弹窗。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/ProcessesPage.tsx:1023",
+ "text": "终端动作设置 disabled: !p.cwd,但两套菜单均未处理该字段;无 cwd 时按钮仍可点击,回调仅短路且无错误提示。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/StartPage.tsx:143",
+ "text": "环境文件保存期间仍允许切换和编辑,保存完成无 request/generation 校验就标记已保存;并发编辑会被错误标记为已保存,启动表单也未等待 envSaving。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/ProcessesPage.tsx:64",
+ "text": "ProcessesPage、ProcessDetailPage、ProjectsPage 仍集中多类职责并复制动作/视图逻辑;EnvFileModal 与 EnvFilePanel 也重复加载、编辑、保存、同步状态流程,持续存在漂移风险。"
+ },
+ {
+ "sev": "LOW",
+ "loc": "web-ui/src/components/FolderBrowser.tsx:94",
+ "text": "面向跨平台项目的面包屑始终用反斜杠拼接,Unix 路径点击后会生成错误目录。"
+ }
+ ],
+ "auditedHash": "4f1cacce35a382137c13ff820ef7d7f5977383cb4e706aedfbfab7a3496850ce",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "frontend_operations",
+ "label": "前端观测与运维页面",
+ "band": "ui-features",
+ "path": "web-ui/src/pages/AnalyticsPage.tsx 等",
+ "paths": [
+ "web-ui/src/pages/AnalyticsPage.tsx",
+ "web-ui/src/pages/LogLibraryPage.tsx",
+ "web-ui/src/pages/LogVolumePage.tsx",
+ "web-ui/src/pages/NotificationsPage.tsx",
+ "web-ui/src/pages/PortFinderPage.tsx",
+ "web-ui/src/pages/TunnelsPage.tsx",
+ "web-ui/src/components/NotificationTray.tsx",
+ "web-ui/src/components/NotifModal.tsx",
+ "web-ui/src/components/DiscordIcon.tsx"
+ ],
+ "coupling": "med",
+ "deps": [
+ "frontend_transport",
+ "integrations_observability"
+ ],
+ "desc": "展示指标、日志容量、通知、端口和隧道状态并触发相关运维动作。",
+ "loc": 6328,
+ "contentHash": "8b04f5a9e8fd4844e2889778872a8ffd1225af3d1b73dcb1887018f8c08fab75",
+ "score": 52,
+ "grade": "D",
+ "tags": [
+ "bloat",
+ "duplication",
+ "god-component"
+ ],
+ "findings": [
+ {
+ "sev": "HIGH",
+ "loc": "web-ui/src/pages/TunnelsPage.tsx:513",
+ "text": "首次渲染 tunnels 为空时 hasStarting=false,useSingleFlightPoll 的 enabled=false 不会执行初始 tick;当前没有独立初始 load,因而不会调用 GET /tunnels,loading 会一直为 true、空态被隐藏,必须手动点击刷新才恢复。整改前曾有 useEffect(() => { load() })。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/PortFinderPage.tsx:120",
+ "text": "约 1,065 行页面同时承担端口扫描、进程匹配、过滤、分页、杀进程、隧道创建及多个内嵌渲染组件,仍是明显的 bloat/god-component,维护和回归风险较高。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/AnalyticsPage.tsx:1042",
+ "text": "AnalyticsPage 与 LogVolumePage:278 各自维护相近的批量日志统计拉取、并发限制、增量 map 合并、错误处理及 single-flight 轮询流程,日志统计适配和策略变更仍需改两份。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/NotifModal.tsx:10",
+ "text": "NotifModal:10 与 NotificationsPage:125 重复维护相同的 notification event 默认配置,新增或调整事件开关时存在漂移风险。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/PortFinderPage.tsx:232",
+ "text": "状态过滤只匹配 p.state.toUpperCase() === LISTENING(快速隧道按钮在 :364 也如此),但非 Windows 后端直接返回 ss/netstat 的标准状态 LISTEN;Linux 监听端口因此无法被“监听中”筛选,也不会出现隧道按钮。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/LogLibraryPage.tsx:401",
+ "text": "后端 list_log_dates 按 newest-first 返回日期,但这里使用 dates.slice(-3).reverse(),超过 3 天时显示最旧三天而不是最新三天;同时日期按钮在 :410 不传日期,:247 的 onView 仅导航到 /processes/:id,点击日期不会打开所选日期日志。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/pages/AnalyticsPage.tsx:681",
+ "text": "上游 :129-132 将 sleeping/starting 计入 NsRow.other,但 statusLabel 只判断 crashed/running,否则固定显示“全部已停止”;纯 sleeping/starting 命名空间会被错误标记为已停止。"
+ }
+ ],
+ "auditedHash": "8b04f5a9e8fd4844e2889778872a8ffd1225af3d1b73dcb1887018f8c08fab75",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "frontend_settings_ai_terminal",
+ "label": "前端设置、AI 与终端",
+ "band": "ui-features",
+ "path": "web-ui/src/components/settings 与 TerminalPanel",
+ "paths": [
+ "web-ui/src/pages/SettingsPage.tsx",
+ "web-ui/src/pages/LoginPage.tsx",
+ "web-ui/src/components/settings/**/*.tsx",
+ "web-ui/src/components/AiPanel.tsx",
+ "web-ui/src/components/TerminalPanel.tsx",
+ "web-ui/src/components/Dialog.tsx"
+ ],
+ "coupling": "high",
+ "deps": [
+ "frontend_transport",
+ "api_security_os",
+ "integrations_observability"
+ ],
+ "desc": "管理安全、AI Provider、通知、隧道和终端设置,并提供浏览器 PTY/AI 交互。",
+ "loc": 6489,
+ "contentHash": "6cc7c34c0c41cb4144b440233ef4fb0f90ce9f23305ac078a8be3883cb57b9bc",
+ "score": 78,
+ "grade": "B",
+ "tags": [
+ "bloat",
+ "god-component"
+ ],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/components/TerminalPanel.tsx:131",
+ "text": "TerminalPanel 当前约 1,136 行,仍在同一组件内处理 xterm 生命周期、WebSocket 建连/重试、输入与命令历史持久化、标签页/分屏、布局和快捷键;本轮虽删除 TerminalStatusBarBtn 并修正底部偏移,未见新增功能回归,但核心边界和维护回归面仍偏大。"
+ }
+ ],
+ "auditedHash": "6cc7c34c0c41cb4144b440233ef4fb0f90ce9f23305ac078a8be3883cb57b9bc",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "frontend_transport",
+ "label": "前端 API、认证与轮询层",
+ "band": "ui-infra",
+ "path": "web-ui/src/lib 与 web-ui/src/hooks",
+ "paths": [
+ "web-ui/src/lib/**/*.ts",
+ "web-ui/src/hooks/**/*.ts",
+ "web-ui/src/types.ts"
+ ],
+ "coupling": "core",
+ "deps": [
+ "api_process_projects",
+ "api_security_os",
+ "integrations_observability"
+ ],
+ "desc": "集中封装 REST/SSE 请求、Bearer 会话、服务器配置和进程/健康轮询。",
+ "tests": [
+ "web-ui/src/lib/projects.test.ts",
+ "web-ui/src/lib/processWeb.test.ts"
+ ],
+ "loc": 3820,
+ "contentHash": "c8fc2adc97e66afa42f1dce9d7e5ad6d1ed93b5d84e198556bb2820997ea5896",
+ "score": 68,
+ "grade": "C",
+ "tags": [
+ "bloat",
+ "any-escape"
+ ],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/hooks/useSingleFlightPoll.ts:71",
+ "text": "enabled=false 时调度器完全跳过首次 tick;useProcesses:31 与 useProjects:30 将用户的 autoRefresh 直接作为 enabled,因此关闭自动刷新或设置加载竞态下可能没有首次进程/项目请求,只能依赖手动 reload 才获得数据。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/hooks/useNotificationTray.ts:30",
+ "text": "INACTIVE 集合遗漏 stopping;当轮询观测到 running/watching → stopping → stopped 时,:86 的 ACTIVE.has(prevStatus) 条件不会成立,停止事件不会进入通知托盘。"
+ },
+ {
+ "sev": "MED",
+ "loc": "web-ui/src/lib/api.ts:401",
+ "text": "通用 request 直接将 JSON.parse 结果强制转换为 T;多个实际端点仍绕过运行时 schema,异常 2xx 响应可被当作合法成功数据继续进入页面。"
+ },
+ {
+ "sev": "LOW",
+ "loc": "web-ui/src/lib/api.ts:493",
+ "text": "api 对象仍在约 1,342 行单文件中聚合多个领域端点;统一 transport 已存在,但按领域拆分可降低维护成本。"
+ }
+ ],
+ "auditedHash": "c8fc2adc97e66afa42f1dce9d7e5ad6d1ed93b5d84e198556bb2820997ea5896",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "bootstrap_daemon",
+ "label": "CLI、守护进程与 Web 入口",
+ "band": "api",
+ "path": "src/main.rs、src/lib.rs、src/daemon、src/cli",
+ "paths": [
+ "src/main.rs",
+ "src/lib.rs",
+ "src/cli/**/*.rs",
+ "src/client/**/*.rs",
+ "src/daemon/mod.rs",
+ "src/daemon/server.rs",
+ "src/daemon/signals.rs",
+ "src/web/**/*.rs",
+ "src/utils/**/*.rs"
+ ],
+ "coupling": "core",
+ "deps": [
+ "api_security_os",
+ "api_process_projects",
+ "state_persistence",
+ "process_lifecycle"
+ ],
+ "desc": "解析 CLI、派生后台守护进程、恢复状态、组装 Axum/嵌入式前端并处理关闭信号。",
+ "tests": [
+ "tests/integration/daemon_lifecycle_test.rs"
+ ],
+ "loc": 3184,
+ "contentHash": "da5ee5a92cdb7598fea2bbe37bd0ca21eb855962c0f2fffc3613453e86108e77",
+ "score": 78,
+ "grade": "B",
+ "tags": [
+ "fallback",
+ "legacy",
+ "glue"
+ ],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "src/utils/pid.rs:174",
+ "text": "capture_process_identity(...)=None 会被 is_some_and 直接解释为 daemon 不在运行;write_pid_file 随后可能删除仍存活 daemon 的 PID 文件,旧进程继续监听而新实例因端口冲突启动失败,违背身份未知时 fail-closed 的不变量。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/utils/pid.rs:141",
+ "text": "兼容 numeric PID 时将 start_time_secs 置零、executable 置空;daemon_record_matches_identity 会跳过启动时间并退回 current_exe。旧 PID 被复用给同路径普通 CLI 进程时可能误判为 daemon,阻塞启动/重启并削弱 PID reuse 防护。"
+ },
+ {
+ "sev": "LOW",
+ "loc": "src/lib.rs:204",
+ "text": "Web 入口直接拼接 host;--host ::1 会生成缺少 IPv6 authority 方括号的 URL,浏览器启动失败。"
+ }
+ ],
+ "auditedHash": "da5ee5a92cdb7598fea2bbe37bd0ca21eb855962c0f2fffc3613453e86108e77",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "api_process_projects",
+ "label": "进程、项目与 Ecosystem API",
+ "band": "api",
+ "path": "src/api/routes/processes.rs、projects.rs、ecosystem.rs",
+ "paths": [
+ "src/api/routes/processes.rs",
+ "src/api/routes/projects.rs",
+ "src/api/routes/ecosystem.rs"
+ ],
+ "coupling": "core",
+ "deps": [
+ "process_lifecycle",
+ "state_persistence",
+ "integrations_observability"
+ ],
+ "desc": "把进程 CRUD、批量命名空间操作、逻辑项目聚合和 Ecosystem 导入映射到运行核心。",
+ "loc": 2288,
+ "contentHash": "1d2b9cfa1a5e454463b5d3a11135c07afb4242e54993b1f89a3a9a04c4e54552",
+ "score": 62,
+ "grade": "C",
+ "tags": [
+ "bloat"
+ ],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/processes.rs:730",
+ "text": "PATCH 更新将 req.cron、req.cwd、req.notify 和 req.log_alert 的 None 回填 existing_config;serde Option 无法区分字段省略与显式 null,因此无法清除已有 cron、CWD、通知覆盖或日志告警配置。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/projects.rs:351",
+ "text": "项目更新仅在 patch.web_port 为 Some 时写入;显式 null 与省略都落到 None,无法清除已有 web_port。effective_launch_uri 同样会把 None 回填旧值。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/ecosystem.rs:84",
+ "text": "Ecosystem 导入中项目关联失败且 delete 清理也失败时,仅尝试 save_to_disk 并拼接错误;若持久化也失败,未设置 background_persistence_error 或明确 persistence failed,可能留下运行中但未持久化的孤儿进程。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/ecosystem.rs:77",
+ "text": "导入总时限只在每个 app 开始前检查,manager.start(app).await 未受 import_deadline/timeout 约束;单个启动阻塞时,批量接口可能超过声明的 5 分钟并长期持有 state_mutation_lock。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/processes.rs:110",
+ "text": "processes.rs 仍同时承载生命周期、日志、环境变量、通知、项目关联、克隆和命名空间批量操作;补偿事务虽完善,但文件级职责和回归面仍高。"
+ }
+ ],
+ "auditedHash": "1d2b9cfa1a5e454463b5d3a11135c07afb4242e54993b1f89a3a9a04c4e54552",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "api_security_os",
+ "label": "认证与本机系统能力 API",
+ "band": "api",
+ "path": "src/api/middleware.rs 与 system/update/scripts/terminal 路由",
+ "paths": [
+ "src/api/mod.rs",
+ "src/api/error.rs",
+ "src/api/middleware.rs",
+ "src/api/routes/mod.rs",
+ "src/api/routes/auth.rs",
+ "src/api/routes/system.rs",
+ "src/api/routes/update.rs",
+ "src/api/routes/scripts.rs",
+ "src/api/routes/ports.rs",
+ "src/api/routes/git.rs",
+ "src/api/routes/terminal.rs",
+ "src/api/routes/terminal_history.rs",
+ "src/api/routes/ui_settings.rs",
+ "src/terminal/**/*.rs"
+ ],
+ "coupling": "core",
+ "deps": [
+ "state_persistence",
+ "process_lifecycle"
+ ],
+ "desc": "执行认证授权、文件系统、脚本、端口、PTY 终端、自更新和守护进程控制等高权限本机操作。",
+ "loc": 5164,
+ "contentHash": "7756548c1935bb9840980a940d1bde7b8b64d7a6fbc4596768f4f9a0c365eade",
+ "score": 56,
+ "grade": "D",
+ "tags": [
+ "bloat",
+ "over-fit"
+ ],
+ "findings": [
+ {
+ "sev": "HIGH",
+ "loc": "src/api/routes/ports.rs:248",
+ "text": "Linux 的 ss -Hntlpu 输出含 users 字段时,parse_line 先按 fields.len()>=7 进入 netstat 分支,把 fields[3] 的 RecvQ 当作本地地址,extract_port 返回 None 丢弃整行;ss 成功时不会回退到 netstat,因此 Linux 监听端口会被漏报,破坏端口页及项目 WebPort 关联。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/ports.rs:180",
+ "text": "Windows netstat 返回所有连接而 Unix -l 仅返回监听 socket;API 注释和前端状态筛选宣称支持全部状态,跨平台语义不一致,且 Windows 可对非监听连接进入受管理进程停止流程。"
+ },
+ {
+ "sev": "LOW",
+ "loc": "src/api/routes/system.rs:84",
+ "text": "system.rs 仍同时承载健康、状态保存恢复、受限 env 文件、统计、重启和打开目录等多个系统边界。"
+ },
+ {
+ "sev": "LOW",
+ "loc": "src/config/env_file.rs:37",
+ "text": "env 路径已限制到注册 cwd、直接子文件并拒绝最终符号链接;canonicalize 与后续 I/O 间仍有同用户恶意替换父目录的 TOCTOU 窗口。"
+ }
+ ],
+ "auditedHash": "7756548c1935bb9840980a940d1bde7b8b64d7a6fbc4596768f4f9a0c365eade",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "process_lifecycle",
+ "label": "进程监督与日志核心",
+ "band": "core",
+ "path": "src/process 与 src/logging",
+ "paths": [
+ "src/process/**/*.rs",
+ "src/logging/**/*.rs"
+ ],
+ "coupling": "core",
+ "deps": [
+ "state_persistence",
+ "integrations_observability"
+ ],
+ "desc": "负责子进程启动、PID 管理、状态机、自动重启、cron、健康检查、文件监听、日志写入和指标采样。",
+ "tests": [
+ "tests/unit/restarter_test.rs"
+ ],
+ "loc": 6380,
+ "contentHash": "4bce75257bde2081e60c1acab6e91ad8036da34c5fe66ff614e2664b5bad2d66",
+ "score": 52,
+ "grade": "D",
+ "tags": [
+ "bloat",
+ "god-component",
+ "duplication"
+ ],
+ "findings": [
+ {
+ "sev": "HIGH",
+ "loc": "src/process/manager.rs:2323",
+ "text": "Cron 运行结束的 inline Exited 路径只切换 Sleeping、清空 PID/identity 和 log_writer,未调用 terminate_retained_process_tree;进程树 guard 已在 2508 放入 ManagedProcess,且 2455 已 preserve_on_drop,下一次运行覆盖旧 guard 时不会终止残留 descendants。"
+ },
+ {
+ "sev": "HIGH",
+ "loc": "src/process/runner.rs:280",
+ "text": "ProcessTreeGuard::new 失败时仅 child.kill()+wait 根进程,未按 Unix process group/Windows job 清理 descendants;hooks.rs:60-65 存在同样的根进程单独清理路径,树所有权建立失败可能留下失控子进程。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/process/manager.rs:437",
+ "text": "恢复已运行进程时先将 watch 配置映射为 Watching,FileWatcher::start 失败却只记录日志并继续,留下 status=Watching 但 file_watcher=None 的假监督状态;正常 do_spawn 路径则会清理并返回错误,恢复路径语义不一致。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/process/manager.rs:2183",
+ "text": "manager.rs 当前约 3431 行;cron_trigger_loop 在 2200-2669 重新复制 do_spawn_with_event_inner:1338 的 hooks、env、PID/tree、health 和提交流程,已与普通生命周期路径发生漂移并直接造成 Cron 树清理遗漏,维护回归面较大。"
+ }
+ ],
+ "auditedHash": "4bce75257bde2081e60c1acab6e91ad8036da34c5fe66ff614e2664b5bad2d66",
+ "auditedAt": "2026-08-30",
+ "auditedRev": ""
+ },
+ {
+ "id": "state_persistence",
+ "label": "配置、模型与 JSON 持久化",
+ "band": "state",
+ "path": "src/daemon/state.rs、src/config、src/models",
+ "paths": [
+ "src/daemon/state.rs",
+ "src/config/**/*.rs",
+ "src/models/**/*.rs"
+ ],
+ "coupling": "core",
+ "deps": [
+ "process_lifecycle",
+ "integrations_observability"
+ ],
+ "desc": "定义跨层数据模型并把进程、项目、认证、通知、Telegram、隧道和 AI 配置保存到本地文件。",
+ "tests": [
+ "tests/unit/config_parse_test.rs"
+ ],
+ "loc": 4929,
+ "contentHash": "3ab64d03ff39195c7dea925ad47ae17ef0d9e3fc357ac689cb0c9e41a0955b67",
+ "score": 84,
+ "grade": "B",
+ "tags": [
+ "clean"
+ ],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "src/config/telegram_checkpoint.rs:57",
+ "text": "save 先读取当前 offset、执行单调性校验,再独立写入;没有覆盖检查到写入的锁。并发调用可同时读到旧 offset,较小 offset 后写覆盖较大 offset,导致 Telegram 更新重复消费。atomic_file 锁只覆盖单次文件操作。"
+ },
+ {
+ "sev": "MED",
+ "loc": "src/models/project.rs:75",
+ "text": "ProjectPatch 的 web_port、launch_uri 等字段均为 Option;serde 会把字段缺失和显式 null 都解码为 None,模型没有 presence/clear 标记,PATCH 无法同时表达保持原值和清除值,项目端口或启动 URI 存在无法清除的状态风险。"
+ }
+ ],
+ "auditedHash": "3ab64d03ff39195c7dea925ad47ae17ef0d9e3fc357ac689cb0c9e41a0955b67",
+ "auditedAt": "2026-08-30",
+ "auditedRev": ""
+ },
+ {
+ "id": "integrations_observability",
+ "label": "AI、通知、隧道与观测集成",
+ "band": "state",
+ "path": "src/api/routes/ai.rs、src/notifications、src/telegram、src/tunnel",
+ "paths": [
+ "src/api/routes/ai.rs",
+ "src/api/routes/ai_context.rs",
+ "src/api/routes/notifications.rs",
+ "src/api/routes/telegram.rs",
+ "src/api/routes/tunnels.rs",
+ "src/api/routes/log_alerts.rs",
+ "src/api/routes/logs.rs",
+ "src/api/routes/metrics.rs",
+ "src/notifications/**/*.rs",
+ "src/telegram/**/*.rs",
+ "src/tunnel/**/*.rs"
+ ],
+ "coupling": "high",
+ "deps": [
+ "state_persistence",
+ "process_lifecycle"
+ ],
+ "desc": "对接 AI Provider、Webhook/Telegram 通知、Cloudflare/ngrok 隧道以及日志/指标查询。",
+ "loc": 6751,
+ "contentHash": "1bfddbc8a44bb7c0db9b0893bda5f346e15a9e690540c08b09d4f8df2b8dc335",
+ "score": 84,
+ "grade": "B",
+ "tags": [
+ "bloat"
+ ],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "src/api/routes/ai.rs:161",
+ "text": "ai.rs 仍约 1495 行,集中承载 AI 设置 CRUD、GitHub Device Flow、模型发现、多个 Provider 出站请求及 SSE/NDJSON 流式解析;当前未核实到新的 P0-P2 安全或功能回归,但该 god-file 仍有明确维护风险。"
+ }
+ ],
+ "auditedHash": "1bfddbc8a44bb7c0db9b0893bda5f346e15a9e690540c08b09d4f8df2b8dc335",
+ "auditedAt": "2026-08-30",
+ "auditedRev": ""
+ },
+ {
+ "id": "desktop_shell",
+ "label": "Windows 桌面壳与托盘入口",
+ "band": "ui-shell",
+ "path": "desktop-shell",
+ "paths": [
+ "desktop-shell/Cargo.toml",
+ "desktop-shell/Cargo.lock",
+ "desktop-shell/build.rs",
+ "desktop-shell/tauri.conf.json",
+ "desktop-shell/capabilities/**/*.json",
+ "desktop-shell/src/**/*.rs",
+ "desktop-shell/ui/**/*"
+ ],
+ "coupling": "high",
+ "deps": [
+ "bootstrap_daemon",
+ "frontend_shell"
+ ],
+ "desc": "提供 Windows 单实例桌面窗口、系统托盘、daemon 启动与恢复,以及内嵌 RunDock 控制台入口。",
+ "loc": 7596,
+ "contentHash": "8d5a7440b8474d0725105759e4b62bcd3d2c1e05089814adf5d18c18ddca0248",
+ "score": 82,
+ "grade": "B",
+ "tags": [
+ "silent-except"
+ ],
+ "findings": [
+ {
+ "sev": "MED",
+ "loc": "desktop-shell/src/windows_app.rs:173",
+ "text": "自启动初始化与托盘切换都静默丢弃错误:initialize_autostart 在 :181 仅判断 is_ok(),失败无提示;托盘 :296 用 unwrap_or(false),:298-301 对 enable/disable 仅判断结果,失败不更新勾选也不给用户诊断。登录自启动可能未生效而界面无可操作反馈。"
+ }
+ ],
+ "auditedHash": "8d5a7440b8474d0725105759e4b62bcd3d2c1e05089814adf5d18c18ddca0248",
+ "auditedAt": "2026-08-30",
+ "auditedRev": "f920c50289134fc8d6d6febba1e6e8895ee859a6"
+ },
+ {
+ "id": "delivery_docs",
+ "label": "构建、发布与工程文档",
+ "band": "delivery",
+ "path": "Cargo/package 配置、CI、安装与 docs",
+ "paths": [
+ "Cargo.toml",
+ "Cargo.lock",
+ "build.rs",
+ "Justfile",
+ "lefthook.yml",
+ "README.md",
+ ".gitignore",
+ "sonar-project.properties",
+ "web-ui/package.json",
+ "web-ui/package-lock.json",
+ "rust-toolchain.toml",
+ "web-ui/tsconfig*.json",
+ "web-ui/vite.config.ts",
+ "web-ui/eslint.config.js",
+ ".github/**/*.yml",
+ ".github/**/*.yaml",
+ "scripts/**/*",
+ "installer/**/*",
+ "packaging/**/*",
+ "release/**/*",
+ "docs/**/*"
+ ],
+ "coupling": "med",
+ "deps": [
+ "bootstrap_daemon",
+ "frontend_shell"
+ ],
+ "desc": "定义依赖、质量命令、Windows/Linux 构建安装、GitHub Release 和面向维护者/用户的文档。",
+ "loc": 17392,
+ "contentHash": "f82ca81a39604216dc653e5f6e72ce1a41c8d6db300d698b0f16ca0d0b626070",
+ "score": 72,
+ "grade": "C",
+ "tags": [
+ "glue"
+ ],
+ "findings": [
+ {
+ "sev": "HIGH",
+ "loc": "web-ui/vite.config.ts:74",
+ "text": "Vite 开发服务器绑定 0.0.0.0,并在 76-80 行将 /api(含 WebSocket)代理到本机 127.0.0.1:2999。LAN 客户端可访问 5173 的同源代理,导致 passwordless daemon 的本地控制面暴露;这与 docs/API.md:928、docs/CLI.md:388 的 loopback-only 假设不一致。"
+ },
+ {
+ "sev": "MED",
+ "loc": "sonar-project.properties:4",
+ "text": "Sonar 仅扫描 src 与 web-ui/src,完全不纳入 desktop-shell;quality.yml:190-193 只对桌面壳执行 cargo test/build,没有 fmt、clippy 或覆盖率,托盘、单实例和升级入口不受 Sonar quality gate 约束。"
+ },
+ {
+ "sev": "MED",
+ "loc": ".github/workflows/quality.yml:143",
+ "text": "Linux package smoke 仅执行脚本语法检查、构建 deb 和 payload grep,没有 dpkg 安装、postinst/prerm/postrm、systemd 启停或健康探针;文档宣传的 Linux 服务安装生命周期仍未被 CI 验证。"
+ },
+ {
+ "sev": "MED",
+ "loc": ".github/workflows/release.yml:234",
+ "text": "正式 Windows release 的 package-installer 只编译、签名、哈希并上传安装包,没有对最终 release artifacts 执行安装、启动、升级、卸载 smoke;quality.yml 的安装测试使用另一套构建产物,不能完全证明实际发布包可用。"
+ }
+ ],
+ "auditedHash": "f82ca81a39604216dc653e5f6e72ce1a41c8d6db300d698b0f16ca0d0b626070",
+ "auditedAt": "2026-08-30",
+ "auditedRev": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.codex/tasks/full-project-audit.md b/.codex/tasks/full-project-audit.md
new file mode 100644
index 0000000..ae8f601
--- /dev/null
+++ b/.codex/tasks/full-project-audit.md
@@ -0,0 +1,60 @@
+# RunDock / Alter 全项目工程审计任务
+
+## 背景
+
+本轮联合使用 Codemap、Code Overhaul Review 与 SonarQube,对当前仓库做一次完整但只读的工程体检。目标是解释系统为何能运行、哪些部分可靠、哪些风险依赖偶然条件,并给出分轮整改路线图。
+
+## 目标
+
+- 建立功能模块级架构地图、依赖关系、核心请求与进程生命周期主线。
+- 审计架构、业务逻辑、数据与状态、稳定性、测试、安全、性能、依赖和可维护性。
+- 运行现有的安全验证命令并收集 SonarQube 客观指标。
+- 交叉验证三类证据,最终生成仓库根目录 `PROJECT_AUDIT.md`。
+
+## 允许修改范围
+
+- `.codemap/**`:Codemap 配置、状态与生成报告。
+- `.codex/tasks/full-project-audit.md`:本任务边界与验收标准。
+- `PROJECT_AUDIT.md`:最终联合审计报告。
+- SonarScanner 所需的临时参数或仓库外临时文件;不得把令牌或密钥写入仓库。
+
+## 禁止修改范围
+
+- `src/**`、`web-ui/src/**`、`tests/**` 及任何生产代码、测试代码与业务配置。
+- `Cargo.toml`、`Cargo.lock`、`web-ui/package.json`、锁文件和依赖版本。
+- 数据库、用户状态目录、进程注册信息、远程服务与生产环境。
+- 不修复、不重构、不删除 Dead/Legacy Code,不自动处理 Sonar 问题。
+
+## 已确定实现要求
+
+- Codemap 使用中文,输出到 `.codemap/`,`modules.json` 为唯一事实源,HTML/Markdown 由脚本生成。
+- 每个 Codemap 功能模块必须由独立只读子任务按固定标准评分;主控只做结构划分与跨模块综合。
+- Code Overhaul 采用 FULL AUDIT 模式;不创建 Beads、不进入整改。
+- SonarQube 使用本机共享实例(若实时验证可用),扫描令牌只在运行时使用并在结束后撤销。
+- 任何测试或扫描不得启动/停止 Alter 管理的项目,不得读写真实用户数据或项目外密钥。
+
+## 验收标准
+
+- `.codemap/modules.json`、`.codemap/codemap.html`、`.codemap/codemap.md` 一致且可重新生成。
+- 已记录构建、测试、lint、typecheck、coverage、依赖检查和 SonarQube 的实际结果或明确阻塞。
+- `PROJECT_AUDIT.md` 包含用户要求的全部章节、文件行号证据、优先级、Top 10、删除候选、暂不应动区域和分轮路线图。
+- 报告明确区分客观扫描、源码审查、推断、未验证项,不把 Quality Gate 或测试通过等同于生产就绪。
+- 完成报告后停止,不实施任何整改。
+
+## 计划验证命令
+
+- `cargo fmt --all -- --check`
+- `cargo check --all-targets`
+- `cargo test --all-targets`
+- `cargo clippy --all-targets --all-features -- -D warnings`
+- `npm run lint`(`web-ui`)
+- `npm run format:check`(`web-ui`)
+- `npm run test -- --coverage` 或现有覆盖率命令(`web-ui`)
+- `npm run build`(`web-ui`)
+- SonarScanner + SonarQube API 指标查询
+
+## 返回格式
+
+- 主产物:`PROJECT_AUDIT.md`
+- 辅助产物:`.codemap/modules.json`、`.codemap/codemap.html`、`.codemap/codemap.md`
+- 最终回复仅汇报结论、验证证据、产物路径和 Git 同步状态,不附带整改实现。
diff --git a/.codex/tasks/p0-p2-remediation.md b/.codex/tasks/p0-p2-remediation.md
new file mode 100644
index 0000000..335f329
--- /dev/null
+++ b/.codex/tasks/p0-p2-remediation.md
@@ -0,0 +1,81 @@
+# RunDock / Alter P0-P2 整改任务
+
+## 背景
+
+依据仓库根目录 `PROJECT_AUDIT.md`,本轮把报告中的 P0、P1、P2 全部整改到可验证状态。目标不是机械清零 Sonar,而是让控制面、文件边界、持久化、生命周期、恢复、外部集成、错误语义、资源上限和质量门禁形成可执行的不变量。
+
+## 目标
+
+- 完成报告 P0-1 至 P0-4。
+- 完成报告 P1-1 至 P1-8。
+- 完成报告 P2 的错误语义、资源上限、质量门禁、依赖/Dead Code、热点职责拆分和关键测试补齐。
+- 保留 `alter` CLI、现有数据目录、JSON 兼容读取和本地单体架构。
+- 用自动化测试、静态检查、隔离运行态和更新后的 Sonar/Codemap 证明整改结果。
+
+## 允许修改范围
+
+- Rust:`src/**`、`tests/**`、`Cargo.toml`、`Cargo.lock`。
+- 前端:`web-ui/src/**`、`web-ui/package.json`、`web-ui/package-lock.json`、必要的前端工具配置。
+- 工程门禁:`.github/workflows/**`、`lefthook.yml`、`Justfile`、必要的安全/覆盖率配置。
+- 文档与审计:`PROJECT_AUDIT.md`、`.codemap/**`、`.codex/tasks/p0-p2-remediation.md`。
+- 已证实未引用的 dead/legacy 文件和依赖;删除前必须有引用扫描与全量验证证据。
+
+## 禁止修改范围
+
+- 不写入、迁移或删除 `%APPDATA%\alter-pm2` 中的真实 `state.json`、`projects.json`、认证配置或日志。
+- 不停止、替换或接管当前安装版 Alter 及其管理的其他项目。
+- 不执行真实 AI、Webhook、Telegram、Tunnel、自更新、安装、发布、部署或远程写操作。
+- 不改变 Codex/OpenAI 登录、认证或模型提供商。
+- 不引入微服务、消息队列、Kubernetes、CQRS、Event Sourcing 或无证据的大型架构。
+- 不提交 API Key、Token、密码、Cookie、`.env`、日志、缓存、构建产物或扫描临时文件。
+
+## 已确定实现要求
+
+### P0
+
+1. 非回环监听必须启用认证;CORS 只允许明确受信来源;默认本机 UI/CLI 兼容。
+2. env 文件 API 只接受单个安全文件名组件,规范化后不得越过进程 cwd,并覆盖 Windows/Unix traversal 测试。
+3. state 保存采用单写者/串行化、唯一临时文件、原子替换、last-known-good 和写后校验;保留旧 JSON 读取兼容。
+4. 自更新只接受固定仓库、正确版本/平台资产,限制重定向和大小,校验 SHA-256 后才允许执行/替换,任何不完整证据 fail-closed。
+
+### P1
+
+1. 进程/项目变更必须明确 commit/compensate;不得返回“失败”却留下不可见副作用。
+2. watcher、LogWriter、health、hooks、cron 由明确 handle 所有,start/restart/crash/stop 行为一致。
+3. 恢复和 kill 校验 PID 身份;不匹配时不接管、不误杀,恢复可重复执行。
+4. AI/Webhook/Tunnel 出站目标有协议、地址和私网边界;秘密读取只返回掩码;诊断内容脱敏并限制大小;Telegram whitelist fail-closed。
+5. 发布工作流使用 job 级最小权限、关键 action pin SHA、签名缺失 fail-closed,并加入 PR/main 质量门禁。
+
+### P2
+
+1. UI/API 明确区分 loading、empty、stale、failed、partial;不得失败后仍显示成功。
+2. 日志读取有行数/字节上限并避免整文件载入;前端实时日志有有界 buffer;轮询 single-flight、可取消、退避。
+3. 在行为测试保护下拆分最危险的 App/ProcessManager/AI 职责,不做无收益全面重写。
+4. 删除已证实无引用的模板/占位/旧实现和依赖,统一 npm 锁文件真相;每项删除可独立证明。
+5. 修复现有 fmt/lint 门禁,并补 auth/CORS/path/state/lifecycle/恢复/外部目标/错误语义/资源上限测试。
+
+## 验收标准
+
+- `PROJECT_AUDIT.md` 中所有 P0、P1、P2 条目都有对应实现、测试或明确证明“不适用/已由更强不变量覆盖”的证据。
+- Rust fmt、check、test、clippy 在受支持工具链上通过;若当前 GNU 环境仍缺工具,必须使用仓库支持的替代工具链完成同等验证,而不是跳过。
+- 前端 format、lint、typecheck、unit、coverage、build 全部通过。
+- 依赖审计和 secrets 扫描无未解释的高风险项;Sonar 重新扫描并记录有意义的变化。
+- 隔离数据目录与非生产端口完成 daemon/API smoke;当前 Alter 管理的 2999/5173 路由不被替换或遗留手动进程。
+- `git diff --check` 通过;最终只有本任务范围内文件;没有 TODO/debug、硬编码秘密或构建临时产物。
+
+## 计划验证命令
+
+- `cargo fmt --all -- --check`
+- `cargo check --all-targets --locked`
+- `cargo test --all-targets --locked`
+- `cargo clippy --all-targets --all-features --locked -- -D warnings`
+- `npm run format:check`、`npm run lint`、`npx tsc -b --pretty false`、`npm test`、覆盖率、`npm run build`
+- 依赖/secret 扫描、Codemap scan/render、SonarScanner/API 查询
+- 隔离 `ALTER_DATA_DIR_SUFFIX`、日志目录和备用端口的 daemon/API smoke
+- `git diff --check`、`git diff --stat`、`git status --short`
+
+## 返回格式
+
+- P0/P1/P2 完成矩阵与关键实现说明。
+- 实际测试命令、结果和剩余限制。
+- 运行态、Git 分支、提交 SHA、远端和推送结果。
diff --git a/.codex/tasks/port-runtime-reconciliation.md b/.codex/tasks/port-runtime-reconciliation.md
new file mode 100644
index 0000000..0a3bbd6
--- /dev/null
+++ b/.codex/tasks/port-runtime-reconciliation.md
@@ -0,0 +1,52 @@
+# 端口扫描与项目运行态校准
+
+## 背景
+
+Windows `netstat -ano` 会为正常的 `TIME_WAIT` 连接返回 PID 0,当前项目页的二次校验错误地将整个端口列表判为无效。与此同时,Niuma Studio(8001/8765)和 Novel Writer(5174)由 Alter 外部进程启动,导致实际运行状态与 Alter 卡片不一致。
+
+## 目标
+
+- 允许端口扫描结果中的 PID 0,但继续禁止其参与项目归属或停止操作。
+- 将 Niuma 主服务、Niuma 发布 Worker 和 Novel Writer 安全迁移为 Alter 托管进程。
+- 验证项目卡片、监听端口、健康接口和实际进程祖先链一致。
+
+## 允许范围
+
+- 修改 Alter 前端端口校验及相关测试。
+- 通过 Alter API 更新 Niuma、Novel Writer 的运行态和项目元数据。
+- 在已验证身份后停止仅属于上述项目的外部进程树。
+- 备份和恢复 `%APPDATA%\alter-pm2\state.json`、`projects.json`。
+
+## 禁止范围
+
+- 不修改 Niuma Studio、Novel Writer 的生产源码、数据库或 `.env`。
+- 不重启 Alter daemon,不启停 CatCare Hub、GroupBrief、RunDock 或其他项目。
+- 不按端口、名称或目录前缀盲目接管或结束进程。
+- 不读取、复制或记录任何密钥、Token、Cookie 或 `.env` 内容。
+
+## 已确定实现要求
+
+- Niuma 项目包含主服务和发布 Worker 两个 Alter 成员,主网页端口为 8001。
+- Novel Writer 使用现有登记项,主网页端口为 5174。
+- Worker 直接运行项目现有 Python 入口,并由应用自身读取被忽略提交的配置。
+- 任一身份或端口检查发生变化时停止;接管失败时恢复元数据和原运行方式。
+
+## 验收标准
+
+- PID 0/TIME_WAIT 数据通过校验,负 PID 和错误类型仍被拒绝。
+- 8001、8765、5174 各有且仅有一个监听者,祖先链均归属 Alter daemon。
+- Niuma 显示 `running 2/2`,Novel Writer 显示 `running 1/1`。
+- Niuma 健康、调度器和 Worker 可用;Novel Writer 5174 返回 HTTP 200。
+- 5173 项目页不再显示“端口扫描返回了无效数据”。
+
+## 测试命令
+
+- `npm exec vitest run src/lib/processWeb.test.ts src/lib/schemas.test.ts src/pages/ProjectsPage.test.tsx`
+- `npm test`
+- `npm run typecheck`
+- `npm run lint`
+- `npm run build`
+
+## 返回格式
+
+报告修改文件、测试结果、运行态端口/PID/祖先链、项目卡片状态、备份路径、提交哈希、分支和推送结果。
diff --git a/.codex/tasks/rundock-second-remediation.md b/.codex/tasks/rundock-second-remediation.md
new file mode 100644
index 0000000..58932de
--- /dev/null
+++ b/.codex/tasks/rundock-second-remediation.md
@@ -0,0 +1,67 @@
+# RunDock 第二轮定点修复执行任务
+
+## 背景
+
+`PROJECT_REAUDIT.md` 复检发现 6 个 HIGH,以及 PATCH/checkpoint 和质量门禁缺口。用户已批准按既定计划实施;只关闭这些问题,不做大文件全面拆分、依赖追新或 Sonar smell 清零。
+
+## 目标
+
+1. 关闭 Vite LAN 暴露、双重重启、首次加载、Linux 端口解析回归。
+2. 修复 Cron retained process tree 与 ProcessTreeGuard 附加失败的清理所有权。
+3. 为进程/项目 PATCH 提供 absent/null/value 语义;保证 Telegram checkpoint 单调提交原子性。
+4. 恢复本地 Sonar、desktop-shell、Windows 安装包和 Linux deb 的可信门禁。
+5. 重新验证 Codemap,生成 `PROJECT_REAUDIT_REMEDIATION.md`。
+
+## 允许修改范围
+
+- `web-ui/vite.config.ts`、相关前端组件/hooks/types/tests。
+- `src/api/routes/ports.rs`、`src/process/{manager,runner,tree,hooks}.rs`。
+- `src/models/{api_types,project}.rs`、相关进程/项目路由和前端 API 类型。
+- `src/config/{atomic_file,telegram_checkpoint}.rs` 及对应测试。
+- `.github/workflows/{quality,release}.yml`、`scripts/` 下新增的本地 Sonar/安装 smoke 脚本。
+- `.gitattributes`、`sonar-project.properties`、`.codemap/` 和整改关闭报告。
+- 为上述行为新增或扩展的测试文件。
+
+## 禁止修改范围
+
+- 不做微服务、数据库、状态格式、端口或外部 Provider 迁移。
+- 不全面拆分 ProcessManager/App/Pages/AI/Terminal。
+- 不执行强推、rebase 已推送历史、自动合并、部署或系统级安装。
+- 不读取、写入或提交 Token、密码、Cookie、`.env` 或浏览器数据。
+- 不重启当前项目运行时,不修改生产数据库或远程服务器。
+
+## 已确定实现要求
+
+- Vite 默认仅 `127.0.0.1`;API/WS 代理也保持 loopback。
+- Env modal 自己 await 唯一 restart,父组件只接收成功通知并刷新。
+- 不改变 `useSingleFlightPoll.enabled` 语义;调用方负责一次初始加载。
+- Unix 端口输出携带 `Ss`/`Netstat` 格式标记,使用独立 parser。
+- Cron 只有在完整树清理成功后才能 Sleeping/重排;失败则 Errored 并停 scheduler。
+- PATCH:缺失保留、null 清空、具体值覆盖;文件/URL/方法/响应结构不变。
+- checkpoint 的读、单调校验、写入使用同一文件操作锁。
+- Sonar 使用本机服务和会话 Token,GitHub-hosted runner 不再强制访问本机。
+- desktop-shell 在 Windows MSVC CI 跑 fmt/clippy/test/build;正式安装包和 deb 做真实生命周期 smoke。
+
+## 验收标准
+
+- 6 个 HIGH 关闭,R1-5 完成,受影响 Codemap 模块无新 HIGH。
+- 根 Rust fmt/check/test/clippy/audit/coverage 通过。
+- 前端 format/lint/typecheck/test/coverage/build/audit 通过,覆盖率不低于复检基线。
+- desktop-shell MSVC 门禁和 Windows 安装包 smoke 通过。
+- Linux deb 安装、健康、升级、卸载 smoke 通过。
+- 本地 Sonar 完成认证扫描并如实记录 gate;若缺 Token,明确阻塞且不伪造指标。
+- `git diff --check` 通过,无范围外修改、秘密、TODO/debug 或临时文件。
+
+## 测试命令
+
+- `cargo fmt --all -- --check`
+- `cargo check --all-targets --locked`
+- `cargo test --all-targets --locked`
+- `cargo clippy --all-targets --all-features --locked -- -D warnings`
+- `npm run format:check && npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm audit --audit-level=high`(在 `web-ui`)
+- desktop-shell 的 fmt/test/clippy/release build(Windows MSVC)。
+- Codemap 官方 scan/apply-audit/render/stamp 流程。
+
+## 返回格式
+
+最终报告:问题关闭矩阵、关键文件、测试/覆盖率、Sonar/Codemap、Git 初始与最终状态、提交 SHA、远端 SHA、Push、PR、CI、是否合并及剩余阻塞。
diff --git a/.codex/tasks/rundock-tray-and-ui-cleanup.md b/.codex/tasks/rundock-tray-and-ui-cleanup.md
new file mode 100644
index 0000000..ac6140c
--- /dev/null
+++ b/.codex/tasks/rundock-tray-and-ui-cleanup.md
@@ -0,0 +1,75 @@
+# RunDock 托盘关闭与底部 UI 精简实施任务
+
+## 背景
+
+当前 Windows 桌面壳已拦截原生关闭事件并调用 `hide()`,但忽略隐藏失败,也没有显式区分“隐藏到托盘”和“真正退出”。React 控制台同时永久显示左下服务器切换器和黑色状态栏,造成本机信息重复、功能入口含义不清。桌面 WebView 与浏览器共用 `127.0.0.1:2999` 页面,因此前端变更需要随 daemon 一并构建和部署。
+
+## 目标
+
+- 原生 X 只隐藏到托盘;托盘菜单退出才关闭桌面壳。
+- 首次成功隐藏后显示一次 Windows 原生说明,后续不重复。
+- 托盘退出保留 daemon 和全部托管项目。
+- 删除正常界面的悬浮“本地”和黑色状态栏,把服务器、终端、AI、统计移入有文字的入口。
+- 新增“设置 → 服务器”管理页,同时保留认证失败时的服务器恢复入口。
+- 完成自动化验证、Windows 本机部署、Git 提交、普通 Push 和 PR;不得自动合并。
+
+## 允许修改范围
+
+- `desktop-shell/` 的窗口、托盘、状态与测试。
+- `web-ui/src/` 的应用外壳、服务器管理、设置页、终端偏移、响应式样式及测试。
+- `.github/workflows/quality.yml` 的 Windows 安装 smoke。
+- 本任务文件及本次改动直接需要的锁文件。
+
+## 禁止修改范围
+
+- 不修改 `/api/v1`、2999 端口、daemon/项目数据 schema、远程服务器 localStorage 格式。
+- 不停止、删除或重建任何托管项目,不修改项目外源码、数据库、`.env` 或凭据。
+- 不引入 Tauri notification 插件,不恢复 5173,不改变登录/认证方式。
+- 不 reset、stash、clean、强推、改写历史或自动合并 PR。
+
+## 已确定实现要求
+
+1. 桌面状态包含 `quitting` 与进程内一次性提示保护;托盘退出和内部 `--quit` 统一走显式退出函数。
+2. 非退出态 `CloseRequested` 必须 `prevent_close`;成功隐藏后才写入 `%APPDATA%\alter-pm2\desktop-shell-tray-notice.json` 并异步显示一次原生说明。
+3. 隐藏失败时窗口保持可见并显示原生错误;不得因错误退出桌面壳。
+4. 托盘左键、菜单“打开 RunDock”和重复启动均恢复、取消最小化并聚焦同一窗口。
+5. 托盘退出文案明确“项目继续运行”,且退出后 2999 与托管项目保持健康。
+6. 正常认证界面不再渲染固定 `ServerSwitcher` 或 `StatusBar`;`AuthGuard` 的 recovery 入口保留。
+7. 侧栏“工具”包含服务器连接、终端、AI 助手、系统统计、端口查找、隧道;开发工具仅在原开发条件下出现。
+8. `/settings/servers` 使用现有服务器存储与校验逻辑,支持本机、HTTPS 远程直连与 SSH 配置,不改变持久化格式。
+9. 终端面板保留现有能力,但底部定位改为 0;移动端不再为已删除状态栏预留空间。
+10. 本机部署前记录 2999 监听者、daemon/shell 命令行、项目 ID/PID 和文件哈希;备份后只替换已验证的 RunDock 二进制并做可回滚重启。
+
+## 验收标准
+
+- WM_CLOSE 后原 `rundock.exe` PID 存活、主窗口不可见;重复启动恢复同一 PID。
+- 首次隐藏显示说明,第二次隐藏不再显示;marker 为有效且持久的本地文件。
+- 托盘退出结束桌面壳,但 daemon health、2999 listener 和托管项目 ID/PID 不变。
+- 正常主界面没有固定“本地/local”和黑色底栏;服务器管理、终端、AI、统计均有明确文字入口。
+- 认证失败或服务器配置损坏时仍能切回本机。
+- 桌面与浏览器页面一致,移动端无底部空隙,生产环境无 5173 listener。
+- Git diff 不含范围外修改、敏感信息、构建产物、调试代码或临时文件。
+
+## 测试命令
+
+```powershell
+cargo fmt --all -- --check
+cargo test --all-targets --locked
+cargo clippy --all-targets --all-features --locked -- -D warnings
+cargo fmt --manifest-path desktop-shell/Cargo.toml -- --check
+cargo check --manifest-path desktop-shell/Cargo.toml --locked
+cargo test --manifest-path desktop-shell/Cargo.toml --locked
+cargo build --manifest-path desktop-shell/Cargo.toml --release --locked
+npm --prefix web-ui run format:check
+npm --prefix web-ui run lint
+npm --prefix web-ui run typecheck
+npm --prefix web-ui test
+npm --prefix web-ui run build
+```
+
+## 返回格式
+
+- 改动摘要、关键行为与接口变化。
+- 自动化测试、Windows smoke 和本机真实验收证据。
+- 部署前后 2999、daemon、桌面壳和项目 PID/health 对比及回滚材料。
+- Git 初始状态、默认分支、任务分支、提交/远端 SHA、Push、PR、CI 与合并状态。
diff --git a/.codex/tasks/rundock-visible-name.md b/.codex/tasks/rundock-visible-name.md
new file mode 100644
index 0000000..8c7ec6c
--- /dev/null
+++ b/.codex/tasks/rundock-visible-name.md
@@ -0,0 +1,68 @@
+# RunDock 对外名称统一
+
+## 背景
+
+项目已经以 RunDock 作为仓库、安装包、桌面端和网页品牌,但 CLI 输出、后台日志、
+少量界面提示、安装说明和公开文档仍把兼容实现名 `alter` 当作产品名称展示,容易让
+用户误以为这是两个项目。
+
+## 目标
+
+- 所有面向用户的产品名称、输出前缀、界面文案和说明统一为 RunDock。
+- `alter` 只作为必须保留的兼容命令或技术标识出现,不再作为产品品牌出现。
+- 不破坏现有安装升级、托管项目、脚本、数据目录或 Linux 服务。
+
+## 允许修改范围
+
+- Rust CLI、daemon、API 与桌面壳中的用户可见文案及相关测试。
+- Web UI 的用户可见名称、错误提示及相关测试。
+- Windows 安装器、Linux 包说明、systemd、发布工作流与质量工具的显示描述。
+- 当前工程验收报告中的项目名称。
+- 根 README 与 `docs/` 下的公开说明文字。
+- 本任务说明文件。
+
+## 禁止修改范围
+
+- 不重命名 `alter.exe`、`alter` CLI、Rust crate/lib/bin 或 Debian 包。
+- 不修改 `ALTER_*` 环境变量、`alter-pm2` 数据目录、`alter-daemon.service`、
+ Prometheus 指标、浏览器存储键、协议头、Inno AppId 或上游 Winget 历史清单。
+- 不修改用户的 `state.json`、`projects.json`、数据库或任何受托管项目状态。
+- 不重启 daemon,不部署,不迁移当前工作目录。
+
+## 已确定实现要求
+
+- CLI 的可见前缀统一为 `[RunDock]`,产品错误和日志使用 `RunDock daemon`。
+- 实际可执行命令示例继续使用兼容命令 `alter ...`,但说明文字只称 RunDock CLI。
+- Web 状态栏只显示 `RunDock` 版本,不显示 `alter CLI`。
+- 浏览器存储损坏提示不得要求用户操作内部键名。
+- 安装器和包元数据仅展示 RunDock;技术路径、服务名和命令保持原值。
+- 公开文档把产品主语统一为 RunDock,代码块中的兼容命令和路径保持原值。
+
+## 验收标准
+
+- 目标文件中不再出现 `[alter]`、`compatible alter CLI`、`alter CLI)`、
+ `Alter data`、`another Alter daemon` 等旧品牌展示。
+- 所有兼容命令、路径、服务名、存储键和环境变量保持不变。
+- 前端构建、测试和 lint 通过;新增文件通过 Prettier,已有的全仓格式基线问题准确记录。
+- Rust 格式、检查、测试和 Clippy 通过;如发布构建受本机环境阻塞,返回准确证据。
+- 最终 diff 只包含本任务文件和明确的用户可见文案修改。
+
+## 测试命令
+
+```powershell
+Push-Location web-ui
+npm run build
+npm test
+npm run lint
+npm run format:check
+Pop-Location
+
+cargo fmt -- --check
+cargo check --all-targets
+cargo test --all-targets
+cargo clippy --all-targets -- -D warnings
+```
+
+## 返回格式
+
+报告统一后的命名边界、修改文件、兼容项保留情况、验证结果、提交、推送与 PR 状态。
diff --git a/.codex/tasks/windows-loopback-proxy-fix.md b/.codex/tasks/windows-loopback-proxy-fix.md
new file mode 100644
index 0000000..2eff089
--- /dev/null
+++ b/.codex/tasks/windows-loopback-proxy-fix.md
@@ -0,0 +1,55 @@
+# Windows loopback proxy fix
+
+## 背景
+
+RunDock daemon 实际监听 `127.0.0.1:2999` 且健康接口可由浏览器和 PowerShell
+正常访问,但 Rust 客户端的健康请求在 500 ms 后超时。现场 Windows 启用了本机
+代理;客户端没有声明本机 daemon 通信必须绕过代理,导致桌面启动器等待 10 秒后
+终止原本健康的 daemon。
+
+## 目标
+
+- 所有 `DaemonClient` HTTP 通信固定直连已验证的 loopback 地址。
+- 保留现有严格健康契约、PID 文件和进程身份校验。
+- 让 CLI、桌面壳和浏览器入口一致识别已启动 daemon。
+
+## 允许修改范围
+
+- `src/client/daemon_client.rs`
+- 与 loopback 客户端代理行为直接相关的测试
+- 本任务说明文件
+
+## 禁止修改范围
+
+- 不修改 `state.json`、`projects.json` 或任何用户项目状态。
+- 不降低陌生进程保护,不按端口结束进程。
+- 不修改 REST 数据结构、前端、桌面壳行为或安装器。
+- 不修改 Windows 全局代理设置,不增加第三方依赖。
+
+## 已确定实现要求
+
+- 为普通、健康探测和流式三个 loopback `reqwest::Client` 显式调用 `no_proxy()`。
+- 继续只允许 `DaemonClient` 连接 `localhost`、`127.0.0.1` 或 `::1`。
+- 不保留临时诊断输出或调试环境变量。
+
+## 验收标准
+
+- `cargo fmt --check`、`cargo check --all-targets`、`cargo test --all-targets`、
+ `cargo clippy --all-targets -- -D warnings` 通过。
+- 开启 Windows 本机代理时,`alter daemon status` 仍能识别健康 daemon。
+- daemon 停止后运行 `alter daemon start`,10 秒内成功;2999 健康。
+- 启动 `rundock.exe` 后恢复桌面控制台,5173 没有监听。
+- 退出桌面壳后 daemon 仍存活,项目保存状态不变。
+
+## 测试命令
+
+```powershell
+cargo fmt --check
+cargo check --all-targets
+cargo test --all-targets
+cargo clippy --all-targets -- -D warnings
+```
+
+## 返回格式
+
+报告根因、修改文件、测试结果、现场 2999/5173 状态、Git 提交与推送结果。
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6577b90
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+* text=auto eol=lf
+
+*.bat text eol=crlf
+*.cmd text eol=crlf
+
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.zip binary
+*.exe binary
+*.dll binary
+*.deb binary
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
new file mode 100644
index 0000000..b5708b4
--- /dev/null
+++ b/.github/workflows/quality.yml
@@ -0,0 +1,217 @@
+name: Quality
+
+on:
+ workflow_call:
+ pull_request:
+ push:
+ branches:
+ - main
+ - master
+ - "codex/**"
+
+permissions:
+ contents: read
+
+env:
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ NPM_VERSION: 11.12.1
+
+jobs:
+ frontend:
+ name: Frontend quality
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ defaults:
+ run:
+ working-directory: web-ui
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Set up Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 24.15.0
+ cache: npm
+ cache-dependency-path: web-ui/package-lock.json
+ - name: Install pinned npm
+ run: npm install --global "npm@$NPM_VERSION" && test "$(npm --version)" = "$NPM_VERSION"
+ - run: npm ci
+ - run: npm run format:check
+ - run: npm run lint
+ - run: npm run typecheck
+ - run: npm audit --audit-level=high
+ - run: npm run test:coverage
+ - name: Upload frontend coverage
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: frontend-coverage
+ path: web-ui/coverage/lcov.info
+ if-no-files-found: error
+ retention-days: 7
+ - run: npm run build
+
+ rust:
+ name: Rust quality
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Set up Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 24.15.0
+ cache: npm
+ cache-dependency-path: web-ui/package-lock.json
+ - name: Build embedded web UI
+ working-directory: web-ui
+ run: |
+ npm install --global "npm@$NPM_VERSION"
+ test "$(npm --version)" = "$NPM_VERSION"
+ npm ci
+ npm run build
+ - name: Set up Rust
+ run: |
+ rustup toolchain install 1.98.0 --profile minimal --component rustfmt,clippy,llvm-tools-preview
+ rustup default 1.98.0
+ - name: Install pinned Rust coverage tool
+ run: cargo install cargo-llvm-cov --version 0.9.0 --locked
+ - name: Install pinned Rust security audit tool
+ run: cargo install cargo-audit --version 0.22.2 --locked
+ - run: cargo fmt --all -- --check
+ - run: cargo check --all-targets --locked
+ - run: cargo audit
+ - name: Test with Rust LCOV coverage
+ run: |
+ mkdir -p coverage
+ cargo llvm-cov --all-targets --locked --lcov --output-path coverage/rust-lcov.info --fail-under-lines 20
+ - name: Upload Rust coverage
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: rust-coverage
+ path: coverage/rust-lcov.info
+ if-no-files-found: error
+ retention-days: 7
+ - run: cargo clippy --all-targets --all-features --locked -- -D warnings
+ - name: Build release binary for package smoke
+ run: cargo build --release --locked
+ - name: Upload Linux release binary
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: rundock-linux-binary
+ path: target/release/alter
+ if-no-files-found: error
+ retention-days: 7
+
+ packaging-linux:
+ name: Linux package smoke
+ needs: rust
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Download tested release binary
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: rundock-linux-binary
+ path: package-input
+ - name: Install, upgrade, health-check, and remove the real Debian package
+ run: |
+ bash -n scripts/build-deb.sh scripts/linux-deb-smoke.sh packaging/debian/postinst packaging/debian/prerm packaging/debian/postrm
+ chmod 755 package-input/alter
+ CI=true bash scripts/linux-deb-smoke.sh package-input/alter
+
+ packaging-windows:
+ name: Windows installer smoke
+ runs-on: windows-2022
+ timeout-minutes: 35
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Set up Node.js
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 24.15.0
+ cache: npm
+ cache-dependency-path: web-ui/package-lock.json
+ - name: Build embedded web UI
+ working-directory: web-ui
+ shell: pwsh
+ run: |
+ npm install --global "npm@$env:NPM_VERSION"
+ if ((npm --version) -ne $env:NPM_VERSION) { throw "Unexpected npm version" }
+ npm ci
+ npm run build
+ - name: Set up Rust
+ shell: pwsh
+ run: |
+ rustup toolchain install 1.98.0 --profile minimal --component rustfmt,clippy --target x86_64-pc-windows-msvc
+ rustup default 1.98.0
+ - name: Build root release binary with MSVC
+ shell: pwsh
+ run: |
+ cargo build --release --locked --target x86_64-pc-windows-msvc
+ if ($LASTEXITCODE -ne 0) { throw "alter release build failed" }
+ New-Item -ItemType Directory -Force -Path target\release | Out-Null
+ Copy-Item target\x86_64-pc-windows-msvc\release\alter.exe target\release\alter.exe
+ - name: Desktop shell fmt
+ shell: pwsh
+ run: cargo fmt --manifest-path desktop-shell\Cargo.toml --all -- --check
+ - name: Desktop shell clippy
+ shell: pwsh
+ run: cargo clippy --manifest-path desktop-shell\Cargo.toml --all-targets --locked --target x86_64-pc-windows-msvc -- -D warnings
+ - name: Desktop shell tests
+ shell: pwsh
+ run: |
+ cargo test --manifest-path desktop-shell\Cargo.toml --all-targets --locked --target x86_64-pc-windows-msvc
+ if ($LASTEXITCODE -ne 0) { throw "desktop shell tests failed" }
+ - name: Desktop shell release build
+ shell: pwsh
+ run: |
+ cargo build --manifest-path desktop-shell\Cargo.toml --release --locked --target x86_64-pc-windows-msvc
+ if ($LASTEXITCODE -ne 0) { throw "desktop shell release build failed" }
+ New-Item -ItemType Directory -Force -Path desktop-shell\target\release | Out-Null
+ Copy-Item desktop-shell\target\x86_64-pc-windows-msvc\release\rundock.exe desktop-shell\target\release\rundock.exe
+ $loader = 'desktop-shell\target\x86_64-pc-windows-msvc\release\WebView2Loader.dll'
+ if (-not (Test-Path -LiteralPath $loader)) {
+ $loaderSource = Get-ChildItem 'desktop-shell\target\x86_64-pc-windows-msvc\release\build' -Filter WebView2Loader.dll -File -Recurse |
+ Where-Object FullName -Match '[\\/]out[\\/]x64[\\/]WebView2Loader\.dll$' |
+ Sort-Object LastWriteTimeUtc -Descending |
+ Select-Object -First 1
+ if (-not $loaderSource) { throw 'WebView2Loader.dll was not produced by the desktop build' }
+ Copy-Item -LiteralPath $loaderSource.FullName -Destination $loader
+ }
+ $signature = Get-AuthenticodeSignature -LiteralPath $loader
+ if ($signature.Status -ne 'Valid' -or
+ $signature.SignerCertificate.Subject -notmatch '(^|,\s*)O=Microsoft Corporation(,|$)') {
+ throw 'WebView2Loader.dll does not have the expected Microsoft signature'
+ }
+ Copy-Item -LiteralPath $loader -Destination desktop-shell\target\release\WebView2Loader.dll
+ - name: Stage verified WebView2 bootstrapper
+ shell: pwsh
+ run: .\scripts\stage-webview2.ps1
+ - name: Install pinned Inno Setup
+ run: choco install innosetup --version=6.7.1 --no-progress -y
+ - name: Compile installer with the real release binary
+ shell: pwsh
+ run: |
+ $metadata = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json
+ $version = ($metadata.packages | Where-Object name -eq 'alter').version
+ if (-not $version) { throw "Unable to derive RunDock version from Cargo metadata" }
+ $issPath = 'installer\alter-setup.iss'
+ $iss = Get-Content -LiteralPath $issPath -Raw
+ $pattern = '#define AppVersion\s+"[^"]+"'
+ if ([regex]::Matches($iss, $pattern).Count -ne 1) {
+ throw "Expected exactly one AppVersion definition"
+ }
+ Set-Content -LiteralPath $issPath -Value ([regex]::Replace($iss, $pattern, "#define AppVersion `"$version`"")) -NoNewline
+ & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\alter-setup.iss
+ if ($LASTEXITCODE -ne 0) { throw "Inno Setup validation failed" }
+ Get-Item -LiteralPath "dist\RunDock-$version-windows-x64-setup.exe" -ErrorAction Stop | Out-Null
+ - name: Install and smoke the packaged desktop app
+ shell: pwsh
+ run: |
+ $metadata = cargo metadata --no-deps --format-version 1 | ConvertFrom-Json
+ $version = ($metadata.packages | Where-Object name -eq 'alter').version
+ .\scripts\windows-installer-smoke.ps1 -InstallerPath "dist\RunDock-$version-windows-x64-setup.exe" -InstallDirectory (Join-Path $env:RUNNER_TEMP 'RunDockInstallSmoke')
diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml
index 1ea2bd8..84fdbbf 100644
--- a/.github/workflows/release-linux.yml
+++ b/.github/workflows/release-linux.yml
@@ -1,4 +1,4 @@
-# RunDock — Linux Release Workflow (compatible alter CLI packages)
+# RunDock — Linux Release Workflow
# Triggers on version tags (v*) → builds Linux binaries → packages .deb → publishes APT repo on GitHub Pages
# https://github.com/damingishere-coder/RunDock
@@ -10,40 +10,74 @@ on:
- "v*"
permissions:
- contents: write
- pages: write
- id-token: write
+ contents: read
+
+# APT publication uses one shared gh-pages branch, so releases must not overlap.
+concurrency:
+ group: release-linux-publication
+ cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ NPM_VERSION: 11.12.1
jobs:
+ validate-release-tag:
+ name: Validate release tag
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Require a matching semantic version
+ env:
+ RELEASE_REF: ${{ github.ref_name }}
+ run: |
+ printf '%s\n' "$RELEASE_REF" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?(\+[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$' || {
+ echo "::error::Release tag must be a strict v-prefixed semantic version"
+ exit 1
+ }
+ TAG_VERSION="${RELEASE_REF#v}"
+ CARGO_VERSION="$(sed -n 's/^version *= *"\([^"]*\)"/\1/p' Cargo.toml | head -n 1)"
+ test "$TAG_VERSION" = "$CARGO_VERSION" || {
+ echo "::error::Release tag version does not match Cargo.toml"
+ exit 1
+ }
+
+ quality:
+ name: Required quality gate
+ uses: ./.github/workflows/quality.yml
+ needs: validate-release-tag
+
# ─────────────────────────────────────────────────────────────────────────────
# Build web UI (shared asset, embedded into the binary via rust-embed)
# ─────────────────────────────────────────────────────────────────────────────
build-web-ui:
name: Build Web UI
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
+ needs: quality
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
- node-version: 22
+ node-version: 24.15.0
cache: npm
cache-dependency-path: web-ui/package-lock.json
- name: Build web UI
run: |
+ npm install --global "npm@$NPM_VERSION"
+ test "$(npm --version)" = "$NPM_VERSION"
cd web-ui
npm ci
npm run build
- name: Upload web UI artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: web-ui-dist
path: web-ui/dist/
@@ -53,26 +87,24 @@ jobs:
# ─────────────────────────────────────────────────────────────────────────────
build-linux-amd64:
name: Build (Linux amd64)
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
needs: build-web-ui
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Restore web UI
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: web-ui-dist
path: web-ui/dist/
- - name: Install Rust stable
- uses: dtolnay/rust-toolchain@stable
- with:
- targets: x86_64-unknown-linux-gnu
+ - name: Install pinned Rust toolchain
+ run: rustup toolchain install 1.98.0 --profile minimal --target x86_64-unknown-linux-gnu
- name: Cache Cargo registry
- uses: actions/cache@v4
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.cargo/registry
@@ -84,10 +116,10 @@ jobs:
- name: Build release binary
env:
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
- run: cargo build --release --target x86_64-unknown-linux-gnu
+ run: cargo build --release --locked --target x86_64-unknown-linux-gnu
- name: Upload binary artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: alter-linux-amd64
path: target/x86_64-unknown-linux-gnu/release/alter
@@ -97,15 +129,15 @@ jobs:
# ─────────────────────────────────────────────────────────────────────────────
build-linux-arm64:
name: Build (Linux arm64)
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
needs: build-web-ui
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Restore web UI
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: web-ui-dist
path: web-ui/dist/
@@ -115,13 +147,11 @@ jobs:
sudo apt-get update -q
sudo apt-get install -y gcc-aarch64-linux-gnu
- - name: Install Rust stable
- uses: dtolnay/rust-toolchain@stable
- with:
- targets: aarch64-unknown-linux-gnu
+ - name: Install pinned Rust toolchain
+ run: rustup toolchain install 1.98.0 --profile minimal --target aarch64-unknown-linux-gnu
- name: Cache Cargo registry
- uses: actions/cache@v4
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.cargo/registry
@@ -134,10 +164,10 @@ jobs:
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
- run: cargo build --release --target aarch64-unknown-linux-gnu
+ run: cargo build --release --locked --target aarch64-unknown-linux-gnu
- name: Upload binary artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: alter-linux-arm64
path: target/aarch64-unknown-linux-gnu/release/alter
@@ -147,31 +177,37 @@ jobs:
# ─────────────────────────────────────────────────────────────────────────────
package-deb:
name: Package .deb
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
needs: [build-linux-amd64, build-linux-arm64]
+ concurrency:
+ group: github-release-${{ github.ref }}
+ cancel-in-progress: false
outputs:
version: ${{ steps.version.outputs.VERSION }}
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Extract version from tag
id: version
+ env:
+ RELEASE_REF: ${{ github.ref_name }}
run: |
- TAG="${{ github.ref_name }}"
+ printf '%s\n' "$RELEASE_REF" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?(\+[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$' || exit 1
+ TAG="$RELEASE_REF"
VERSION="${TAG#v}"
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
- name: Download amd64 binary
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: alter-linux-amd64
path: binaries/amd64/
- name: Download arm64 binary
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: alter-linux-arm64
path: binaries/arm64/
@@ -185,33 +221,86 @@ jobs:
run: chmod +x scripts/build-deb.sh
- name: Build amd64 .deb
- run: ./scripts/build-deb.sh binaries/amd64/alter ${{ steps.version.outputs.VERSION }} amd64
+ env:
+ RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
+ run: ./scripts/build-deb.sh binaries/amd64/alter "$RELEASE_VERSION" amd64
- name: Build arm64 .deb
- run: ./scripts/build-deb.sh binaries/arm64/alter ${{ steps.version.outputs.VERSION }} arm64
+ env:
+ RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
+ run: ./scripts/build-deb.sh binaries/arm64/alter "$RELEASE_VERSION" arm64
+
+ - name: Generate and sign checksum manifest (required)
+ env:
+ APT_GPG_KEY: ${{ secrets.APT_GPG_KEY }}
+ APT_GPG_FINGERPRINT: ${{ vars.APT_GPG_FINGERPRINT }}
+ run: |
+ if [ -z "$APT_GPG_KEY" ]; then
+ echo "::error::APT_GPG_KEY is required; refusing to publish unverifiable packages."
+ exit 1
+ fi
+ if [ -z "$APT_GPG_FINGERPRINT" ]; then
+ echo "::error::APT_GPG_FINGERPRINT repository variable is required."
+ exit 1
+ fi
+ printf '%s' "$APT_GPG_KEY" | gpg --batch --import
+ KEY_ID=$(gpg --list-secret-keys --with-colons --fingerprint | awk -F: '/^fpr/{print $10; exit}')
+ test -n "$KEY_ID" || { echo "::error::No signing key was imported"; exit 1; }
+ test "$KEY_ID" = "$APT_GPG_FINGERPRINT" || {
+ echo "::error::Imported signing key fingerprint does not match APT_GPG_FINGERPRINT"
+ exit 1
+ }
+ sha256sum ./*.deb > SHA256SUMS
+ gpg --batch --yes --armor --default-key "$KEY_ID" \
+ --detach-sign --output SHA256SUMS.asc SHA256SUMS
+ gpg --armor --export "$KEY_ID" > rundock-release-key.asc
- name: Upload .deb artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: alter-deb-packages
+ path: |
+ *.deb
+ SHA256SUMS
+ SHA256SUMS.asc
+ rundock-release-key.asc
+
+ publish-release-assets:
+ name: Publish Linux release assets
+ runs-on: ubuntu-22.04
+ needs: package-deb
+ permissions:
+ contents: write
+
+ steps:
+ - name: Download signed .deb packages
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: alter-deb-packages
- path: "*.deb"
+ path: release-assets/
- name: Attach .deb files to GitHub Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
- files: "*.deb"
+ files: |
+ release-assets/*.deb
+ release-assets/SHA256SUMS
+ release-assets/SHA256SUMS.asc
+ release-assets/rundock-release-key.asc
# ─────────────────────────────────────────────────────────────────────────────
# Publish: update APT repository on gh-pages branch
# ─────────────────────────────────────────────────────────────────────────────
publish-apt-repo:
name: Publish APT Repository
- runs-on: ubuntu-latest
- needs: package-deb
+ runs-on: ubuntu-22.04
+ needs: [package-deb, publish-release-assets]
+ permissions:
+ contents: write
steps:
- name: Checkout main repo
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
@@ -221,8 +310,8 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
if git ls-remote --exit-code origin gh-pages; then
- git fetch origin gh-pages
- git worktree add gh-pages gh-pages
+ git fetch origin gh-pages:refs/remotes/origin/gh-pages
+ git worktree add -b publish-gh-pages gh-pages origin/gh-pages
else
echo "gh-pages branch does not exist — creating orphan branch"
git worktree add --orphan gh-pages
@@ -233,7 +322,7 @@ jobs:
fi
- name: Download .deb packages
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: alter-deb-packages
path: new-debs/
@@ -268,19 +357,28 @@ jobs:
-o APT::FTPArchive::Release::Description="RunDock process manager APT repository" \
release dists/stable > dists/stable/Release
- - name: Sign Release (optional — requires APT_GPG_KEY secret)
+ - name: Sign Release (required)
env:
APT_GPG_KEY: ${{ secrets.APT_GPG_KEY }}
+ APT_GPG_FINGERPRINT: ${{ vars.APT_GPG_FINGERPRINT }}
run: |
if [ -z "$APT_GPG_KEY" ]; then
- echo "::warning::APT_GPG_KEY secret is not set — skipping GPG signing. Packages will be unsigned."
- exit 0
+ echo "::error::APT_GPG_KEY secret is required; refusing to publish an unsigned repository."
+ exit 1
+ fi
+ if [ -z "$APT_GPG_FINGERPRINT" ]; then
+ echo "::error::APT_GPG_FINGERPRINT repository variable is required."
+ exit 1
fi
echo "$APT_GPG_KEY" | gpg --batch --import
- KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec/{print $5; exit}')
- echo "Signing with key: $KEY_ID"
+ KEY_ID=$(gpg --list-secret-keys --with-colons --fingerprint | awk -F: '/^fpr/{print $10; exit}')
+ test -n "$KEY_ID"
+ test "$KEY_ID" = "$APT_GPG_FINGERPRINT" || {
+ echo "::error::Imported signing key fingerprint does not match APT_GPG_FINGERPRINT"
+ exit 1
+ }
cd gh-pages/apt
gpg --batch --yes --default-key "$KEY_ID" \
@@ -290,12 +388,25 @@ jobs:
# Export public key for users to download
gpg --armor --export "$KEY_ID" > ../gpg-key.asc
+ test -s dists/stable/InRelease
+ test -s dists/stable/Release.gpg
+ test -s ../gpg-key.asc
- name: Commit and push to gh-pages
+ env:
+ RELEASE_VERSION: ${{ needs.package-deb.outputs.version }}
run: |
cd gh-pages
+ test -s gpg-key.asc
git add apt/
- # Only add the GPG public key if signing ran
- [ -f gpg-key.asc ] && git add gpg-key.asc || true
- git commit -m "apt: release ${{ needs.package-deb.outputs.version }}" || echo "Nothing to commit"
- git push origin gh-pages
+ git add gpg-key.asc
+ if git diff --cached --quiet; then
+ echo "No APT repository changes to publish"
+ else
+ git commit -m "apt: release $RELEASE_VERSION"
+ fi
+ git fetch origin gh-pages
+ git rebase origin/gh-pages
+ git push origin HEAD:gh-pages
+ git fetch origin gh-pages
+ test "$(git rev-parse HEAD)" = "$(git rev-parse origin/gh-pages)"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 39677de..6d23cc5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,31 +10,58 @@ on:
- "v*"
permissions:
- contents: write
+ contents: read
env:
CARGO_TERM_COLOR: always
BINARY_NAME: alter
+ DESKTOP_BINARY_NAME: rundock
+ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
+ NPM_VERSION: 11.12.1
jobs:
+ validate-release-tag:
+ name: Validate release tag
+ runs-on: windows-2022
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Require a matching semantic version
+ shell: pwsh
+ env:
+ RELEASE_REF: ${{ github.ref_name }}
+ run: |
+ if ($env:RELEASE_REF -notmatch '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$') {
+ throw "Release tag must be a strict v-prefixed semantic version"
+ }
+ $tagVersion = $env:RELEASE_REF.Substring(1)
+ $cargoVersion = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
+ if ($tagVersion -ne $cargoVersion) {
+ throw "Release tag version does not match Cargo.toml"
+ }
+
+ quality:
+ name: Required quality gate
+ uses: ./.github/workflows/quality.yml
+ needs: validate-release-tag
+
# ─────────────────────────────────────────────────────────────────────────────
# Build Windows x64 release binary
# ─────────────────────────────────────────────────────────────────────────────
build-windows:
name: Build (Windows x64)
- runs-on: windows-latest
+ runs-on: windows-2022
+ needs: quality
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - name: Install Rust stable
- uses: dtolnay/rust-toolchain@stable
- with:
- targets: x86_64-pc-windows-msvc
+ - name: Install pinned Rust toolchain
+ run: rustup toolchain install 1.98.0 --profile minimal --target x86_64-pc-windows-msvc
- name: Cache Cargo registry
- uses: actions/cache@v4
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.cargo/registry
@@ -45,128 +72,268 @@ jobs:
${{ runner.os }}-cargo-
- name: Setup Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
- node-version: 22
+ node-version: 24.15.0
cache: npm
cache-dependency-path: web-ui/package-lock.json
- name: Build web UI
+ shell: pwsh
run: |
+ npm install --global "npm@$env:NPM_VERSION"
+ if ((npm --version) -ne $env:NPM_VERSION) { throw "Unexpected npm version" }
cd web-ui
npm ci
npm run build
- - name: Build release binary
+ - name: Build release binaries
env:
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
- run: cargo build --release --target x86_64-pc-windows-msvc
-
- # Copy binary to target/release/ where Inno Setup script expects it
- - name: Stage binary
+ ALTER_UPDATE_PUBLISHER_SHA256: ${{ vars.WINDOWS_SIGNER_SHA256 }}
+ RELEASE_REF: ${{ github.ref_name }}
+ shell: pwsh
+ run: |
+ $tagVersion = $env:RELEASE_REF.Substring(1)
+ $cargoVersion = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
+ if ($tagVersion -ne $cargoVersion) {
+ throw "Release tag version $tagVersion does not match Cargo.toml $cargoVersion"
+ }
+ if ($env:ALTER_UPDATE_PUBLISHER_SHA256 -notmatch '^[0-9A-Fa-f]{64}$') {
+ throw "WINDOWS_SIGNER_SHA256 repository variable must contain the 64-character publisher certificate SHA-256."
+ }
+ cargo build --release --locked --target x86_64-pc-windows-msvc
+ if ($LASTEXITCODE -ne 0) { throw "alter release binary build failed" }
+ cargo build --manifest-path desktop-shell\Cargo.toml --release --locked --target x86_64-pc-windows-msvc
+ if ($LASTEXITCODE -ne 0) { throw "desktop shell release binary build failed" }
+
+ # Copy binaries to the stable paths consumed by Inno Setup.
+ - name: Stage binaries
run: |
New-Item -ItemType Directory -Force -Path target\release | Out-Null
+ New-Item -ItemType Directory -Force -Path desktop-shell\target\release | Out-Null
Copy-Item target\x86_64-pc-windows-msvc\release\alter.exe target\release\alter.exe
+ Copy-Item desktop-shell\target\x86_64-pc-windows-msvc\release\rundock.exe desktop-shell\target\release\rundock.exe
+ $loader = 'desktop-shell\target\x86_64-pc-windows-msvc\release\WebView2Loader.dll'
+ if (-not (Test-Path -LiteralPath $loader)) {
+ $loader = Get-ChildItem 'desktop-shell\target\x86_64-pc-windows-msvc\release\build' -Filter WebView2Loader.dll -File -Recurse |
+ Where-Object FullName -Match '[\\/]out[\\/]x64[\\/]WebView2Loader\.dll$' |
+ Sort-Object LastWriteTimeUtc -Descending |
+ Select-Object -First 1 -ExpandProperty FullName
+ }
+ if (-not $loader) { throw 'WebView2Loader.dll was not produced by the desktop build' }
+ $signature = Get-AuthenticodeSignature -LiteralPath $loader
+ if ($signature.Status -ne 'Valid' -or
+ $signature.SignerCertificate.Subject -notmatch '(^|,\s*)O=Microsoft Corporation(,|$)') {
+ throw 'WebView2Loader.dll does not have the expected Microsoft signature'
+ }
+ Copy-Item $loader desktop-shell\target\release\WebView2Loader.dll
+
+ - name: Sign and verify binaries (required)
+ shell: pwsh
+ env:
+ WINDOWS_SIGNING_CERT_BASE64: ${{ secrets.WINDOWS_SIGNING_CERT_BASE64 }}
+ WINDOWS_SIGNING_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERT_PASSWORD }}
+ WINDOWS_SIGNER_SHA256: ${{ vars.WINDOWS_SIGNER_SHA256 }}
+ run: |
+ if ([string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERT_BASE64) -or
+ [string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERT_PASSWORD)) {
+ throw "WINDOWS_SIGNING_CERT_BASE64 and WINDOWS_SIGNING_CERT_PASSWORD are required; refusing to publish unsigned Windows binaries."
+ }
+ $pfx = Join-Path $env:RUNNER_TEMP "rundock-signing.pfx"
+ [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_SIGNING_CERT_BASE64))
+ $certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
+ $pfx,
+ $env:WINDOWS_SIGNING_CERT_PASSWORD
+ )
+ $certificateSha256 = $certificate.GetCertHashString([Security.Cryptography.HashAlgorithmName]::SHA256)
+ if ($certificateSha256 -ne $env:WINDOWS_SIGNER_SHA256) {
+ throw "signing certificate does not match WINDOWS_SIGNER_SHA256"
+ }
+ $signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
+ Sort-Object FullName -Descending | Select-Object -First 1
+ if (-not $signtool) { throw "signtool.exe was not found" }
+ $binaries = @('target\release\alter.exe', 'desktop-shell\target\release\rundock.exe')
+ foreach ($binary in $binaries) {
+ & $signtool.FullName sign /fd SHA256 /td SHA256 /tr https://timestamp.digicert.com /f $pfx /p $env:WINDOWS_SIGNING_CERT_PASSWORD $binary
+ if ($LASTEXITCODE -ne 0) { throw "binary signing failed: $binary" }
+ & $signtool.FullName verify /pa /all $binary
+ if ($LASTEXITCODE -ne 0) { throw "binary signature verification failed: $binary" }
+ }
+
+ - name: Remove signing material
+ if: always()
+ shell: pwsh
+ run: Remove-Item -LiteralPath (Join-Path $env:RUNNER_TEMP "rundock-signing.pfx") -Force -ErrorAction SilentlyContinue
- - name: Upload binary artifact
- uses: actions/upload-artifact@v4
+ - name: Upload binary artifacts
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: rundock-windows-x64
- path: target/release/alter.exe
+ path: |
+ target/release/alter.exe
+ desktop-shell/target/release/rundock.exe
+ desktop-shell/target/release/WebView2Loader.dll
+ if-no-files-found: error
# ─────────────────────────────────────────────────────────────────────────────
# Package: Inno Setup installer
# ─────────────────────────────────────────────────────────────────────────────
package-installer:
name: Package Installer
- runs-on: windows-latest
+ runs-on: windows-2022
needs: build-windows
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download binary
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rundock-windows-x64
- path: target/release
+ path: .
+
+ - name: Stage verified WebView2 bootstrapper
+ shell: pwsh
+ run: .\scripts\stage-webview2.ps1
- name: Extract version from tag
id: version
shell: pwsh
+ env:
+ RELEASE_REF: ${{ github.ref_name }}
run: |
- $tag = "${{ github.ref_name }}"
- $version = $tag.TrimStart("v")
+ if ($env:RELEASE_REF -notmatch '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$') {
+ throw "Release tag must be a strict v-prefixed semantic version"
+ }
+ $tag = $env:RELEASE_REF
+ $version = $tag.Substring(1)
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
echo "TAG=$tag" >> $env:GITHUB_OUTPUT
- name: Update version in Inno Setup script
shell: pwsh
+ env:
+ RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: |
$iss = Get-Content installer\alter-setup.iss -Raw
- $iss = $iss -replace '#define AppVersion\s+"[^"]+"', "#define AppVersion `"${{ steps.version.outputs.VERSION }}`""
+ $pattern = '#define AppVersion\s+"[^"]+"'
+ $matches = [regex]::Matches($iss, $pattern)
+ if ($matches.Count -ne 1) {
+ throw "Expected exactly one AppVersion definition, found $($matches.Count)"
+ }
+ $iss = [regex]::Replace($iss, $pattern, "#define AppVersion `"$env:RELEASE_VERSION`"")
Set-Content installer\alter-setup.iss $iss
- name: Install Inno Setup
- run: choco install innosetup --no-progress -y
+ run: choco install innosetup --version=6.7.1 --no-progress -y
- name: Build installer
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\alter-setup.iss
+ - name: Sign and verify installer (required)
+ shell: pwsh
+ env:
+ WINDOWS_SIGNING_CERT_BASE64: ${{ secrets.WINDOWS_SIGNING_CERT_BASE64 }}
+ WINDOWS_SIGNING_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERT_PASSWORD }}
+ WINDOWS_SIGNER_SHA256: ${{ vars.WINDOWS_SIGNER_SHA256 }}
+ RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
+ run: |
+ if ([string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERT_BASE64) -or
+ [string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERT_PASSWORD)) {
+ throw "WINDOWS_SIGNING_CERT_BASE64 and WINDOWS_SIGNING_CERT_PASSWORD are required; refusing to publish unsigned Windows installers."
+ }
+ $pfx = Join-Path $env:RUNNER_TEMP "rundock-signing.pfx"
+ [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_SIGNING_CERT_BASE64))
+ $certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
+ $pfx,
+ $env:WINDOWS_SIGNING_CERT_PASSWORD
+ )
+ $certificateSha256 = $certificate.GetCertHashString([Security.Cryptography.HashAlgorithmName]::SHA256)
+ if ($certificateSha256 -ne $env:WINDOWS_SIGNER_SHA256) {
+ throw "signing certificate does not match WINDOWS_SIGNER_SHA256"
+ }
+ $signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
+ Sort-Object FullName -Descending | Select-Object -First 1
+ if (-not $signtool) { throw "signtool.exe was not found" }
+ $expectedName = "RunDock-$env:RELEASE_VERSION-windows-x64-setup.exe"
+ $installer = Get-Item -LiteralPath (Join-Path "dist" $expectedName) -ErrorAction Stop
+ & $signtool.FullName sign /fd SHA256 /td SHA256 /tr https://timestamp.digicert.com /f $pfx /p $env:WINDOWS_SIGNING_CERT_PASSWORD $installer.FullName
+ if ($LASTEXITCODE -ne 0) { throw "installer signing failed" }
+ & $signtool.FullName verify /pa /all $installer.FullName
+ if ($LASTEXITCODE -ne 0) { throw "installer signature verification failed" }
+
+ - name: Remove signing material
+ if: always()
+ shell: pwsh
+ run: Remove-Item -LiteralPath (Join-Path $env:RUNNER_TEMP "rundock-signing.pfx") -Force -ErrorAction SilentlyContinue
+
+ - name: Smoke the signed installer
+ shell: pwsh
+ env:
+ RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
+ run: .\scripts\windows-installer-smoke.ps1 -InstallerPath "dist\RunDock-$env:RELEASE_VERSION-windows-x64-setup.exe" -InstallDirectory (Join-Path $env:RUNNER_TEMP 'RunDockReleaseSmoke')
+
- name: Compute SHA256
id: sha256
shell: pwsh
+ env:
+ RELEASE_VERSION: ${{ steps.version.outputs.VERSION }}
run: |
- $file = Get-ChildItem dist\*.exe | Select-Object -First 1
+ $expectedName = "RunDock-$env:RELEASE_VERSION-windows-x64-setup.exe"
+ $file = Get-Item -LiteralPath (Join-Path "dist" $expectedName) -ErrorAction Stop
$hash = (Get-FileHash $file.FullName -Algorithm SHA256).Hash.ToLower()
echo "HASH=$hash" >> $env:GITHUB_OUTPUT
echo "INSTALLER=$($file.Name)" >> $env:GITHUB_OUTPUT
Write-Host "SHA256: $hash"
- name: Upload installer artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: rundock-installer
path: dist/*.exe
- - name: Save hash for manifest step
- uses: actions/upload-artifact@v4
- with:
- name: installer-hash
- path: dist/
-
outputs:
- version: ${{ steps.version.outputs.VERSION }}
+ version: ${{ steps.version.outputs.VERSION }}
installer: ${{ steps.sha256.outputs.INSTALLER }}
- sha256: ${{ steps.sha256.outputs.HASH }}
+ sha256: ${{ steps.sha256.outputs.HASH }}
# ─────────────────────────────────────────────────────────────────────────────
# Publish GitHub Release
# ─────────────────────────────────────────────────────────────────────────────
publish-release:
name: Publish GitHub Release
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
needs: package-installer
+ concurrency:
+ group: github-release-${{ github.ref }}
+ cancel-in-progress: false
+ permissions:
+ contents: write
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download installer
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rundock-installer
path: dist/
- name: Generate release notes
id: notes
+ env:
+ RELEASE_VERSION: ${{ needs.package-installer.outputs.version }}
+ RELEASE_INSTALLER: ${{ needs.package-installer.outputs.installer }}
+ RELEASE_SHA256: ${{ needs.package-installer.outputs.sha256 }}
run: |
- VERSION="${{ needs.package-installer.outputs.version }}"
- INSTALLER="${{ needs.package-installer.outputs.installer }}"
- SHA256="${{ needs.package-installer.outputs.sha256 }}"
+ VERSION="$RELEASE_VERSION"
+ INSTALLER="$RELEASE_INSTALLER"
+ SHA256="$RELEASE_SHA256"
cat >> $GITHUB_OUTPUT << EOF
BODY</dev/null || cargo test
+ cargo test --all-targets --all-features --locked
# @group Testing : Run cargo clippy with warnings-as-errors
lint-rust:
- cargo clippy -- -D warnings
+ cargo clippy --all-targets --all-features --locked -- -D warnings
# @group Testing : Audit dependencies for known CVEs (requires cargo-audit)
audit:
@@ -47,8 +47,8 @@ fmt-rust:
# @group BuildDev : Build with tokio-console support (requires cargo-watch + unstable tokio)
# Run `tokio-console` in a separate terminal to connect
console:
- RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio-console
- RUSTFLAGS="--cfg tokio_unstable" cargo run --features tokio-console -- daemon start
+ cargo --config 'build.rustflags=["--cfg","tokio_unstable"]' build --features tokio-console
+ cargo --config 'build.rustflags=["--cfg","tokio_unstable"]' run --features tokio-console -- daemon start
# ─── Frontend ─────────────────────────────────────────────────────────────────
@@ -95,7 +95,7 @@ daemon-start:
# @group Utilities : Stop the alter daemon
daemon-stop:
- curl -s -X POST http://127.0.0.1:2999/api/v1/system/shutdown || echo "daemon not running"
+ ./target/debug/alter daemon stop
# @group Utilities : Show daemon status
daemon-status:
@@ -103,7 +103,7 @@ daemon-status:
# @group Utilities : Kill daemon so cargo build can replace the binary (Windows)
kill:
- curl -s -X POST http://127.0.0.1:2999/api/v1/system/shutdown || true
+ ./target/debug/alter daemon stop
# ─── Install dev tools ────────────────────────────────────────────────────────
diff --git a/PROJECT_AUDIT.md b/PROJECT_AUDIT.md
new file mode 100644
index 0000000..a8026f8
--- /dev/null
+++ b/PROJECT_AUDIT.md
@@ -0,0 +1,201 @@
+# RunDock P0–P2 工程整改验收报告
+
+> 验收日期:2026-08-26(Asia/Shanghai)
+> 方法:原始全面审计 + Codemap 增量审计 + Code Overhaul 复核 + SonarQube 复扫 + 全量构建/测试 + 隔离运行态 smoke
+> 项目:`rundock-alter-v1-audit`
+> 范围:仓库中的 Rust daemon/CLI、React 控制台、状态文件、外部集成、测试、CI、发布、安装与文档
+
+## 0. 结论
+
+原报告中的 **P0 4 项、P1 8 项及功能性 P2 均已完成整改并通过自动化验证**。控制面、env 文件边界、持久化、更新信任链、进程生命周期、PID 身份、出站请求、Telegram、发布权限、错误语义、资源上限和质量门禁已经从“依赖运行前提”改为代码或 CI 可执行的不变量。
+
+本轮没有把“P2 完成”解释成机械清空所有大文件或 Sonar smell。高风险职责已经完成第一轮提取并补行为测试;剩余问题是大组件继续拆分、同用户本机 TOCTOU、非 Linux POSIX 极窄 PGID 复用窗口、低覆盖率与可访问性细节,均归入 P3/后续渐进治理,不再是 P0–P2 功能阻塞。
+
+关键验收结果:
+
+- Rust:`182` 个单元测试 + `3` 个集成测试全部通过;fmt、check、clippy `-D warnings` 全部通过。
+- 前端:`25` 个测试文件、`107` 项测试全部通过;format、lint、typecheck、build 全部通过。
+- 前端覆盖率:Statements `37.58%`、Branches `36.43%`、Functions `34.00%`、Lines `39.47%`。
+- 依赖:npm `0 vulnerabilities`;RustSec `0 vulnerabilities`,仅 1 个允许的 unmaintained transitive warning。
+- Codemap:12/12 模块最新,平均 `80.9/100`;`2 A / 8 B / 2 C`;`0 HIGH / 11 MED / 7 LOW`。
+- SonarQube:分析任务成功,`0 vulnerabilities`、`0 security hotspots`、`19` 个 Bugs 且均为 Minor;Coverage `29.6%`。
+- 隔离 smoke:端口 `33991` health=`ok`、Dashboard HTTP `200`、受控 shutdown 成功且进程/监听均退出。
+
+## 1. 证据边界
+
+### 已实际完成
+
+- 检查了完整 Git diff、源文件、测试、依赖锁文件、CI、安装/发布脚本、Codemap 和本机 SonarQube 结果。
+- 使用工作区隔离目录 `.codex/validation-data/final-20260826-1051` 和端口 `33991` 启动当前构建。
+- 复核真实 `2999/5173` 监听仍属于原安装版 daemon/Vite 进程;未替换、停止或接管。
+- 运行了 Rust/前端全套门禁、依赖审计、YAML/PowerShell 语法检查和秘密扫描。
+
+### 未执行或不能由本机证明
+
+- 没有调用真实 AI、Webhook、Telegram、Tunnel、自更新、安装、发布、部署或远程写操作。
+- 没有读取或修改真实 `%APPDATA%\alter-pm2` 数据。
+- 没有本机 Git Bash,因此 Debian shell 脚本由 CI 的 `bash -n`/真实打包 smoke 负责;本机只完成静态复核。
+- 本机未安装 `cargo-llvm-cov`/`llvm-tools-preview`,所以 Sonar 只导入前端 LCOV;CI 已固定 `cargo-llvm-cov 0.9.0` 并要求 Rust line coverage 至少 20%。
+- Sonar Scanner 上传成功不等于 Quality Gate 通过;第 8 节单独报告门禁失败条件。
+
+## 2. P0 完成矩阵
+
+| ID | 状态 | 关键实现 | 验证 |
+|---|---|---|---|
+| P0-1 控制面认证/CORS | 完成 | 明文 listener 仅允许 loopback;非回环拒绝启动;CORS 仅接受受信 loopback Origin;长连接使用一次性、路径/查询绑定 ticket;普通会话不再进入 URL | auth/CORS/stream ticket/loopback 单测;隔离 daemon smoke |
+| P0-2 env 路径穿越 | 完成 | 文件名必须是单一安全 env 组件;限定到已注册进程 cwd;拒绝绝对路径、Windows/Unix traversal、ADS、符号链接/reparse point;读取 no-follow 且限制大小 | 跨平台 traversal、dangling link、merge_env 单测 |
+| P0-3 状态并发与损坏恢复 | 完成 | 配置/状态写入串行;唯一临时文件;原子替换;主/备份语义校验;跨文件 marker;损坏主文件只能从有效备份恢复,不能伪装首次运行 | atomic file、state transaction、重复 ID、并发/恢复单测 |
+| P0-4 自更新信任链 | 完成 | 固定仓库/tag/平台资产;限制重定向与大小;必须有 SHA-256;Windows 安装器还校验固定发布者证书 hash;任何缺项 fail-closed | URL、SemVer、digest、证书 hash、篡改候选单测 |
+
+## 3. P1 完成矩阵
+
+| ID | 状态 | 关键实现与证据 |
+|---|---|---|
+| P1-1 副作用/持久化部分成功 | 完成 | 进程、项目、端口和 Git 操作保存前捕获完整 snapshot;失败执行补偿恢复;无法安全回滚时返回明确 partial-state 错误,不再重复执行未知副作用。 |
+| P1-2 watcher 所有权 | 完成 | watcher handle 保存在 `ManagedProcess`,stop/restart/rollback 明确释放;创建失败不再静默忽略。 |
+| P1-3 生命周期漂移 | 完成 | start/watch/crash restart/cron/adopt 共用受控 spawn/commit;hooks、health、watcher、日志与 generation 校验一致;spawn 聚合 future 不再在 OS child 创建后被外层 timeout 取消。 |
+| P1-4 LogWriter 泄漏 | 完成 | writer 具有明确停止/Drop 所有权;替换/stop 会终止旧任务;轮转写入错误可观察。 |
+| P1-5 PID 复用/进程树 | 完成 | 保存不可变启动时间;Windows 使用稳定 HANDLE + Job Object,Linux 使用 pidfd + 独立进程组;cron/自动重启也转移树所有权;身份不明时拒绝 kill。 |
+| P1-6 AI/通知/隧道出站 | 完成 | 统一协议/主机/IP 策略,拒绝凭据 URL、私网/回环绕过与 DNS rebinding;AI 上下文脱敏并有总量限制;Device Flow 每流绑定随机 polling credential;installer pipe 始终 drain 且输出有界。 |
+| P1-7 Telegram 空白名单 | 完成 | enabled bot 必须有 token 和显式 chat/sender allowlist;checkpoint 与 token fingerprint 绑定;空名单 fail-closed。 |
+| P1-8 发布安全 | 完成 | workflow 默认 `contents: read`;打包/签名 job 无写权限;Release 上传在只消费 artifact 的独立 write job;APT 缺签名或固定 fingerprint 不匹配即停止;Actions pin SHA。 |
+
+额外复核关闭的问题:
+
+- session `DashMap` guard 不再跨 SSE/WebSocket `await` 持有,logout 不会被长连接阻塞。
+- logout、密码/PIN/安全设置变更在锁内复核 session,避免撤销竞态。
+- daemon restart handoff 的提交与外部 shutdown 由同一互斥锁线性化;外部 shutdown 先到时新 daemon 会被回收。
+- Unix 活跃进程组 leader 身份暂时不可见时 fail-closed,不会继续 signal 数字 PGID。
+
+## 4. P2 完成矩阵
+
+| 主题 | 状态 | 本轮结果 |
+|---|---|---|
+| 错误语义 | 完成 | API 统一非 2xx 与 `success:false`;UI 区分 loading/failed/empty/stale/partial;配置损坏、日志/流解析、保存和刷新错误不再显示成功 toast 或零值。 |
+| 日志/流资源上限 | 完成 | 日志 tail 从文件尾部有界读取;lines/bytes/aggregate 有硬上限;SSE/脚本/AI/provider 行和总量有界;前端日志 ring buffer。 |
+| 轮询并发 | 完成 | 引入 single-flight + AbortController + generation;页面卸载/服务器切换阻止旧响应回写;health 告警有抑制、恢复和退避。 |
+| CI/质量门禁 | 完成 | PR/main/codex 分支运行 Rust/前端质量;Release 复用门禁;固定 Rust 1.98.0、Node 24.15.0、npm 11.12.1;coverage/audit/package smoke/Sonar 等待齐备。 |
+| npm 漏洞 | 完成 | lockfile 更新后 `npm audit --audit-level=high` 为 0;未使用 `audit fix --force`。 |
+| Rust 直接依赖 | 完成 | 删除未引用 `thiserror`、`tokio-util`、`rand`,使用实际需要的 `rand_core`;保留经编译证明需要的传递依赖。 |
+| 锁文件真相 | 完成 | 删除漂移的 `bun.lock`,npm/package-lock/CI 成为唯一受支持路径。 |
+| bundle | 完成 | 路由/供应商分块并加入 500 KiB 强制预算;最大 JS chunk `290.60 kB`(gzip `72.35 kB`)。 |
+| dead/legacy | 完成 | 删除 Vite 模板 CSS/示例测试、未接入通知旧架构、rolling restart stub、未使用 hook 与旧 release 文档;保留兼容字段。 |
+| 热点职责第一轮拆分 | 完成 | App 提取 AuthGuard/AppSidebar/ServerSwitcher/StatusBar/SystemStats/ErrorBoundary;AI 提取 context/脱敏;进程核心提取 identity/tree;前端 API 提取 transport/schema/domain helper。 |
+| 测试补齐 | 完成 | 覆盖 auth/CORS/ticket、env path、atomic state、事务恢复、PID/tree/lifecycle race、出站策略、Device Flow credential、UI 错误态、single-flight、server switching 与关键页面。 |
+
+结构性 P2 的验收标准是“先提取最高风险边界并以行为测试固定语义”,不是一次把所有大文件重写。Codemap 仍将 Processes/ProcessDetail/PortFinder/Terminal/manager/AI 等标为 MED bloat,这些已降为 P3 维护债,不应在同一安全整改提交里继续做高风险大重构。
+
+## 5. 架构与数据边界
+
+整改后的核心链路:
+
+```text
+Browser / CLI
+ -> strict loopback listener + trusted Origin / Bearer / one-time ticket
+ -> Axum route validation + bounded blocking/outbound work
+ -> ProcessManager generation + ProcessIdentity + ProcessTreeGuard
+ -> serial mutation locks + runtime snapshots + compensate
+ -> atomic primary/backup pair + semantic validation + transaction marker
+```
+
+项目仍没有业务数据库;持久化是受控 JSON 文件集。没有证据支持为此引入 Redis、消息队列、微服务或数据库迁移。更合理的后续工作是继续缩小模块内部职责。
+
+秘密边界:
+
+- API 只返回 `*_set`/hint,AI、Telegram、通知等设置不再回传原始秘密。
+- ProcessInfo 对外投影不再泄露完整 env。
+- AI 诊断在出站前处理常见 key/token/header/URL query 并限制 UTF-8 字符数。
+- 当前工作树与 Sonar TextAndSecretsSensor 未发现凭据命中;这不是对完整 Git 历史的绝对保证。
+
+## 6. Codemap 最终结果
+
+本轮扫描 `225` 个跟踪文件、`75,264` 行(包含 lockfile、文档与交付资产),12 个模块全部 fresh:
+
+| 等级 | 模块 |
+|---|---|
+| A | bootstrap_daemon 90、state_persistence 93 |
+| B | frontend_shell 77、frontend_settings_ai_terminal 78、frontend_transport 82、api_process_projects 80、api_security_os 85、process_lifecycle 82、integrations_observability 84、delivery_docs 88 |
+| C | frontend_process_projects 64、frontend_operations 68 |
+
+平均分 `80.9/100`,发现项 `0 HIGH / 11 MED / 7 LOW`。交互地图与 Markdown 均已重建:`.codemap/codemap.html`、`.codemap/codemap.md`。
+
+## 7. 自动化验证
+
+| 命令 | 结果 |
+|---|---|
+| `cargo fmt --all -- --check` | 通过 |
+| `cargo check --all-targets --locked` | 通过;本机仅提示资源编译器不可用,debug build 不嵌入图标 |
+| `cargo test --all-targets --locked` | 通过;182 unit + 3 integration |
+| `cargo clippy --all-targets --all-features --locked -- -D warnings` | 通过 |
+| `npm run format:check` | 通过 |
+| `npm run lint` | 通过 |
+| `npm run typecheck` | 通过 |
+| `npm test` | 通过;25 files / 107 tests |
+| `npm run test:coverage` | 通过;37.58 / 36.43 / 34.00 / 39.47 |
+| `npm run build` | 通过;1,876 modules;max chunk 290.60 kB |
+| `npm audit --audit-level=high` | 通过;0 vulnerabilities |
+| `cargo-audit 0.22.2 audit --no-fetch` | 通过;380 dependencies;0 vulnerabilities;1 allowed warning |
+| PyYAML parse workflows/lefthook | 通过 |
+| PowerShell AST parse `scripts/release.ps1` | 通过 |
+| Codemap scan | 通过;12/12 fresh |
+| Sonar analysis task | SUCCESS;Analysis ID `d635c7f4-33fc-4e14-9a9e-dbfcee18d119` |
+
+前端测试有 jsdom 对 Canvas/navigation 的 3 条非致命提示,不影响结果。RustSec 唯一 warning 是 `serial 0.4.0` unmaintained,经 `portable-pty 0.8.1` 传递引入;没有已知 vulnerability,应在上游替换可行时升级,不应为消警贸然改 terminal backend。
+
+## 8. SonarQube 最终结果
+
+本机 SonarQube Community Build `26.8.0.126808`,最终扫描 210 个文件并成功上传:
+
+| 指标 | 原审计快照 | 最终 |
+|---|---:|---:|
+| NCLOC | 29,690 / 167 metric files | 53,281 |
+| Bugs | 33 | 19(全部 Minor) |
+| Vulnerabilities | 1 | 0 |
+| Security Hotspots | 0 | 0 |
+| Code Smells | 551 | 571 |
+| Coverage | 0.0% | 29.6% |
+| Duplication | 3.2% | 3.4% |
+
+NCLOC 和 smells 不能直接横向解释为退化:最终扫描纳入了更多生产源文件与本轮新增边界代码;本轮目标不是机械清空全部 smell。两个 `String.sort()` Critical Bugs 已改为 `localeCompare` 并在复扫后消失,剩余 19 个 Bugs 均是 Sonar 将 click/keyboard 可访问性规则归类为 Minor。
+
+### Quality Gate:真实状态是 ERROR
+
+分析任务 `SUCCESS`,但服务端门禁未通过:
+
+- new coverage `32.9% < 80%`
+- new duplicated lines density `3.82426% > 3%`
+- new violations `291 > 0`
+
+这与旧报告中“Quality Gate OK 但没有 conditions”不同:现在门禁已有 3 个真实条件,并会 fail-closed。当前工作树相对旧版本包含整轮大规模整改,且本机没有 Rust LCOV,所以无法诚实声称达到 80% 新代码覆盖率。CI 会生成 Rust LCOV,但 80% 仍是后续测试投资目标;不应通过排除业务源文件或伪造覆盖率让门禁变绿。
+
+## 9. 隔离运行态验收
+
+使用:
+
+- data:`.codex/validation-data/final-20260826-1051`
+- logs:同目录 `logs/`
+- bind:`127.0.0.1:33991`
+
+结果:
+
+- `/api/v1/system/health`:`status=ok`,version `1.1.0`
+- `/`:HTTP `200`
+- `POST /api/v1/system/shutdown`:`success=true`
+- PID `129700` 已退出,端口 `33991` 无残留 listener
+- 原端口 `2999` 仍由 `C:\Program Files\alter\alter.exe` 管理;`5173` 仍为原 Vite 进程
+
+因此 smoke 证明的是当前构建在隔离数据和备用端口可启动/关闭,不是对真实用户状态的迁移,也不是发布或部署。
+
+## 10. 剩余 P3 / 平台限制
+
+1. `manager.rs`、ProcessesPage、ProcessDetailPage、AnalyticsPage、PortFinderPage、TerminalPanel 和 `ai.rs` 仍偏大,应按既有测试逐模块拆分。
+2. 同一 Windows 用户下的恶意本机进程仍可能在 env 校验与按路径 rename 之间替换父目录;远程 traversal 与最终 symlink/reparse 已关闭。完全消除需目录句柄相对 I/O。
+3. macOS/BSD 没有可移植稳定进程组句柄;验证 leader 身份到数字 PGID signal 间有极窄复用窗口。Linux pidfd、Windows HANDLE/Job 不受该限制。
+4. 锁屏服务端 logout 是 best-effort;网络失败时本地 token 已清除,但服务端 session 会保留到 24 小时过期。
+5. `StoredPasskey` 仅为兼容 raw 字段;API/文档明确 passkey 尚未实现。
+6. 根 README 与 docs/README 有重叠内容,后续应确定单一权威来源或增加一致性检查。
+7. 本机 Sonar Rust 插件产生 comment highlighting offset 警告,且缺 Rust LCOV;CI 工具链是权威覆盖率验证路径。
+
+## 11. 最终判断
+
+RunDock 仍保持“单体 Rust daemon + React 控制台 + 本地文件状态”的合适架构,但其安全与可靠性已经不再主要依靠单用户、回环地址和低并发碰巧成立。P0/P1 的高风险入口和一致性问题已形成代码级护栏,功能性 P2 已形成测试与 CI 门禁;剩余工作是可规划、可渐进的 P3 维护和覆盖率投资。
diff --git a/PROJECT_REAUDIT.md b/PROJECT_REAUDIT.md
new file mode 100644
index 0000000..431024d
--- /dev/null
+++ b/PROJECT_REAUDIT.md
@@ -0,0 +1,290 @@
+# RunDock / Alter 第二次工程复检报告
+
+> 复检日期:2026-08-30
+> 对比基线:`PROJECT_AUDIT.md`(2026-08-26)
+> 当前分支:`codex/p0-p2-remediation`
+> 当前 HEAD:`f920c50289134fc8d6d6febba1e6e8895ee859a6`
+> 方法:Codemap 13 模块独立复审 + Code Overhaul 全面复核 + SonarQube/LCOV/CI 证据交叉验证
+> 边界:本轮只复检;未修改生产代码、测试代码、运行配置或远端状态
+
+## 0. 最终结论
+
+当前工程健康度为 **68.9/100(C)**,低于上次 Codemap 的 `80.9/100`。这不代表全部整改失效:核心架构仍合适,多数 P0 安全护栏、资源上限、依赖治理、bundle 和基础测试仍然有效;但本次更严格的调用链复核找到了 **6 个 HIGH**,其中包括开发代理暴露、Cron 进程树所有权、重复重启、首次轮询失效和 Linux 端口漏报。
+
+按上次报告的 23 个主题逐项严格复核:
+
+- **13/23 完全解决并仍有当前证据支持**;
+- **10/23 已有主要整改,但关键边界仍未闭合,改判为部分解决/重新打开**;
+- **0/23 完全未处理**。
+
+这是一种“按主题整行验收”的保守计数:某一主题只要还有一个会影响真实行为的关键分支,就不继续记为完成。
+
+本轮结论是:**可以停止大范围的代码洁癖和全面拆文件,但还不能停止本轮优化。** 应先做一轮范围很窄的回归修复,关闭第 4 节的 6 个 HIGH,并补对应行为测试;随后恢复可执行的 Sonar/CI 门禁。完成这些后即可停止,不建议继续做全面重构。
+
+## 1. 证据边界与当前状态
+
+### 1.1 Git 与代码范围
+
+- 工作分支相对 `origin/codex/p0-p2-remediation` **ahead 1**;本地提交 `f920c50` 是 Linux Clippy 条件编译修复,尚未推送,不能视为远端已验证。
+- 上次验收提交 `d3b4d36` 之后新增了 Windows daemon 重启、PID 0、桌面壳、loopback proxy、品牌与托盘/UI 修正等代码;当前 Codemap 覆盖 **236 个文件、83,436 LOC**,上次为 225 个文件、75,270 LOC。
+- 本轮仅更新审计产物:本文件和 `.codemap/`;没有 commit、push、PR 更新、运行时启停或生产变更。
+
+### 1.2 自动化验证
+
+| 验证 | 当前结果 |
+|---|---|
+| 根 Rust fmt/check/clippy | 通过 |
+| 根 Rust 测试 | 通过:189 单元 + 3 集成,共 192 个 |
+| 前端 lint/typecheck | 通过 |
+| 前端测试 | 通过:26 个测试文件、113 个测试 |
+| 前端 build | 通过:1,876 modules;最大 chunk 290.60 kB |
+| npm 高危漏洞审计 | 通过:0 vulnerabilities |
+| 前端 format:check | 本机失败:89 个 CRLF 工作树文件;Git 索引为 LF,远端 CI 同提交通过,属于 Windows 行尾可复现性问题,不记作源代码格式回归 |
+| desktop-shell fmt | 通过 |
+| desktop-shell test/clippy | 本机失败:当前工具链是 `x86_64-pc-windows-gnu` 且缺 `windres`;测试二进制还出现 `STATUS_ENTRYPOINT_NOT_FOUND`;远端 Windows installer smoke 在 `eb4ad22` 通过,因此暂定为本机工具链/产物不一致,不能宣称本地门禁通过,也不能直接定性为功能回归 |
+| cargo-audit | 本机未安装,未进行系统级安装;CI 路径仍是权威证据 |
+
+### 1.3 当前远端门禁
+
+- PR:`https://github.com/damingishere-coder/RunDock/pull/1`,状态 `OPEN / UNSTABLE`,远端 HEAD 为 `eb4ad22`。
+- 最新 Quality run `33262470419`:Frontend quality 和 Windows installer smoke 成功;Rust quality 因 Linux Clippy 失败;Linux package 与 Sonar job 随后跳过。
+- 本地 `f920c50` 已让根 Rust Clippy 通过,但尚未 push,故没有远端证据。
+- SonarQube gate configuration 因 `SONAR_HOST_URL` / `SONAR_TOKEN` 未配置而失败,当前质量门禁不是可执行状态。
+
+## 2. 上次 P0 / P1 / P2 的真实关闭状态
+
+### 2.1 P0:3/4 完全解决,1/4 部分解决
+
+| 上次主题 | 当前状态 | 复检结论 |
+|---|---|---|
+| P0-1 控制面认证/CORS | **部分解决 / 重新打开** | 生产 daemon 的回环监听、鉴权、CORS 和 stream ticket 仍有效;但 Vite 在 `0.0.0.0:5173` 提供同源 `/api`/WebSocket 代理,可把 passwordless loopback 控制面间接暴露给 LAN。 |
+| P0-2 env 路径穿越 | **完成** | 安全组件名、cwd 边界、no-follow、大小限制和跨平台 traversal 拒绝仍保留;未发现回归。 |
+| P0-3 状态并发与损坏恢复 | **完成** | 原子写、备份校验、marker 与损坏恢复主链仍成立;Telegram checkpoint 的窄并发问题单独列在 P1/P2 状态边界,不推翻核心状态事务。 |
+| P0-4 自更新信任链 | **完成** | 固定资产、摘要、发布者校验和 fail-closed 仍存在;未发现新的绕过。 |
+
+### 2.2 P1:4/8 完全解决,4/8 部分解决
+
+| 上次主题 | 当前状态 | 复检结论 |
+|---|---|---|
+| P1-1 副作用/持久化部分成功 | **部分解决** | 多数 snapshot/补偿存在;生态导入在清理或持久化失败时仍可能留下孤儿/未持久化进程,项目/进程 PATCH 也不能表达显式清空。 |
+| P1-2 watcher 所有权 | **部分解决** | 正常启动路径会处理 watcher 创建失败;恢复路径只记日志并保持 `Watching`,可能形成 `file_watcher=None` 的假监督状态。 |
+| P1-3 生命周期漂移 | **部分解决 / 重新打开** | `cron_trigger_loop` 仍复制普通 spawn/commit 流程,且退出分支漏掉 retained process tree 终止,已经出现真实语义漂移。 |
+| P1-4 LogWriter 泄漏 | **完成** | 明确停止/Drop 所有权仍在;未发现新的 writer 泄漏证据。 |
+| P1-5 PID 复用/进程树 | **部分解决 / 重新打开** | Job/pidfd/启动时间基础存在;Cron 正常退出没有释放 retained tree,树所有权创建失败时也只 kill 根进程,身份捕获失败还会被部分路径当作“未运行”。 |
+| P1-6 AI/通知/隧道出站 | **完成** | 统一出站策略、大小限制、credential 绑定和私网/回环限制仍在;未发现新绕过。 |
+| P1-7 Telegram 空白名单 | **完成** | token 与显式 allowlist 的 fail-closed 仍成立;checkpoint 单调写入竞态影响重复消费,不会把空白名单重新变成放行。 |
+| P1-8 发布安全 | **完成** | job 权限、签名、固定指纹和 pinned Actions 仍在;发布 artifact 的安装生命周期验证不足归入 P2 门禁完整性。 |
+
+### 2.3 P2:6/11 完全解决,5/11 部分解决
+
+| 上次主题 | 当前状态 | 复检结论 |
+|---|---|---|
+| 错误语义 | **部分解决** | 统一 API 错误主链有效,但部分页面仍把 sleeping/starting 错判,日期筛选和不可清空配置会给出错误状态。 |
+| 日志/流资源上限 | **完成** | 有界 tail、aggregate、SSE/AI/provider 限制与前端 ring buffer 仍在。 |
+| 轮询并发 | **部分解决 / 重新打开** | single-flight/AbortController 存在,但 `enabled=false` 同时禁掉首次 tick,Tunnels 及部分 hooks 可以永远不做初始加载。 |
+| CI/质量门禁 | **部分解决 / 重新打开** | 门禁文件存在,但当前 PR 为红;Sonar 配置 job 失败;desktop-shell 未进入 Sonar/覆盖率/独立 fmt-clippy;Linux/正式 release artifact smoke 不完整。 |
+| npm 漏洞 | **完成** | 当前 `npm audit --audit-level=high` 为 0。 |
+| Rust 直接依赖 | **完成** | 未发现上次已删除依赖重新引入。 |
+| 锁文件真相 | **完成** | npm/package-lock/CI 单一路径仍成立。 |
+| bundle | **完成** | 当前最大 chunk 290.60 kB,仍低于 500 KiB 预算。 |
+| dead/legacy | **完成** | 未发现值得本轮继续处理的新死代码;没有为了清理而清理的必要。 |
+| 热点职责第一轮拆分 | **部分解决** | 已提取的边界仍有价值;但 Cron 与普通 spawn 的重复已造成进程树缺陷,ProcessManager 的该段重复需要定点合流。 |
+| 测试补齐 | **部分解决** | 测试数量增加且主套件通过,但缺少重复重启、首次 tick、Linux `ss`、Cron tree cleanup 和 Vite LAN 边界测试;desktop-shell 本机套件未通过。 |
+
+## 3. Codemap 第二次结果
+
+### 3.1 总体变化
+
+| 指标 | 上次 | 当前 | 说明 |
+|---|---:|---:|---|
+| 模块数 | 12 | 13 | 新增 `desktop_shell` 正式模块 |
+| 文件 / LOC | 225 / 75,270 | 236 / 83,436 | 桌面壳和后续功能进入地图 |
+| 平均健康度 | 80.9 | **68.9** | 下降 12.0;既有范围扩大,也有本次发现的 HIGH 与更严格的调用链审查 |
+| 等级分布 | 2A / 8B / 2C | **0A / 6B / 3C / 4D** | D 集中在进程前端、操作页、系统端口和生命周期 |
+| Findings | 0 HIGH / 11 MED / 7 LOW | **6 HIGH / 33 MED / 6 LOW** | 报告只展开当前有业务价值的 HIGH 和关键 MED |
+
+### 3.2 当前模块评分
+
+| 模块 | 分数 | 等级 | 当前判断 |
+|---|---:|---:|---|
+| frontend_shell | 76 | B | App/ServerSwitcher 仍大,但非本轮阻塞 |
+| frontend_process_projects | 52 | D | 重复重启、不可清空配置、页面职责过载 |
+| frontend_operations | 52 | D | Tunnels 首次加载、Linux 状态兼容、日期/状态语义 |
+| frontend_settings_ai_terminal | 78 | B | Terminal 大,但未发现新高风险回归 |
+| frontend_transport | 68 | C | `enabled` 与初始加载语义耦合、运行时 schema 边界偏弱 |
+| bootstrap_daemon | 78 | B | PID 身份未知/legacy 记录仍有 fail-closed 缺口 |
+| api_process_projects | 62 | C | PATCH clear 语义、导入补偿与总超时边界 |
+| api_security_os | 56 | D | Linux `ss` 解析漏报、跨平台端口语义漂移 |
+| process_lifecycle | 52 | D | Cron 复制路径与进程树清理是当前最高维护风险 |
+| state_persistence | 84 | B | 核心状态稳健;checkpoint 单调写入缺少同锁事务 |
+| integrations_observability | 84 | B | 未发现新 P0/P1;AI 路由大文件暂缓处理 |
+| desktop_shell | 82 | B | 代码边界清晰;错误可观察性和质量门禁仍需补齐 |
+| delivery_docs | 72 | C | Vite LAN 代理、Sonar/安装生命周期门禁缺口 |
+
+Codemap 已重新生成:`.codemap/codemap.md` 与 `.codemap/codemap.html`,13/13 模块均为 fresh。
+
+## 4. 当前仍值得继续处理的问题
+
+以下只列会影响安全、运行正确性或门禁可信度的问题;普通大文件、低收益拆分和依赖追新不进入本轮。
+
+### R0-1 Vite 开发代理破坏 loopback-only 假设
+
+- **优先级:P0 / HIGH;预计工作量:S**
+- 证据:`web-ui/vite.config.ts:72-80` 将开发服务器绑定 `0.0.0.0`,并把 `/api` 和 WebSocket 代理到 `127.0.0.1:2999`;文档明确说 passwordless 模式只能在 loopback 使用。
+- 影响:同一局域网中的客户端可经 `:5173` 间接访问本机 daemon;Vite 代理让 daemon 看到的是本机来源,削弱生产端的 loopback/CORS 防线。
+- 建议:默认只绑定 loopback;若确需 LAN 调试,使用显式 opt-in 且强制控制面密码,并加入行为测试。
+
+### R1-1 Cron 与进程树所有权仍会漂移
+
+- **优先级:P1 / HIGH;预计工作量:M**
+- 证据:`src/process/manager.rs:2323-2337` 的 Cron 退出路径清空 PID/identity/log writer,却没有调用 retained tree 终止;`src/process/runner.rs:280-285` 和 `src/process/hooks.rs:60-65` 在 tree guard 创建失败时只 kill 根进程。
+- 影响:带 descendants 的 Cron/job/hook 在边界失败或根进程提前退出后可能残留,下一次运行覆盖旧 guard,违背上次声称已统一的进程树所有权。
+- 建议:把 Cron 的结束/提交路径真正合流到共享生命周期原语;对 guard 创建失败和正常退出都做树级清理,并补 Unix/Windows 定向测试。
+
+### R1-2 “保存并重启”会连续重启两次
+
+- **优先级:P1 / HIGH;预计工作量:S**
+- 证据:`web-ui/src/components/EnvFileModal.tsx:142-143` 先直接 `restartProcess` 再调用 `onRestart`;`ProcessDetailPage.tsx:395-400` 的回调又调用相同 API,且第二次没有被当前 handler await。
+- 影响:一次用户操作会连续重启两次,可能重复 hooks、通知、日志切换和短暂端口冲突。
+- 建议:保留一个 restart 所有者,并以“API 仅调用一次”的组件测试锁定。
+
+### R1-3 条件轮询禁用了首次加载
+
+- **优先级:P1 / HIGH;预计工作量:S**
+- 证据:`web-ui/src/pages/TunnelsPage.tsx:513-516` 在初始 tunnels 为空时传入 `enabled=false`;`useSingleFlightPoll` 不再执行首次 tick,页面可一直 loading,只有手动刷新才能恢复。`useProcesses`/`useProjects` 也共享同类语义风险。
+- 影响:整改轮询重叠时引入了“初始请求与周期请求被同一开关关闭”的功能回归。
+- 建议:把 `initialLoad` 与 `intervalEnabled` 分开;测试首次 mount、关闭自动刷新、服务器切换三种状态。
+
+### R1-4 Linux `ss` 监听端口会被当成 netstat 丢弃
+
+- **优先级:P1 / HIGH;预计工作量:S**
+- 证据:`src/api/routes/ports.rs:248` 用 `fields.len() >= 7` 判断 netstat;常见 `ss -Hntlpu` 带 users 字段时同样达到 7 列,于是把 RecvQ 当成本地地址并丢弃整行。`ss` 命令成功后不会回退 netstat。
+- 影响:Linux 监听端口页、端口占用判断和 Project WebPort 关联可能漏报。
+- 建议:按命令来源使用独立 parser,加入真实 `ss`/`netstat` fixture;前端统一复用已能接受 `LISTEN`/`LISTENING` 的 `processWeb.ts` helper。
+
+### R1-5 状态更新仍缺少“显式清空”和同锁单调提交
+
+- **优先级:P1 / MED;预计工作量:M**
+- 证据:`ProcessPatch`/`ProjectPatch` 的 `Option` 无法区分字段缺失与 JSON `null`,cron/cwd/notify/web_port/launch_uri 等已设值不能可靠清空;`telegram_checkpoint::save` 在同一锁外先 load/校验再 write,并发写入可发生较小 offset 后写覆盖。
+- 影响:UI 看似保存成功但旧配置残留;Telegram 极端并发下会重复消费更新。
+- 建议:使用可表达 absent/null/value 的 patch 类型;把 checkpoint 读-校验-写放进同一个文件级临界区。
+
+### R2-1 质量门禁仍不能证明“当前提交可发布”
+
+- **优先级:P2 / MED;预计工作量:M**
+- 当前 PR 为红,本地修复尚未推送验证;Sonar 配置 job 失败。
+- `desktop-shell` 未进入 Sonar、覆盖率和独立 fmt/clippy;当前本机 GNU 工具链也不能执行其测试。
+- Linux package smoke 未安装 `.deb`/运行 maintainer scripts/systemd 健康检查;正式 Windows release artifact 没有再做安装/升级/卸载 smoke。
+- 前端覆盖率相对上次下降约 2–3 个百分点,新增测试没有跟上新增源代码。
+- 建议:先恢复 Sonar 认证与远端绿灯,再补 desktop-shell 和最终发布 artifact 的最小可发布性验证;不要求把覆盖率机械抬到 80%,只要求新增高风险路径有行为测试。
+
+## 5. Code Overhaul 复核
+
+### 5.1 架构与范围
+
+“单体 Rust daemon + React 控制台 + Windows 桌面壳 + 本地 JSON 状态”仍符合当前产品规模。没有证据支持拆成微服务、引入数据库或替换技术栈。真正的问题不是单体,而是少数职责在不同路径重复实现:普通启动与 Cron 启动、初始加载与周期轮询、API patch 与 UI 表单状态。
+
+关键链路仍清晰:
+
+```text
+Desktop shell / Browser
+ │
+ ▼
+React UI ── transport/auth/polling ── Axum API
+ │
+ ┌──────────────────┼──────────────────┐
+ ▼ ▼ ▼
+ Process lifecycle State/config OS/Providers
+ │ │ │
+ Job/pidfd/PGID Atomic JSON Ports/AI/Tunnel
+```
+
+### 5.2 复杂度与重复代码
+
+- `src/process/manager.rs` 约 3,431 行,且 Cron 区段复制了普通 spawn/commit 主链;这项重复已经产生实际 HIGH,因此值得定点合流。
+- `ProcessesPage`、`ProcessDetailPage`、`PortFinderPage`、`AnalyticsPage`、`TerminalPanel`、`api.ts` 和 `ai.rs` 仍大,但除报告列出的具体语义错误外,**不建议本轮全面拆分**。
+- 通知默认值、统计转换和部分页面状态存在重复,尚未造成同等级生产问题,继续优化收益低于回归风险。
+
+### 5.3 依赖、性能与安全
+
+- npm 高危漏洞为 0;Cargo 直接依赖未发现重新膨胀。
+- npm/Cargo 有常规 minor/major 更新可用,但没有当前漏洞或故障驱动,不建议本轮追新。
+- bundle 预算仍通过;日志/流的有界策略仍有效,没有发现新的性能阻塞证据。
+- 生产控制面、env 边界、更新签名和出站策略总体维持上轮加固结果;当前安全重点只保留 Vite LAN 代理这一条。
+
+### 5.4 明确不建议继续处理的项目
+
+- 为了降低单文件行数而全面拆 App/Pages/AI/Terminal。
+- 机械清空 Sonar smells、Minor 可访问性告警或所有重复代码。
+- 无漏洞驱动的依赖大版本升级。
+- 引入微服务、数据库、全新状态框架或重写前端。
+- 上次已归入 P3 的本机同用户 TOCTOU、非 Linux POSIX 极窄 PGID 窗口,除非出现真实故障证据。
+
+## 6. SonarQube 前后指标
+
+### 6.1 可比较结果
+
+| 指标 | 上次已认证扫描 | 当前 | 变化结论 |
+|---|---:|---:|---|
+| NCLOC | 53,281 | N/A | 当前扫描未认证,不能比较 |
+| Bugs | 19 Minor | N/A | 不能比较 |
+| Vulnerabilities | 0 | N/A | 不能比较 |
+| Security Hotspots | 0 | N/A | 不能比较 |
+| Code Smells | 571 | N/A | 不能比较 |
+| Sonar Coverage | 29.6% | N/A | 不能比较 |
+| Duplication | 3.4% | N/A | 不能比较 |
+| Quality Gate | ERROR | 配置 job FAILURE / scan 不可执行 | 没有改善证据,门禁可用性反而退化 |
+
+本机 SonarQube `127.0.0.1:9000` 可达,但当前没有可用认证;Scanner 请求 `/api/v2/analysis/version` 返回 `401 Unauthorized`。远端 workflow 的 `SONAR_HOST_URL` 和 `SONAR_TOKEN` 也为空,因此不能生成可信的“整改后第二次 Sonar 指标”。本报告不会把 2026-08-26 的 `.scannerwork/report-task.txt` 旧结果冒充当前结果。
+
+### 6.2 前端 LCOV 的可比较旁证
+
+这不是 Sonar 全项目指标,但使用同一前端测试覆盖率口径,可以作为趋势旁证:
+
+| 前端覆盖率 | 上次 | 当前 | 变化 |
+|---|---:|---:|---:|
+| Statements | 37.58% | 35.01% | -2.57 pp |
+| Branches | 36.43% | 34.12% | -2.31 pp |
+| Functions | 34.00% | 32.29% | -1.71 pp |
+| Lines | 39.47% | 36.52% | -2.95 pp |
+
+测试从 107 增加到 113,但新增代码增长更快;覆盖率趋势小幅下降。重点应补第 4 节的高风险行为测试,不应为了数字盲目补低价值行覆盖。
+
+## 7. 回归判断
+
+### 已确认的回归/遗漏
+
+1. Tunnels 的首次加载被整改后的条件轮询关闭。
+2. Env 保存并重启形成双重 API 调用。
+3. Linux 端口 parser 的启发式判断会丢弃常见 `ss` 输出。
+4. Cron 仍有复制生命周期路径并遗漏树清理;上次“已统一”的结论不完整。
+
+这些关键代码多数可追溯到上次主整改提交 `c2de2b1`,在 `PROJECT_AUDIT.md` 生成时已经存在,属于 **上次验收漏检的整改回归/未闭合边界**,不是本次复检期间产生的代码变化。
+
+### 后续提交是否引入新回归
+
+- 未发现 `d3b4d36` 之后的品牌、托盘、底栏和 loopback proxy 修改造成新的已证实 P0/P1 生产回归。
+- 新增 desktop-shell 扩大了发布面,但当前主要证据是质量门禁覆盖不足和本机 GNU/MSVC 工具链不一致;还不能据此宣称已发生桌面功能回归。
+- 当前远端 CI 红、Sonar 不可执行本身就是发布风险;在绿灯恢复前不能说当前分支已达到可发布状态。
+
+## 8. 影响 / 工作量与停止条件
+
+| 顺序 | 问题 | 影响 | 工作量 | 建议 |
+|---:|---|---|---|---|
+| 1 | Vite LAN 代理旁路 | 高 | S | 立即关闭默认外网绑定并测试 |
+| 2 | Cron/guard 进程树所有权 | 高 | M | 合流生命周期,补树级清理测试 |
+| 3 | Env 双重重启 | 高 | S | 单一 restart 所有者 + 调用次数测试 |
+| 4 | 首次轮询被禁用 | 高 | S | 分离 initial load 与 interval |
+| 5 | Linux `ss` 漏报 | 高 | S | 独立 parser + fixture |
+| 6 | Patch/checkpoint 一致性 | 中 | M | 支持显式清空;同锁单调提交 |
+| 7 | CI/Sonar/desktop/release smoke | 中 | M | 恢复可执行门禁并验证当前 SHA |
+
+达到以下条件后,可以停止本轮优化:
+
+1. 第 4 节 6 个 HIGH 全部关闭,并有对应行为测试;
+2. 根 Rust、前端、desktop-shell 的适用门禁全部通过;
+3. 当前提交在远端 CI 变绿,Linux package 不再被跳过;
+4. Sonar 能对当前 SHA 完成一次认证扫描,并如实记录 gate;
+5. 不再开启大文件全面拆分、依赖追新或 smell 清零。
+
+在此之前:**不建议继续做新的功能性优化,也不建议发布或合并;但建议把修复范围严格限制在上述问题。**
diff --git a/README.md b/README.md
index 9cf9922..923bad3 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-> A polished local project and process manager for Windows (and cross-platform). RunDock keeps related services together, exposes logs and ports when needed, and retains the compatible `alter` CLI for existing scripts.
+> A polished local project and process manager for Windows (and cross-platform). RunDock keeps related services together and exposes logs and ports when needed.
[](./LICENSE)
[](https://www.rust-lang.org/)
@@ -18,7 +18,27 @@
### Manual installer
Download the latest `RunDock-x.x.x-windows-x64-setup.exe` from [Releases](https://github.com/damingishere-coder/RunDock/releases) and run it.
-RunDock is the product name; the compatible `alter.exe` command is added to your `PATH` automatically.
+The installer adds the RunDock command-line tools to your `PATH` automatically.
+The installer adds the `rundock.exe` desktop app. It opens the local dashboard in
+its own window, starts the daemon when needed, stays available in the system
+tray, and enables current-user login startup by default. Closing the window
+hides it to the tray; **Exit RunDock** closes only the desktop app and leaves the
+daemon and managed projects running.
+
+### Debian / Ubuntu (signed APT repository)
+
+Download the public key, inspect its fingerprint against the key attached to the matching [GitHub Release](https://github.com/damingishere-coder/RunDock/releases), then install it:
+
+```bash
+curl -fsSL https://damingishere-coder.github.io/RunDock/gpg-key.asc -o rundock-release-key.asc
+gpg --show-keys --fingerprint rundock-release-key.asc
+sudo gpg --dearmor --yes -o /usr/share/keyrings/rundock-archive-keyring.gpg rundock-release-key.asc
+echo "deb [signed-by=/usr/share/keyrings/rundock-archive-keyring.gpg] https://damingishere-coder.github.io/RunDock/apt stable main" | sudo tee /etc/apt/sources.list.d/rundock.list
+sudo apt update
+sudo apt install alter
+```
+
+The packaged service is opt-in: `sudo systemctl enable --now alter-daemon.service`. System mode uses `/var/lib/alter-pm2` and `/var/log/alter-pm2`; it is isolated from per-user data.
---
@@ -33,7 +53,7 @@ RunDock is the product name; the compatible `alter.exe` command is added to your
- **State persistence** — save and restore your process list across reboots
- **Ecosystem config** — define all apps in one TOML or JSON file
- **Full REST API** — automate everything
-- **Single binary** — no runtime dependencies
+- **Self-contained Windows installer** — desktop app plus the compatible CLI/daemon
- **Dashboard authentication** — password-protect the web UI with Argon2id hashing, session tokens, and a PIN quick-unlock
- **Telegram bot** — control your processes from Telegram: list, start, stop, restart, tail logs, and receive crash/restart alerts
- **AI assistant** — multi-provider chat panel (Ollama, GitHub Models, Claude, OpenAI-compatible) with streaming responses and process-aware context
@@ -45,13 +65,20 @@ RunDock is the product name; the compatible `alter.exe` command is added to your
### Build from source
-Requires [Rust](https://rustup.rs/).
+Requires [Rust 1.98](https://rustup.rs/), Node.js 24, and npm. The dashboard
+must be built before Rust embeds `web-ui/dist` into the binary.
```powershell
git clone https://github.com/damingishere-coder/RunDock
cd RunDock
-cargo build --release
+cd web-ui
+npm ci
+npm run build
+cd ..
+cargo build --release --locked
# Binary: target\release\alter.exe
+cargo build --manifest-path desktop-shell\Cargo.toml --release --locked
+# Desktop app: desktop-shell\target\release\rundock.exe
```
---
@@ -65,7 +92,7 @@ alter daemon start
# Start processes
alter start python -- -m http.server 8080
alter start node --name api -- server.js
-alter start "go run main.go" --name backend --cwd C:\projects\api
+alter start go --name backend --cwd C:\projects\api -- run main.go
# List processes
alter list
@@ -85,10 +112,20 @@ RunDock is built with Windows as a first-class platform:
- Spawned processes use `CREATE_NO_WINDOW` — **no black console popups**
- Daemon runs completely hidden in the background
+- `rundock.exe` provides a single-instance WebView2 window and system tray
+- Login startup is enabled by default with `--background` and can be toggled from the tray
+- Closing or exiting the desktop app never stops managed projects
- `npm`, `yarn`, `npx` and other `.cmd` scripts work directly
- Terminal button opens Windows Terminal or `cmd.exe` in the process directory
- Data stored in `%APPDATA%\alter-pm2\`
+If the desktop window cannot reach the daemon, its local failure page offers
+**Retry**, **Open log directory**, **Copy diagnostics**, and **Open in browser**.
+An occupied or incompatible port 2999 is reported without ending that listener.
+If WebView2 is unavailable, repair the RunDock installation or install the
+Microsoft Edge WebView2 Evergreen Runtime; the installer normally deploys it
+automatically after verifying Microsoft's signature.
+
---
## Ecosystem Config
diff --git a/build.rs b/build.rs
index d1c8589..577c98f 100644
--- a/build.rs
+++ b/build.rs
@@ -12,11 +12,24 @@ fn main() {
println!("cargo:rerun-if-env-changed=GH_OAUTH_CLIENT_ID");
println!("cargo:rerun-if-changed=assets/rundock-icon.ico");
- if cfg!(windows) {
+ if cfg!(windows) && std::env::var_os("CARGO_FEATURE_WINDOWS_RESOURCE").is_some() {
let mut resource = winresource::WindowsResource::new();
resource.set_icon("assets/rundock-icon.ico");
- resource
- .compile()
- .expect("failed to embed the RunDock Windows icon");
+ resource.set("ProductName", "RunDock");
+ resource.set("FileDescription", "RunDock compatible CLI and daemon");
+ resource.set("CompanyName", "DAMING");
+ resource.set("OriginalFilename", "alter.exe");
+ if let Err(error) = resource.compile() {
+ let profile = std::env::var("PROFILE").unwrap_or_default();
+ let in_ci = std::env::var("CI")
+ .map(|value| value.eq_ignore_ascii_case("true") || value == "1")
+ .unwrap_or(false);
+ if profile == "release" || in_ci {
+ panic!("failed to embed the RunDock Windows icon: {error}");
+ }
+ println!(
+ "cargo:warning=Windows resource compiler unavailable; continuing the local {profile} build without an embedded icon: {error}"
+ );
+ }
}
}
diff --git a/desktop-shell/Cargo.lock b/desktop-shell/Cargo.lock
new file mode 100644
index 0000000..3aee280
--- /dev/null
+++ b/desktop-shell/Cargo.lock
@@ -0,0 +1,6901 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "alter"
+version = "1.1.0"
+dependencies = [
+ "anyhow",
+ "argon2",
+ "async-stream",
+ "axum",
+ "bytes",
+ "chrono",
+ "clap",
+ "copilot-client",
+ "cron",
+ "dashmap",
+ "dirs 5.0.1",
+ "dotenvy",
+ "futures",
+ "libc",
+ "mime_guess",
+ "notify",
+ "portable-pty",
+ "rand_core 0.6.4",
+ "reqwest 0.12.28",
+ "rust-embed",
+ "serde",
+ "serde_json",
+ "sha2 0.10.9",
+ "socket2 0.5.10",
+ "sysinfo",
+ "tokio",
+ "tokio-stream",
+ "toml 0.8.2",
+ "tower-http",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+ "uuid",
+ "windows 0.58.0",
+ "winresource",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures 0.2.17",
+ "password-hash",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "atk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "auto-launch"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
+dependencies = [
+ "dirs 4.0.0",
+ "thiserror 1.0.69",
+ "winreg 0.10.1",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "axum"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
+dependencies = [
+ "axum-core",
+ "axum-macros",
+ "base64 0.22.1",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sha1",
+ "sync_wrapper",
+ "tokio",
+ "tokio-tungstenite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-macros"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "blocking"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.13.1",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
+dependencies = [
+ "serde",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "cc"
+version = "1.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chacha20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.1",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "combine"
+version = "4.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "cookie"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
+dependencies = [
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "copilot-client"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05af63157660994ef55d49e0ec37b7b22d30b5994377e41cede63b4e6f5ac095"
+dependencies = [
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-graphics"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.10.1",
+ "core-graphics-types",
+ "foreign-types 0.5.0",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.10.1",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "cron"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f8c3e73077b4b4a6ab1ea5047c37c57aee77657bc8ecd6f29b0af082d0b0c07"
+dependencies = [
+ "chrono",
+ "nom",
+ "once_cell",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ctor"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
+dependencies = [
+ "ctor-proc-macro",
+ "dtor",
+]
+
+[[package]]
+name = "ctor-proc-macro"
+version = "0.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "dashmap"
+version = "6.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+
+[[package]]
+name = "dbus"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.1",
+ "const-oid",
+ "crypto-common 0.2.2",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys 0.3.7",
+]
+
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys 0.4.1",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys 0.5.0",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users 0.4.6",
+ "winapi",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users 0.4.6",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users 0.5.2",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "dom_query"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
+dependencies = [
+ "bit-set",
+ "cssparser",
+ "foldhash",
+ "html5ever",
+ "precomputed-hash",
+ "selectors",
+ "tendril",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dtor"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4"
+dependencies = [
+ "dtor-proc-macro",
+]
+
+[[package]]
+name = "dtor-proc-macro"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "embed-resource"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml 1.1.4+spec-1.1.0",
+ "vswhom",
+ "winreg 0.55.0",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset 0.9.1",
+ "rustc_version",
+]
+
+[[package]]
+name = "filedescriptor"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
+dependencies = [
+ "libc",
+ "thiserror 1.0.69",
+ "winapi",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared 0.3.1",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fsevent-sys"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
+
+[[package]]
+name = "futures-util"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.13.1",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.14.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+dependencies = [
+ "log",
+ "markup5ever",
+]
+
+[[package]]
+name = "http"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "hyper"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2 0.6.5",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core 0.62.2",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ico"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
+dependencies = [
+ "byteorder",
+ "png 0.17.16",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
+
+[[package]]
+name = "icu_properties"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
+
+[[package]]
+name = "icu_provider"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "moxcms",
+ "num-traits",
+ "png 0.18.1",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "infer"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "inotify"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
+dependencies = [
+ "bitflags 1.3.2",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ioctl-rs"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jiff"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
+dependencies = [
+ "defmt",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
+dependencies = [
+ "bitflags 2.13.1",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "kqueue"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea"
+dependencies = [
+ "kqueue-sys",
+ "libc",
+]
+
+[[package]]
+name = "kqueue-sys"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
+dependencies = [
+ "bitflags 2.13.1",
+ "libc",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "markup5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
+dependencies = [
+ "log",
+ "tendril",
+ "web_atoms",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "muda"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.20",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.13.1",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys 0.3.1",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nix"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "libc",
+ "memoffset 0.6.5",
+ "pin-utils",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "notify"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
+dependencies = [
+ "bitflags 2.13.1",
+ "crossbeam-channel",
+ "filetime",
+ "fsevent-sys",
+ "inotify",
+ "kqueue",
+ "libc",
+ "log",
+ "mio 0.8.11",
+ "walkdir",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags 2.13.1",
+ "dispatch2",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-core-graphics"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
+dependencies = [
+ "bitflags 2.13.1",
+ "dispatch2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-text"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-core-text",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "openssl"
+version = "0.10.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
+dependencies = [
+ "bitflags 2.13.1",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
+
+[[package]]
+name = "plist"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap 2.14.0",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.13.1",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "portable-pty"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be"
+dependencies = [
+ "anyhow",
+ "bitflags 1.3.2",
+ "downcast-rs",
+ "filedescriptor",
+ "lazy_static",
+ "libc",
+ "log",
+ "nix",
+ "serial",
+ "shared_library",
+ "shell-words",
+ "winapi",
+ "winreg 0.10.1",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit 0.25.13+spec-1.1.0",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pxfm"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
+
+[[package]]
+name = "quick-xml"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2 0.6.5",
+ "thiserror 2.0.20",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
+dependencies = [
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand 0.10.2",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.20",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2 0.6.5",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
+dependencies = [
+ "rand_chacha",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.1",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams 0.4.2",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams 0.5.0",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rundock-desktop"
+version = "1.1.0"
+dependencies = [
+ "alter",
+ "anyhow",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-autostart",
+ "tauri-plugin-single-instance",
+ "tokio",
+ "url",
+ "windows 0.61.3",
+]
+
+[[package]]
+name = "rust-embed"
+version = "8.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9"
+dependencies = [
+ "axum",
+ "mime_guess",
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "tokio",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097"
+dependencies = [
+ "mime_guess",
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "syn 2.0.119",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0"
+dependencies = [
+ "sha2 0.11.0",
+ "walkdir",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "selectors"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
+dependencies = [
+ "bitflags 2.13.1",
+ "cssparser",
+ "derive_more",
+ "log",
+ "new_debug_unreachable",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "servo_arc",
+ "smallvec",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a"
+dependencies = [
+ "base64 0.22.1",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.0",
+ "jiff",
+ "schemars 0.9.0",
+ "schemars 1.2.2",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "serial"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86"
+dependencies = [
+ "serial-core",
+ "serial-unix",
+ "serial-windows",
+]
+
+[[package]]
+name = "serial-core"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "serial-unix"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7"
+dependencies = [
+ "ioctl-rs",
+ "libc",
+ "serial-core",
+ "termios",
+]
+
+[[package]]
+name = "serial-windows"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162"
+dependencies = [
+ "libc",
+ "serial-core",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.1",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shared_library"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
+dependencies = [
+ "lazy_static",
+ "libc",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
+dependencies = [
+ "bytemuck",
+ "js-sys",
+ "ndk",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "redox_syscall",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "string_cache"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "symlink"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "sysinfo"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+ "memchr",
+ "ntapi",
+ "windows 0.57.0",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
+dependencies = [
+ "bitflags 2.13.1",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.2",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.35.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
+dependencies = [
+ "bitflags 2.13.1",
+ "block2",
+ "core-foundation 0.10.1",
+ "core-graphics",
+ "crossbeam-channel",
+ "dbus",
+ "dispatch2",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-sys",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "raw-window-handle",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri"
+version = "2.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cookie",
+ "dirs 6.0.0",
+ "dunce",
+ "embed_plist",
+ "getrandom 0.3.4",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "image",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "percent-encoding",
+ "plist",
+ "raw-window-handle",
+ "reqwest 0.13.4",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror 2.0.20",
+ "tokio",
+ "tray-icon",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows 0.61.3",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs 6.0.0",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5"
+dependencies = [
+ "base64 0.22.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png 0.17.16",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2 0.10.9",
+ "syn 2.0.119",
+ "tauri-utils",
+ "thiserror 2.0.20",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
+dependencies = [
+ "anyhow",
+ "glob",
+ "plist",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-plugin-autostart"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70"
+dependencies = [
+ "auto-launch",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "tauri-plugin-single-instance"
+version = "2.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3214becf9ef5783c0ae99a3bb25adf5353a7a16ebf53e74b909e29205735c6c"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tauri",
+ "thiserror 2.0.20",
+ "tokio",
+ "tracing",
+ "windows-sys 0.60.2",
+ "zbus",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.20",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows 0.61.3",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows 0.61.3",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dom_query",
+ "dunce",
+ "glob",
+ "http",
+ "infer",
+ "json-patch",
+ "log",
+ "memchr",
+ "phf",
+ "plist",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.20",
+ "toml 1.1.4+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6"
+dependencies = [
+ "dunce",
+ "embed-resource",
+ "toml 1.1.4+spec-1.1.0",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tendril"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
+dependencies = [
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "termios"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
+dependencies = [
+ "thiserror-impl 2.0.20",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio 1.2.2",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.6.5",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
+dependencies = [
+ "futures-util",
+ "log",
+ "tokio",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "libc",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.4+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.13+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow 1.0.4",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.13.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-appender"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
+dependencies = [
+ "crossbeam-channel",
+ "symlink",
+ "thiserror 2.0.20",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e"
+dependencies = [
+ "crossbeam-channel",
+ "dirs 6.0.0",
+ "libappindicator",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.20",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "tungstenite"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.9.5",
+ "sha1",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "uds_windows"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
+dependencies = [
+ "memoffset 0.9.1",
+ "tempfile",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-ucd-ident"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+dependencies = [
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web_atoms"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"
+dependencies = [
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
+dependencies = [
+ "thiserror 2.0.20",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
+dependencies = [
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "windows-sys 0.59.0",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
+dependencies = [
+ "windows-core 0.57.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
+dependencies = [
+ "windows-core 0.58.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link 0.1.3",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
+dependencies = [
+ "windows-implement 0.57.0",
+ "windows-interface 0.57.0",
+ "windows-result 0.1.2",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
+dependencies = [
+ "windows-implement 0.58.0",
+ "windows-interface 0.58.0",
+ "windows-result 0.2.0",
+ "windows-strings 0.1.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.58.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+dependencies = [
+ "windows-result 0.2.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winreg"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "winresource"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087"
+dependencies = [
+ "toml 1.1.4+spec-1.1.0",
+ "version_check",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+
+[[package]]
+name = "wry"
+version = "0.55.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
+dependencies = [
+ "base64 0.22.1",
+ "block2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs 6.0.0",
+ "dom_query",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "libc",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2 0.10.9",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.20",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows 0.61.3",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zbus"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 1.0.4",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zvariant",
+]
+
+[[package]]
+name = "zcheapstr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+
+[[package]]
+name = "zvariant"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 1.0.4",
+ "zcheapstr",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.4",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 3.0.4",
+ "winnow 1.0.4",
+]
diff --git a/desktop-shell/Cargo.toml b/desktop-shell/Cargo.toml
new file mode 100644
index 0000000..5f5e8ac
--- /dev/null
+++ b/desktop-shell/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "rundock-desktop"
+version = "1.1.0"
+edition = "2021"
+description = "RunDock Windows desktop shell"
+build = "build.rs"
+
+[[bin]]
+name = "rundock"
+path = "src/main.rs"
+test = false
+
+[dependencies]
+serde_json = "1"
+url = "2"
+
+[target.'cfg(windows)'.dependencies]
+alter = { path = "..", default-features = false }
+anyhow = "1"
+tauri = { version = "2.11", features = ["image-png", "tray-icon"] }
+tauri-plugin-autostart = "2.5"
+tauri-plugin-single-instance = "2.4"
+tokio = { version = "1", features = ["time"] }
+windows = { version = "0.61", features = ["Win32_UI_WindowsAndMessaging"] }
+
+[target.'cfg(windows)'.build-dependencies]
+tauri-build = "2.6"
diff --git a/desktop-shell/build.rs b/desktop-shell/build.rs
new file mode 100644
index 0000000..c3c4dc4
--- /dev/null
+++ b/desktop-shell/build.rs
@@ -0,0 +1,7 @@
+#[cfg(windows)]
+fn main() {
+ tauri_build::build()
+}
+
+#[cfg(not(windows))]
+fn main() {}
diff --git a/desktop-shell/capabilities/default.json b/desktop-shell/capabilities/default.json
new file mode 100644
index 0000000..4272f29
--- /dev/null
+++ b/desktop-shell/capabilities/default.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "local-shell",
+ "description": "Local startup page only; no remote RunDock page receives Tauri IPC permissions.",
+ "local": true,
+ "windows": ["main"],
+ "permissions": ["core:default"]
+}
diff --git a/desktop-shell/src/lib.rs b/desktop-shell/src/lib.rs
new file mode 100644
index 0000000..3874d84
--- /dev/null
+++ b/desktop-shell/src/lib.rs
@@ -0,0 +1,162 @@
+use url::Url;
+
+pub const DASHBOARD_URL: &str = "http://127.0.0.1:2999/";
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum LaunchMode {
+ Foreground,
+ Background,
+ QuitExisting,
+}
+
+impl LaunchMode {
+ pub fn from_args(args: impl IntoIterator- ) -> Self {
+ let mut background = false;
+ for argument in args {
+ match argument.as_str() {
+ "--quit" => return Self::QuitExisting,
+ "--background" => background = true,
+ _ => {}
+ }
+ }
+ if background {
+ Self::Background
+ } else {
+ Self::Foreground
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum NavigationDecision {
+ AllowInternal,
+ OpenExternal,
+ ShellAction,
+ Deny,
+}
+
+pub fn classify_navigation(url: &Url) -> NavigationDecision {
+ if url.scheme() == "rundock-shell" {
+ return NavigationDecision::ShellAction;
+ }
+ if (url.scheme() == "tauri" && url.host_str() == Some("localhost"))
+ || (matches!(url.scheme(), "http" | "https") && url.host_str() == Some("tauri.localhost"))
+ {
+ return NavigationDecision::AllowInternal;
+ }
+ if url.scheme() == "http"
+ && url.host_str() == Some("127.0.0.1")
+ && url.port_or_known_default() == Some(2999)
+ && url.username().is_empty()
+ && url.password().is_none()
+ {
+ return NavigationDecision::AllowInternal;
+ }
+ if matches!(url.scheme(), "http" | "https") {
+ return NavigationDecision::OpenExternal;
+ }
+ if matches!(
+ url.scheme(),
+ "file" | "javascript" | "data" | "about" | "blob" | "devtools"
+ ) {
+ return NavigationDecision::Deny;
+ }
+ if url.scheme().len() >= 2
+ && url.scheme().chars().all(|character| {
+ character.is_ascii_alphanumeric() || matches!(character, '+' | '-' | '.')
+ })
+ {
+ return NavigationDecision::OpenExternal;
+ }
+ NavigationDecision::Deny
+}
+
+#[cfg(windows)]
+mod windows_app;
+
+#[cfg(windows)]
+pub use windows_app::run;
+
+#[cfg(windows)]
+pub fn show_fatal_error(detail: &str) {
+ use windows::core::HSTRING;
+ use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_ICONERROR, MB_OK};
+
+ let message = format!(
+ "RunDock 桌面窗口无法启动。请修复或重新安装 RunDock;如果系统缺少组件,请安装 Microsoft Edge WebView2 Runtime。\n\n详细信息:{detail}"
+ );
+ unsafe {
+ let _ = MessageBoxW(
+ None,
+ &HSTRING::from(message),
+ &HSTRING::from("RunDock"),
+ MB_OK | MB_ICONERROR,
+ );
+ }
+}
+
+#[cfg(not(windows))]
+pub fn run() -> Result<(), Box
> {
+ Err("RunDock desktop shell is supported on Windows only".into())
+}
+
+#[cfg(not(windows))]
+pub fn show_fatal_error(detail: &str) {
+ eprintln!("RunDock: {detail}");
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn launch_mode_is_explicit_and_quit_wins() {
+ assert_eq!(LaunchMode::from_args(Vec::new()), LaunchMode::Foreground);
+ assert_eq!(
+ LaunchMode::from_args(vec!["--background".to_string()]),
+ LaunchMode::Background
+ );
+ assert_eq!(
+ LaunchMode::from_args(vec!["--background".to_string(), "--quit".to_string()]),
+ LaunchMode::QuitExisting
+ );
+ }
+
+ #[test]
+ fn only_the_canonical_dashboard_is_embedded() {
+ assert_eq!(
+ classify_navigation(&Url::parse("http://tauri.localhost/index.html").unwrap()),
+ NavigationDecision::AllowInternal
+ );
+ assert_eq!(
+ classify_navigation(&Url::parse("http://127.0.0.1:2999/processes").unwrap()),
+ NavigationDecision::AllowInternal
+ );
+ assert_eq!(
+ classify_navigation(&Url::parse("http://localhost:2999/").unwrap()),
+ NavigationDecision::OpenExternal
+ );
+ assert_eq!(
+ classify_navigation(&Url::parse("http://127.0.0.1:5173/").unwrap()),
+ NavigationDecision::OpenExternal
+ );
+ assert_eq!(
+ classify_navigation(
+ &Url::parse("https://github.com/damingishere-coder/RunDock").unwrap()
+ ),
+ NavigationDecision::OpenExternal
+ );
+ assert_eq!(
+ classify_navigation(&Url::parse("wanmotai://open").unwrap()),
+ NavigationDecision::OpenExternal
+ );
+ assert_eq!(
+ classify_navigation(&Url::parse("file:///C:/Windows/System32/cmd.exe").unwrap()),
+ NavigationDecision::Deny
+ );
+ assert_eq!(
+ classify_navigation(&Url::parse("rundock-shell://retry").unwrap()),
+ NavigationDecision::ShellAction
+ );
+ }
+}
diff --git a/desktop-shell/src/main.rs b/desktop-shell/src/main.rs
new file mode 100644
index 0000000..549f62a
--- /dev/null
+++ b/desktop-shell/src/main.rs
@@ -0,0 +1,8 @@
+#![cfg_attr(windows, windows_subsystem = "windows")]
+
+fn main() {
+ if let Err(error) = rundock_desktop::run() {
+ rundock_desktop::show_fatal_error(&error.to_string());
+ std::process::exit(1);
+ }
+}
diff --git a/desktop-shell/src/windows_app.rs b/desktop-shell/src/windows_app.rs
new file mode 100644
index 0000000..948867a
--- /dev/null
+++ b/desktop-shell/src/windows_app.rs
@@ -0,0 +1,407 @@
+use crate::{classify_navigation, LaunchMode, NavigationDecision, DASHBOARD_URL};
+use alter::daemon::lifecycle::ensure_daemon;
+use std::path::{Path, PathBuf};
+use std::sync::atomic::{AtomicBool, Ordering};
+use tauri::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem};
+use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
+use tauri::webview::{NewWindowResponse, WebviewWindowBuilder};
+use tauri::{AppHandle, Manager, WebviewUrl, WindowEvent};
+use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
+
+const HOST: &str = "127.0.0.1";
+const PORT: u16 = 2999;
+const AUTOSTART_MARKER: &str = "desktop-shell-autostart.json";
+const TRAY_NOTICE_MARKER: &str = "desktop-shell-tray-notice.json";
+const SKIP_AUTOSTART_ENV: &str = "RUNDOCK_SKIP_AUTOSTART_INIT";
+const TRAY_NOTICE_MESSAGE: &str = "RunDock 已缩小到系统托盘。左键托盘图标可重新打开;只有在托盘菜单选择“退出 RunDock”时才会关闭桌面端。后台项目会继续运行。";
+
+struct LaunchState {
+ in_progress: AtomicBool,
+ quitting: AtomicBool,
+ tray_notice_shown: AtomicBool,
+}
+
+fn should_intercept_close(quitting: bool) -> bool {
+ !quitting
+}
+
+fn show_native_message(title: &'static str, message: String, error: bool) {
+ std::thread::spawn(move || {
+ use windows::core::HSTRING;
+ use windows::Win32::UI::WindowsAndMessaging::{
+ MessageBoxW, MB_ICONERROR, MB_ICONINFORMATION, MB_OK, MB_SETFOREGROUND,
+ };
+
+ let style = MB_OK
+ | MB_SETFOREGROUND
+ | if error {
+ MB_ICONERROR
+ } else {
+ MB_ICONINFORMATION
+ };
+ unsafe {
+ let _ = MessageBoxW(None, &HSTRING::from(message), &HSTRING::from(title), style);
+ }
+ });
+}
+
+fn claim_tray_notice(shown: &AtomicBool, marker: &Path) -> bool {
+ if shown.swap(true, Ordering::SeqCst) || marker.exists() {
+ return false;
+ }
+ if let Err(error) =
+ alter::config::atomic_file::write_with_backup(marker, br#"{"shown":true}"#, None)
+ {
+ eprintln!(
+ "RunDock could not persist the tray notice marker {}: {error}",
+ marker.display()
+ );
+ }
+ true
+}
+
+fn show_tray_notice_once(app: &AppHandle) {
+ let state = app.state::();
+ let marker = alter::config::paths::data_dir().join(TRAY_NOTICE_MARKER);
+ if claim_tray_notice(&state.tray_notice_shown, &marker) {
+ show_native_message("RunDock", TRAY_NOTICE_MESSAGE.to_string(), false);
+ }
+}
+
+fn request_exit(app: &AppHandle) {
+ app.state::()
+ .quitting
+ .store(true, Ordering::SeqCst);
+ app.exit(0);
+}
+
+fn open_with_windows(target: &str) {
+ use std::os::windows::process::CommandExt;
+ const CREATE_NO_WINDOW: u32 = 0x08000000;
+ let _ = std::process::Command::new("explorer.exe")
+ .arg(target)
+ .creation_flags(CREATE_NO_WINDOW)
+ .spawn();
+}
+
+fn sibling_daemon_exe() -> Result {
+ if let Some(override_path) = std::env::var_os("RUNDOCK_DAEMON_EXE") {
+ let path = PathBuf::from(override_path);
+ if path.is_file() {
+ return Ok(path);
+ }
+ return Err(format!(
+ "RUNDOCK_DAEMON_EXE 指向的文件不存在:{}",
+ path.display()
+ ));
+ }
+ let current = std::env::current_exe().map_err(|error| error.to_string())?;
+ let path = current
+ .parent()
+ .ok_or_else(|| "无法确定 RunDock 安装目录".to_string())?
+ .join("alter.exe");
+ if path.is_file() {
+ Ok(path)
+ } else {
+ Err("未找到 RunDock 后台程序。请重新安装 RunDock。".to_string())
+ }
+}
+
+fn show_window(app: &AppHandle) {
+ if let Some(window) = app.get_webview_window("main") {
+ let _ = window.unminimize();
+ let _ = window.show();
+ let _ = window.set_focus();
+ }
+}
+
+fn render_launching(app: &AppHandle) {
+ if let Some(window) = app.get_webview_window("main") {
+ let _ = window.eval("window.__setLaunching && window.__setLaunching()");
+ }
+}
+
+fn render_failure(app: &AppHandle, message: &str) {
+ if let Some(window) = app.get_webview_window("main") {
+ let logs = alter::config::paths::daemon_log_file();
+ let diagnostic = format!("{message}\n\n日志:{}", logs.display());
+ if let Ok(serialized) = serde_json::to_string(&diagnostic) {
+ let _ = window.eval(format!(
+ "window.__setLaunchError && window.__setLaunchError({serialized})"
+ ));
+ }
+ }
+}
+
+fn launch_dashboard(app: AppHandle) {
+ let state = app.state::();
+ if state.in_progress.swap(true, Ordering::SeqCst) {
+ return;
+ }
+ render_launching(&app);
+ tauri::async_runtime::spawn(async move {
+ tokio::time::sleep(std::time::Duration::from_millis(150)).await;
+ let result = match sibling_daemon_exe() {
+ Ok(daemon_exe) => ensure_daemon(&daemon_exe, HOST, PORT).await.map(|_| ()),
+ Err(error) => Err(anyhow::anyhow!(error)),
+ };
+ app.state::()
+ .in_progress
+ .store(false, Ordering::SeqCst);
+ match result {
+ Ok(()) => {
+ if let (Some(window), Ok(url)) =
+ (app.get_webview_window("main"), DASHBOARD_URL.parse())
+ {
+ let _ = window.navigate(url);
+ }
+ }
+ Err(error) => render_failure(&app, &error.to_string()),
+ }
+ });
+}
+
+fn handle_shell_action(app: &AppHandle, action: Option<&str>) {
+ match action {
+ Some("retry") => launch_dashboard(app.clone()),
+ Some("open-logs") => open_with_windows(&alter::config::paths::data_dir().to_string_lossy()),
+ Some("open-browser") => open_with_windows(DASHBOARD_URL),
+ _ => {}
+ }
+}
+
+fn initialize_autostart(app: &AppHandle) {
+ if std::env::var_os(SKIP_AUTOSTART_ENV).is_some() {
+ return;
+ }
+ let marker = alter::config::paths::data_dir().join(AUTOSTART_MARKER);
+ if marker.exists() {
+ return;
+ }
+ if app.autolaunch().enable().is_ok() {
+ let _ = alter::config::atomic_file::write_with_backup(
+ &marker,
+ br#"{"initialized":true}"#,
+ None,
+ );
+ }
+}
+
+fn create_main_window(app: &tauri::App) -> tauri::Result<()> {
+ let navigation_app = app.handle().clone();
+ let new_window_app = app.handle().clone();
+ let close_app = app.handle().clone();
+ let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
+ .title("RunDock")
+ .inner_size(1280.0, 820.0)
+ .min_inner_size(960.0, 640.0)
+ .visible(false)
+ .on_navigation(move |url| match classify_navigation(url) {
+ NavigationDecision::AllowInternal => true,
+ NavigationDecision::OpenExternal => {
+ open_with_windows(url.as_str());
+ false
+ }
+ NavigationDecision::ShellAction => {
+ handle_shell_action(&navigation_app, url.host_str());
+ false
+ }
+ NavigationDecision::Deny => false,
+ })
+ .on_new_window(move |url, _| {
+ match classify_navigation(&url) {
+ NavigationDecision::OpenExternal => open_with_windows(url.as_str()),
+ NavigationDecision::ShellAction => {
+ handle_shell_action(&new_window_app, url.host_str())
+ }
+ NavigationDecision::AllowInternal | NavigationDecision::Deny => {}
+ }
+ NewWindowResponse::Deny
+ })
+ .build()?;
+ let close_window = window.clone();
+ window.on_window_event(move |event| {
+ if let WindowEvent::CloseRequested { api, .. } = event {
+ if !should_intercept_close(
+ close_app
+ .state::()
+ .quitting
+ .load(Ordering::SeqCst),
+ ) {
+ return;
+ }
+ api.prevent_close();
+ match close_window.hide() {
+ Ok(()) => show_tray_notice_once(&close_app),
+ Err(error) => {
+ let _ = close_window.show();
+ let _ = close_window.set_focus();
+ show_native_message(
+ "RunDock 无法缩小到托盘",
+ format!("窗口仍保持打开。请稍后重试。\n\n详细信息:{error}"),
+ true,
+ );
+ }
+ }
+ }
+ });
+ Ok(())
+}
+
+fn create_tray(app: &tauri::App) -> tauri::Result<()> {
+ let open = MenuItem::with_id(app, "open", "打开 RunDock", true, None::<&str>)?;
+ let browser = MenuItem::with_id(app, "browser", "在浏览器中打开", true, None::<&str>)?;
+ let autostart = CheckMenuItem::with_id(
+ app,
+ "autostart",
+ "登录 Windows 时自动启动",
+ true,
+ app.autolaunch().is_enabled().unwrap_or(false),
+ None::<&str>,
+ )?;
+ let separator = PredefinedMenuItem::separator(app)?;
+ let quit = MenuItem::with_id(
+ app,
+ "quit",
+ "退出 RunDock(项目继续运行)",
+ true,
+ None::<&str>,
+ )?;
+ let menu = Menu::with_items(app, &[&open, &browser, &autostart, &separator, &quit])?;
+ let autostart_item = autostart.clone();
+ TrayIconBuilder::with_id("rundock-tray")
+ .icon(tauri::image::Image::from_bytes(include_bytes!(
+ "../../assets/rundock-icon-1024.png"
+ ))?)
+ .tooltip("RunDock")
+ .menu(&menu)
+ .show_menu_on_left_click(false)
+ .on_tray_icon_event(|tray, event| {
+ if matches!(
+ event,
+ TrayIconEvent::Click {
+ button: MouseButton::Left,
+ button_state: MouseButtonState::Up,
+ ..
+ }
+ ) {
+ show_window(tray.app_handle());
+ }
+ })
+ .on_menu_event(move |app, event| match event.id().as_ref() {
+ "open" => show_window(app),
+ "browser" => open_with_windows(DASHBOARD_URL),
+ "autostart" => {
+ let manager = app.autolaunch();
+ let enabled = manager.is_enabled().unwrap_or(false);
+ let changed = if enabled {
+ manager.disable().is_ok()
+ } else {
+ manager.enable().is_ok()
+ };
+ if changed {
+ let _ = autostart_item.set_checked(!enabled);
+ }
+ }
+ "quit" => request_exit(app),
+ _ => {}
+ })
+ .build(app)?;
+ Ok(())
+}
+
+pub fn run() -> Result<(), Box> {
+ let mode = LaunchMode::from_args(std::env::args().skip(1));
+ let mut builder = tauri::Builder::default();
+ builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, _| {
+ let duplicate_mode = LaunchMode::from_args(args);
+ if duplicate_mode == LaunchMode::QuitExisting {
+ request_exit(app);
+ } else {
+ show_window(app);
+ }
+ }));
+ builder = builder.plugin(tauri_plugin_autostart::init(
+ MacosLauncher::LaunchAgent,
+ Some(vec!["--background"]),
+ ));
+ builder
+ .manage(LaunchState {
+ in_progress: AtomicBool::new(false),
+ quitting: AtomicBool::new(false),
+ tray_notice_shown: AtomicBool::new(false),
+ })
+ .setup(move |app| {
+ if mode == LaunchMode::QuitExisting {
+ request_exit(app.handle());
+ return Ok(());
+ }
+ initialize_autostart(app.handle());
+ create_main_window(app)?;
+ create_tray(app)?;
+ if mode == LaunchMode::Foreground {
+ show_window(app.handle());
+ }
+ launch_dashboard(app.handle().clone());
+ Ok(())
+ })
+ .run(tauri::generate_context!())?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::time::{SystemTime, UNIX_EPOCH};
+
+ #[test]
+ fn close_is_intercepted_until_an_explicit_quit() {
+ assert!(should_intercept_close(false));
+ assert!(!should_intercept_close(true));
+ }
+
+ #[test]
+ fn tray_notice_is_claimed_once_and_persists_across_processes() {
+ let unique = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("system clock should be after the Unix epoch")
+ .as_nanos();
+ let directory = std::env::temp_dir().join(format!(
+ "rundock-tray-notice-{}-{unique}",
+ std::process::id()
+ ));
+ let marker = directory.join(TRAY_NOTICE_MARKER);
+ let shown = AtomicBool::new(false);
+
+ assert!(claim_tray_notice(&shown, &marker));
+ assert!(marker.is_file());
+ assert!(!claim_tray_notice(&shown, &marker));
+
+ let next_process = AtomicBool::new(false);
+ assert!(!claim_tray_notice(&next_process, &marker));
+
+ std::fs::remove_dir_all(directory).expect("temporary marker directory should be removable");
+ }
+
+ #[test]
+ fn tray_notice_stays_once_per_process_when_persistence_fails() {
+ let unique = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("system clock should be after the Unix epoch")
+ .as_nanos();
+ let parent_file = std::env::temp_dir().join(format!(
+ "rundock-tray-notice-parent-{}-{unique}",
+ std::process::id()
+ ));
+ std::fs::write(&parent_file, b"not a directory")
+ .expect("temporary parent file should be writable");
+ let marker = parent_file.join(TRAY_NOTICE_MARKER);
+ let shown = AtomicBool::new(false);
+
+ assert!(claim_tray_notice(&shown, &marker));
+ assert!(!marker.exists());
+ assert!(!claim_tray_notice(&shown, &marker));
+
+ std::fs::remove_file(parent_file).expect("temporary parent file should be removable");
+ }
+}
diff --git a/desktop-shell/tauri.conf.json b/desktop-shell/tauri.conf.json
new file mode 100644
index 0000000..362ff9e
--- /dev/null
+++ b/desktop-shell/tauri.conf.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "productName": "RunDock",
+ "version": "1.1.0",
+ "identifier": "com.daming.rundock",
+ "build": {
+ "frontendDist": "ui"
+ },
+ "app": {
+ "windows": [],
+ "security": {
+ "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src http://127.0.0.1:2999"
+ }
+ },
+ "bundle": {
+ "active": false,
+ "icon": [
+ "../assets/rundock-icon.ico"
+ ]
+ }
+}
diff --git a/desktop-shell/ui/index.html b/desktop-shell/ui/index.html
new file mode 100644
index 0000000..42a63c0
--- /dev/null
+++ b/desktop-shell/ui/index.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+ RunDock 正在启动
+
+
+
+
+
+
+
+ RunDock 暂时无法打开
+ 没有结束任何未知进程,也没有修改你的项目状态。你可以重试或查看诊断信息。
+
+
+
+ 正式入口固定为 http://127.0.0.1:2999/;5173 仅用于源码开发。
+
+
+
+
diff --git a/docs/API.md b/docs/API.md
index f7b9a91..82503a8 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1,6 +1,6 @@
# REST API Reference
-> The alter daemon exposes a full HTTP REST API on `http://127.0.0.1:2999/api/v1`.
+> The RunDock daemon exposes a full HTTP REST API on `http://127.0.0.1:2999/api/v1`.
> All request and response bodies use JSON. All endpoints return standard HTTP status codes.
---
@@ -34,7 +34,17 @@ Removing the dashboard password returns the daemon to passwordless mode. Keep th
Authorization: Bearer
```
-For EventSource / SSE connections that cannot set request headers, append `?token=` to the URL.
+For EventSource / SSE connections that cannot set request headers, first use the bearer token to request a one-time, path-bound stream ticket. The ticket expires after 30 seconds and is consumed by the first matching request; never put a session or master token in a URL.
+
+```http
+POST /api/v1/stream-ticket
+Authorization: Bearer
+Content-Type: application/json
+
+{ "path": "/processes//logs/stream" }
+```
+
+Then open `/api/v1/processes//logs/stream?ticket=` with `EventSource`.
**Unauthenticated response (401):**
```json
@@ -122,6 +132,9 @@ Start a new process.
| `watch_paths` | string[] | no | `[]` | Paths to watch |
| `watch_ignore` | string[] | no | `[]` | Patterns to ignore |
| `max_log_size_mb` | number | no | `10` | Log rotation threshold |
+| `cron` | string | no | null | Cron expression for scheduled execution |
+| `notify` | NotificationConfig | no | null | Process-level notification override |
+| `log_alert` | LogAlertOverride | no | null | Process-level log alert override |
**Example:**
```json
@@ -334,7 +347,18 @@ GET /api/v1/processes/api/logs/stream
**Client example (JavaScript):**
```javascript
-const es = new EventSource('/api/v1/processes/api/logs/stream');
+const path = '/processes/api/logs/stream';
+const ticketResponse = await fetch('/api/v1/stream-ticket', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${sessionToken}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ path }),
+});
+if (!ticketResponse.ok) throw new Error(`ticket request failed: ${ticketResponse.status}`);
+const { ticket } = await ticketResponse.json();
+const es = new EventSource(`/api/v1${path}?ticket=${encodeURIComponent(ticket)}`);
es.onmessage = (e) => {
const line = JSON.parse(e.data);
console.log(`[${line.stream}] ${line.content}`);
@@ -357,9 +381,11 @@ GET /api/v1/system/health
```json
{
"status": "ok",
- "version": "0.3.0",
+ "version": "1.1.0",
"uptime_secs": 3600,
- "process_count": 5
+ "process_count": 5,
+ "persistence_healthy": true,
+ "persistence_error": null
}
```
@@ -421,10 +447,11 @@ POST /api/v1/system/restart
```
**Behaviour:**
-1. Saves the current process state to disk
-2. Removes the PID file
-3. Spawns a detached watcher (`cmd /C timeout 1 && alter daemon start` on Windows, `sh -c 'sleep 1 && alter daemon start'` on Linux/macOS)
-4. Exits — managed processes survive because they are spawned outside the daemon's job object
+1. Validates and saves the current process/project state
+2. Directly starts the same executable as a replacement with a private handoff token and readiness file
+3. Stops accepting mutations, joins background writers, and releases listener/PID ownership only after the final save
+4. Accepts the replacement only after its PID identity, port ownership, and health response match; otherwise terminates it and resumes the current daemon
+5. Managed processes survive and are re-adopted only when their persisted PID identity still matches
**Response:**
```json
@@ -735,17 +762,32 @@ Notification settings are stored at `%APPDATA%\alter-pm2\notifications.json` and
"webhook": { "url": "https://example.com/hook", "enabled": true },
"slack": { "webhook_url": "https://hooks.slack.com/...", "enabled": true, "channel": "#alerts" },
"teams": { "webhook_url": "https://outlook.office.com/...", "enabled": false },
+ "discord": { "webhook_url": "https://discord.com/api/webhooks/...", "enabled": false },
+ "events_override": true,
"events": { "on_crash": true, "on_restart": true, "on_start": false, "on_stop": false }
}
```
-> All channel fields are optional — omit any you don't need. `channel` on Slack overrides the webhook's default channel.
+> All channel fields are optional — omit any you don't need. `channel` on Slack overrides the webhook's default channel. Set `events_override: true` when this scope must explicitly override inherited event flags, including the valid “all events disabled” case; legacy records without it continue to inherit when every event flag is false.
+
+### `PATCH /processes/{id}/notifications`
+
+Update only the process-level notification override without restarting the process.
+
+```json
+PATCH /api/v1/processes/api/notifications
+{ "notify": { /* NotificationConfig, or null to inherit */ } }
+```
+
+**Response:** the updated process object.
+
+---
---
### `GET /notifications`
-Return the full notifications store (global config + all namespace overrides).
+Return the notification store shape (global config + all namespace overrides). Secret URL fields are redacted as `__RUNDOCK_SECRET_SET__`; sending that placeholder back in an update preserves the currently stored secret instead of replacing it.
```
GET /api/v1/notifications
@@ -821,7 +863,7 @@ POST /api/v1/notifications/test
**Response:**
```json
-{ "success": true, "message": "test notification dispatched" }
+{ "success": true, "message": "test notification delivered to 1 target(s)" }
```
**Config cascade priority:** process-level `notify` → namespace config → global config. The first non-null value per channel wins.
@@ -848,6 +890,25 @@ Desktop launch URIs must use a validated non-HTTP custom protocol such as `wanmo
---
+## Complete Route Inventory
+
+The detailed examples above cover the most common calls. This inventory is kept aligned with `src/api/mod.rs` and the Axum route declarations so less common capabilities remain discoverable. All routes use the `/api/v1` prefix; only `/auth/*` is public. Streaming routes require a short-lived ticket from `POST /stream-ticket`.
+
+| Area | Current routes |
+|------|----------------|
+| Process extensions | `GET /processes/{id}/metrics/history`, `GET /processes/{id}/logs/stats`, `GET /processes/{id}/cron/history`, `PATCH /processes/{id}/enabled`, `PATCH /processes/{id}/project`, `PATCH /processes/{id}/notifications`, `POST /processes/{id}/clone`, `GET /processes/{id}/envfiles`, `GET/PUT /processes/{id}/envfile` |
+| Namespace lifecycle | `POST /processes/namespace/{ns}/start`, `/stop`, `/restart` |
+| Git | `GET /processes/{id}/git`, `POST /processes/{id}/git/pull` |
+| System | `GET /system/stats`, `/paths`, `/check-env`, `/list-env`, `/read-env`, `/browse`; `POST /system/write-env`, `/sync-env`, `/open-folder`; `GET/PUT /system/ui-settings`; `PUT /system/ui-settings/view-mode` |
+| Saved scripts | `GET/POST /scripts`, `GET/DELETE /scripts/{name}`, `GET /scripts/{name}/run` |
+| AI | `GET/PUT /ai/settings`, `POST /ai/chat`, `POST /ai/auth/start`, `GET /ai/auth/status`, `DELETE /ai/auth`, `GET /ai/models` |
+| Ports | `GET /ports`, `POST /ports/kill/{pid}` (body must repeat the confirmed port and process name) |
+| Tunnels | `GET/POST /tunnels`, `POST /tunnels/{id}/stop`, `DELETE /tunnels/{id}`, `GET/PUT /tunnels/settings`, `POST /tunnels/settings/test`, `POST /tunnels/settings/install`, `GET /tunnels/settings/install/stream` |
+| Terminals | `GET /terminals`, `GET /terminals/ws`, `GET/PUT /terminals/history/{key}` |
+| Operations | `GET /metrics`, `GET/PUT /log-alerts`, `PUT/DELETE /log-alerts/namespace/{ns}`, `GET /system/update/check`, `POST /system/update/apply` |
+
+---
+
## HTTP Status Codes
| Code | Meaning |
@@ -864,7 +925,7 @@ Desktop launch URIs must use a validated non-HTTP custom protocol such as `wanmo
## CORS
-The API allows cross-origin requests from any origin. All HTTP methods and headers are permitted.
+Same-origin calls are supported normally. Cross-origin browser calls are accepted only from loopback HTTP origins on the daemon port or the Vite development port `5173`; arbitrary internet, LAN, `null`, file, and HTTPS origins are rejected. Allowed methods are GET, POST, PUT, PATCH, DELETE, and OPTIONS, with `Authorization` and `Content-Type` request headers.
---
@@ -897,6 +958,10 @@ curl -X POST http://localhost:2999/api/v1/processes \
-H "Content-Type: application/json" \
-d '{"script":"python","name":"api","args":["-m","http.server","8080"]}'
-# Stream logs
-curl -N http://localhost:2999/api/v1/processes/api/logs/stream
+# Stream logs after requesting a one-time ticket with an authenticated bearer request
+ticket=$(curl -fsS -X POST http://localhost:2999/api/v1/stream-ticket \
+ -H "Authorization: Bearer $ALTER_SESSION_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"path":"/processes/api/logs/stream"}' | jq -r .ticket)
+curl -N "http://localhost:2999/api/v1/processes/api/logs/stream?ticket=$ticket"
```
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 5f5db0c..d251cad 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1,14 +1,14 @@
# Architecture
-> How alter works under the hood — daemon design, process lifecycle, logging pipeline, and data flow.
+> How RunDock works under the hood — daemon design, process lifecycle, logging pipeline, and data flow.
---
## Overview
-alter is a **single binary** that plays two roles depending on how it is invoked:
+The RunDock backend is a **single binary** that plays two roles depending on how it is invoked:
-1. **CLI** — the user-facing interface (`alter start`, `alter list`, etc.)
+1. **CLI** — RunDock's command-line interface (`alter start`, `alter list`, etc.)
2. **Daemon** — a long-running background HTTP server that manages processes
```
@@ -53,10 +53,10 @@ src/
│ └── mod.rs
├── api/
│ ├── routes/
-│ │ ├── processes.rs # /processes/* endpoints
-│ │ ├── system.rs # /system/* endpoints (health, save, shutdown)
-│ │ ├── ecosystem.rs # /ecosystem endpoint
-│ │ └── logs.rs # (reserved)
+│ │ ├── processes.rs, projects.rs, git.rs
+│ │ ├── auth.rs, system.rs, update.rs, ports.rs
+│ │ ├── ai.rs, telegram.rs, notifications.rs, log_alerts.rs
+│ │ └── tunnels.rs, terminal.rs, scripts.rs, metrics.rs
│ ├── error.rs # ApiError → HTTP response conversion
│ ├── middleware.rs
│ └── mod.rs
@@ -69,6 +69,9 @@ src/
│ ├── manager.rs # ProcessManager — high-level lifecycle API
│ ├── instance.rs # ManagedProcess — per-process state
│ ├── runner.rs # Spawn child + pipe stdout/stderr
+│ ├── identity.rs # Immutable PID identity + verified termination
+│ ├── tree.rs # Windows Job / Unix process-group ownership
+│ ├── health.rs # Bounded loopback health probes
│ ├── restarter.rs # Auto-restart loop with exponential backoff
│ ├── watcher.rs # File system watcher (watch mode)
│ └── mod.rs
@@ -80,6 +83,8 @@ src/
├── config/
│ ├── ecosystem.rs # AppConfig + EcosystemConfig structs
│ ├── paths.rs # Platform-aware data/log paths
+│ ├── atomic_file.rs # Bounded atomic JSON + validated LKG recovery
+│ ├── state_transaction.rs # Crash-recoverable state/projects pair
│ ├── daemon_config.rs # DaemonConfig (port, host)
│ └── mod.rs
├── models/
@@ -90,9 +95,12 @@ src/
├── client/
│ ├── daemon_client.rs # reqwest HTTP client (CLI → daemon)
│ └── mod.rs
-├── web/
-│ ├── assets/ # index.html, app.js, style.css
-│ └── mod.rs # rust-embed serving
+├── web/ # rust-embed serving for web-ui/dist
+├── telegram/, tunnel/, notifications/
+├── web-ui/ # React/Vite dashboard source
+├── desktop-shell/ # Windows-only Tauri 2/WebView2 desktop shell
+│ ├── src/ # Tray, single-instance, startup and navigation policy
+│ └── ui/ # Local startup/error page (no daemon IPC capability)
└── utils/
├── pid.rs, format.rs, table.rs
└── mod.rs
@@ -106,11 +114,11 @@ src/
alter daemon start
│
▼
-DaemonClient::is_alive() → TCP connect to :2999
+DaemonClient::probe_readiness() → TCP connect + strict health contract on :2999
│
- ├── alive? → print "daemon already running", exit
- │
- └── not alive?
+ ├── verified RunDock? → reuse it
+ ├── port unused? → start the sibling backend executable
+ └── occupied/incompatible? → fail with diagnostics; never end the listener
│
▼
Spawn hidden child: alter --internal-daemon --port 2999
@@ -118,10 +126,10 @@ DaemonClient::is_alive() → TCP connect to :2999
(Unix: stdio → /dev/null)
│
▼
- Poll :2999 every 100ms (up to 5s)
+ Poll :2999 every 100ms (up to 10s)
│
▼
- GET /api/v1/system/health → "ok"
+ GET /api/v1/system/health → status/version/PID/persistence contract
│
▼
Print "daemon started at http://127.0.0.1:2999/"
@@ -133,14 +141,15 @@ DaemonClient::is_alive() → TCP connect to :2999
DaemonState::new()
│
├── ProcessManager::new() (empty DashMap)
- ├── load state.json if exists
- │ └── restore() → restart previously-running processes
+ ├── recover and validate state.json + projects.json
+ │ └── restore() → re-adopt only a live process whose PID identity still matches;
+ │ otherwise retain it as stopped/error state
│
▼
Server::start()
├── socket2: bind TCP with SO_REUSEADDR
├── Build Axum router (REST API + static assets)
- ├── Add CORS layer (all origins/methods/headers)
+ ├── Add CORS allowlist (same origin + explicit loopback dashboard/dev origins)
├── Add tracing layer
└── tokio::serve() → async loop
```
@@ -250,31 +259,34 @@ AsyncBufReadExt::lines() (tokio)
**File:** `%APPDATA%\alter-pm2\state.json` (Windows) or `~/.alter-pm2/state.json`
-**Format:**
+**Format (simplified):**
```json
{
+ "schema_version": 1,
"saved_at": "2026-02-22T10:00:00Z",
"apps": [
{
"id": "uuid",
"config": { /* full AppConfig */ },
"restart_count": 2,
- "autorestart_on_restore": true
+ "last_pid": 1234,
+ "process_identity": { "start_time_secs": 1771735200 },
+ "cron_was_active": false
}
]
}
```
-**Auto-save:** After every process state change (start, stop, restart, delete, edit), a background `tokio::spawn` calls `save_to_disk()`. This uses an atomic write: writes to `state.json.tmp` first, then renames — preventing corruption from partial writes.
+**Auto-save:** Lifecycle mutations are serialized with persistence and rollback. Individual JSON files use bounded temporary files, durable atomic replacement, semantic validation, and a validated last-known-good copy. Runtime state and logical projects additionally use a transaction marker so startup can deterministically roll forward or roll back an interrupted pair update.
**Restore on startup:**
```rust
for app in saved.apps {
- if app.autorestart_on_restore {
- manager.start(app.config).await // restart it
- } else {
- manager.register_stopped(app.config).await // show in list, don't start
- }
+ // Re-adopt only when the saved PID is alive and its immutable start
+ // identity still matches. Never guess-restart an ordinary process.
+ if live_pid_identity_matches(&app) { adopt(app) }
+ else if app.cron_was_active { restore_sleeping_cron(app) }
+ else { register_stopped(app) }
}
```
@@ -334,14 +346,32 @@ status → Watching (same as Running but watcher is active)
## Web Dashboard Architecture
-The web dashboard is **compiled into the binary** using [rust-embed](https://github.com/pyros2097/rust-embed). At compile time, `index.html`, `app.js`, and `style.css` are embedded as byte arrays. At runtime, they are served directly from memory — no disk reads, no external files needed.
+The React dashboard is built by Vite into `web-ui/dist` and then **compiled into
+`alter.exe`** using [rust-embed](https://github.com/pyros2097/rust-embed).
+At runtime the daemon serves those assets from memory on the same loopback
+origin as `/api/v1`.
**Technology:**
-- **Frontend:** Vanilla JavaScript — no framework, no build step
-- **Styling:** Hand-written CSS with CSS custom properties (dark theme)
+- **Frontend:** React 19 + TypeScript + Vite
+- **Styling:** CSS with shared theme variables
- **Real-time updates:** Auto-refresh every 3 seconds + SSE for log streaming
- **Transport:** Fetch API + EventSource
+### Windows desktop shell
+
+`rundock.exe` is a separate Tauri 2 package. Its WebView2 window initially loads
+only a bundled startup/error page, calls the shared daemon lifecycle state
+machine with the sibling `alter.exe`, and navigates to
+`http://127.0.0.1:2999/` only after health ownership is verified. The remote
+dashboard receives no Tauri IPC, filesystem, or shell capability. Navigation
+away from the canonical 2999 loopback origin is denied in the WebView and safe
+external HTTP(S) or registered custom-protocol links are handed to Windows.
+
+Only one desktop shell instance is allowed. A second launch restores the
+existing window. The close button hides it to the tray; tray exit stops only
+`rundock.exe`. Current-user login startup passes `--background`, so the daemon
+can be recovered without showing a window.
+
**Dashboard views:**
| View | Description |
@@ -382,7 +412,7 @@ This design means:
| Data directory | `%APPDATA%\alter-pm2\` |
| `.cmd` scripts (npm, yarn) | Wrapped in `cmd /C` automatically |
| Terminal button | Tries `wt.exe` (Windows Terminal), falls back to `cmd.exe` |
-| Startup integration | PowerShell Scheduled Task |
+| Startup integration | Desktop installer: tray autostart; CLI-only: optional Scheduled Task |
| Port reuse | `SO_REUSEADDR` via socket2 crate |
### Linux / macOS
@@ -452,3 +482,7 @@ CLI prints result table
| `socket2` | 0.5 | Low-level socket control (SO_REUSEADDR) |
| `anyhow` + `thiserror` | — | Error handling |
| `windows` | 0.58 | Win32 API (Windows only, process flags) |
+
+The independent `desktop-shell/Cargo.toml` adds Tauri 2, its single-instance and
+autostart plugins, and Windows WebView2 integration without adding those
+dependencies to Linux CLI/package builds.
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index e69e6b4..d8a5e76 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-All notable changes to alter are documented here.
+All notable changes to RunDock are documented here.
Format: `[version] — YYYY-MM-DD` with sections: **Added**, **Changed**, **Fixed**, **Removed**.
@@ -26,13 +26,13 @@ Format: `[version] — YYYY-MM-DD` with sections: **Added**, **Changed**, **Fixe
- `enabled` flag preserved by clone and update operations; defaults to `true` for backward compatibility
**Terminal Command History**
-- `GET /api/v1/terminal-history/:key` and `PUT /api/v1/terminal-history/:key` — per-process terminal history API
+- `GET /api/v1/terminals/history/:key` and `PUT /api/v1/terminals/history/:key` — per-process terminal history API
- History persisted to `%APPDATA%\alter-pm2\terminal-history.json` across daemon restarts
- Deduplication and frequency tracking per command
**Daemon-side UI Settings**
-- `GET /api/v1/ui-settings` and `PUT /api/v1/ui-settings` — store UI preferences on the daemon
-- `GET/PUT /api/v1/ui-settings/view-mode` — view mode (table/card) now persisted server-side
+- `GET /api/v1/system/ui-settings` and `PUT /api/v1/system/ui-settings` — store UI preferences on the daemon
+- View mode (table/card) is persisted inside the daemon-side UI settings document
- Settings stored in `%APPDATA%\alter-pm2\ui-settings.json`
**Sidebar Namespace Groups**
@@ -64,7 +64,7 @@ Format: `[version] — YYYY-MM-DD` with sections: **Added**, **Changed**, **Fixe
- Master CLI token — persistent token stored in `auth.json`, read by the CLI only, never returned to the browser
- Login / Setup page in the web UI — first launch shows a setup wizard; subsequent visits show the login form
- Auth middleware protecting all API routes when a password is configured
-- Passkey / WebAuthn stubs (endpoints registered; return "not supported" until a full WebAuthn backend is wired up)
+- Passkey / WebAuthn remains explicitly unsupported (`passkeys_supported: false`); no registration or verification endpoints are exposed until a complete backend is implemented.
- New REST API: `GET /api/v1/auth/status`, `POST /api/v1/auth/setup`, `POST /api/v1/auth/login`, `POST /api/v1/auth/pin/login`, `DELETE /api/v1/auth/session`, `POST /api/v1/auth/change-password`, `POST/DELETE /api/v1/auth/pin`, `PATCH /api/v1/auth/settings`
- Settings stored in `%APPDATA%\alter-pm2\auth.json`
diff --git a/docs/CLI.md b/docs/CLI.md
index 461b82e..3833125 100644
--- a/docs/CLI.md
+++ b/docs/CLI.md
@@ -1,6 +1,6 @@
# CLI Reference
-> Complete reference for all `alter` commands, flags, and options.
+> Complete reference for RunDock command-line actions, flags, and options.
---
@@ -51,7 +51,7 @@ alter start