diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..02f2612
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,24 @@
+# EditorConfig — 跨编辑器统一代码风格
+# https://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = crlf
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{rs,toml}]
+indent_size = 4
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[Makefile]
+indent_style = tab
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..43b96ad
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,32 @@
+# Git 行尾符规范化
+# 本仓库统一 CRLF(Windows 原生项目)
+
+# 源码文本文件
+*.ts text eol=crlf
+*.tsx text eol=crlf
+*.js text eol=crlf
+*.json text eol=crlf
+*.html text eol=crlf
+*.css text eol=crlf
+*.md text eol=crlf
+*.rs text eol=crlf
+*.toml text eol=crlf
+*.yml text eol=crlf
+*.yaml text eol=crlf
+*.svg text eol=crlf
+*.txt text eol=crlf
+*.editorconfig text eol=crlf
+*.gitattributes text eol=crlf
+*.gitignore text eol=crlf
+LICENSE text eol=crlf
+
+# 二进制文件
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.pdf binary
+*.dll binary
+*.exe binary
+*.nsis binary
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..0497638
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,20 @@
+# 代码所有者 — 自动分配 PR 审查
+# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+
+# 全局所有者
+* @LHY0125
+
+# Rust 代码
+/core/ @LHY0125
+/cli/ @LHY0125
+/gui/ @LHY0125
+/Cargo.toml @LHY0125
+/rust-toolchain.toml @LHY0125
+
+# 前端代码
+/src/ @LHY0125
+/tests/ @LHY0125
+/e2e/ @LHY0125
+
+# CI/CD 和配置文件
+/.github/ @LHY0125
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..67edefb
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,5 @@
+# 开源赞助
+# 支持 PathEditor 的开发
+
+github: LHY0125
+# 如需定制功能或商业授权,请通过 GitHub Issues 联系
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..bec6d24
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,73 @@
+# Dependabot 自动依赖更新配置
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates
+
+version: 2
+updates:
+ # npm 前端依赖
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "09:00"
+ timezone: "Asia/Shanghai"
+ versioning-strategy: "auto"
+ allow:
+ - dependency-type: "all"
+ labels:
+ - "dependencies"
+ - "javascript"
+ commit-message:
+ prefix: "chore(deps)"
+ prefix-development: "chore(deps-dev)"
+ open-pull-requests-limit: 5
+ groups:
+ react:
+ patterns:
+ - "react"
+ - "react-dom"
+ - "@types/react"
+ - "@types/react-dom"
+ tauri:
+ patterns:
+ - "@tauri-apps/*"
+ testing:
+ patterns:
+ - "@testing-library/*"
+ - "@playwright/test"
+ - "vitest"
+ - "jsdom"
+ eslint:
+ patterns:
+ - "eslint"
+ - "eslint-plugin-*"
+ - "typescript-eslint"
+ - "globals"
+ - "@eslint/js"
+
+ # Cargo Rust 依赖
+ - package-ecosystem: "cargo"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "09:00"
+ timezone: "Asia/Shanghai"
+ labels:
+ - "dependencies"
+ - "rust"
+ commit-message:
+ prefix: "chore(deps)"
+ prefix-development: "chore(deps-dev)"
+ open-pull-requests-limit: 3
+
+ # GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ labels:
+ - "dependencies"
+ - "ci"
+ commit-message:
+ prefix: "ci(deps)"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 430fe29..ef90ef7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ permissions:
jobs:
frontend:
- name: 前端检查 (TypeScript + Lint + Test)
+ name: 前端检查 (格式 + 类型 + Lint + 测试 + 覆盖率)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -24,23 +24,37 @@ jobs:
- run: npm ci
+ - name: Prettier 格式检查
+ run: npx prettier --check "src/**/*.{ts,tsx}" "tests/**/*.{ts,tsx}" "e2e/**/*.ts"
+
- name: TypeScript 类型检查
run: npx tsc -b --noEmit
- name: ESLint
run: npx eslint src/ tests/ e2e/
- - name: Vitest 测试
- run: npm test
+ - name: Vitest 测试 + 覆盖率
+ run: npx vitest run --coverage
+
+ - name: 上传覆盖率到 Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ files: ./coverage/cobertura-coverage.xml
+ flags: frontend
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
rust:
- name: Rust 检查 (Check + Clippy + Test)
+ name: Rust 检查 (格式 + Check + Clippy + Test)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
+ - name: Cargo Format Check
+ run: cargo fmt --check
+
- name: Cargo Check
run: cargo check
@@ -49,6 +63,3 @@ jobs:
- name: Cargo Test
run: cargo test
-
- - name: Cargo Format Check
- run: cargo fmt --check
diff --git a/.gitignore b/.gitignore
index b24c0f3..e22172e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,18 @@ dist
dist-ssr
*.local
+# Coverage
+coverage/
+*.lcov
+
+# Sync conflicts
+*.sync-conflict-*
+
+# Test artifacts
+test-results/
+playwright-report/
+.nyc_output/
+
# Editor directories and files
.vscode/*
!.vscode/extensions.json
@@ -22,9 +34,17 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# AI assistant
.claude/
.codegraph/
CLAUDE.md
+
+# Platform
e2e/debug-screenshot.png
-test-results/
target/
+
+# Archive
+*.zip
+*.7z
+*.tar.gz
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100644
index 0000000..0a4b97d
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1 @@
+npx --no -- commitlint --edit $1
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 0000000..2312dc5
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+npx lint-staged
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 0000000..2588642
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,8 @@
+{
+ "default": true,
+ "MD013": false,
+ "MD033": {
+ "allowed_elements": ["img", "br", "kbd", "summary", "details"]
+ },
+ "MD041": false
+}
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..3880956
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,10 @@
+node_modules
+dist
+dist-ssr
+target
+*.local
+*.log
+test-results
+coverage
+Cargo.lock
+package-lock.json
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..6dd41b5
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,10 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "tabWidth": 2,
+ "endOfLine": "crlf",
+ "arrowParens": "always",
+ "bracketSpacing": true
+}
diff --git a/README.md b/README.md
index 957cbeb..436b7b8 100644
--- a/README.md
+++ b/README.md
@@ -11,11 +11,33 @@
+
+
---
+## 截图
+
+### 主界面
+
+
+
+### 路径编辑
+
+
+
+### 冲突检测
+
+
+
+### CLI 命令行
+
+
+
+---
+
## 简介
PathEditor 是 Windows PATH 环境变量的可视化管理工具。支持系统变量和用户变量的增删改查、拖拽排序、一键清理无效路径、导入导出以及完整的撤销/重做。
@@ -251,18 +273,18 @@ npx tauri build
### 技术栈
-| 层 | 技术 |
-|---|---|
-| 前端框架 | React 19 + TypeScript (strict) |
-| UI 样式 | Tailwind CSS 4 |
-| 状态管理 | Zustand |
-| 国际化 | i18next |
-| 桌面框架 | Tauri 2.x |
-| 核心库 | Rust workspace (core + gui + cli) |
-| 前端测试 | Vitest (100 个测试) |
-| Rust 测试 | cargo test (57 个测试) |
-| 构建 | Vite + Cargo |
-| 打包 | NSIS |
+| 层 | 技术 |
+| --------- | --------------------------------- |
+| 前端框架 | React 19 + TypeScript (strict) |
+| UI 样式 | Tailwind CSS 4 |
+| 状态管理 | Zustand |
+| 国际化 | i18next |
+| 桌面框架 | Tauri 2.x |
+| 核心库 | Rust workspace (core + gui + cli) |
+| 前端测试 | Vitest (100 个测试) |
+| Rust 测试 | cargo test (57 个测试) |
+| 构建 | Vite + Cargo |
+| 打包 | NSIS |
### 项目结构
@@ -291,15 +313,15 @@ docs/ # 审查文档
## 快捷键
-| 快捷键 | 功能 |
-|--------|------|
+| 快捷键 | 功能 |
+| -------- | -------- |
| `Ctrl+N` | 新建路径 |
-| `Ctrl+S` | 保存 |
-| `Ctrl+Z` | 撤销 |
-| `Ctrl+Y` | 重做 |
-| `Ctrl+F` | 搜索 |
+| `Ctrl+S` | 保存 |
+| `Ctrl+Z` | 撤销 |
+| `Ctrl+Y` | 重做 |
+| `Ctrl+F` | 搜索 |
| `Delete` | 删除选中 |
-| `F1` | 帮助 |
+| `F1` | 帮助 |
## 贡献
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..29b8af0
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,48 @@
+# 路线图
+
+PathEditor 的未来发展方向。
+
+## v5.1 (下一个版本)
+
+- [ ] **CLI 模块化** — `cli/src/main.rs` 拆分为 `commands/` 子模块
+- [ ] **自动更新** — 内置 Tauri updater,无需手动下载安装包
+- [ ] **深色模式优化** — 对齐 Windows 系统主题自动切换
+- [ ] **性能优化** — 虚拟滚动支持超长 PATH 列表(1000+ 条目)
+
+## v5.2
+
+- [ ] **PATH 历史快照** — 保存每次修改的时间线,支持回退到任意历史节点
+- [ ] **规则引擎** — 自定义 PATH 整理规则(如「所有 Python 路径放最前」)
+- [ ] **收藏夹** — 常用路径快速添加
+- [ ] **冲突解决方案引导** — 可视化的可执行文件冲突对比与解决建议
+
+## v6.0 (长期)
+
+- [ ] **跨平台支持** — 适配 Linux (`/etc/environment` + `~/.profile`) 和 macOS (`path_helper`)
+- [ ] **Web 管理面板** — 远程管理多台 Windows 服务器的 PATH 环境变量
+- [ ] **插件系统** — 第三方扩展生态(如 Anaconda/VSCode/VS 自动检测与配置)
+- [ ] **Windows Package Manager 集成** — 与 winget/chocolatey 联动,检测包管理器安装的路径
+
+## 已交付
+
+### v5.0.0
+
+- ✅ Cargo workspace 三层架构 (core + gui + cli)
+- ✅ CLI 命令行工具 (18 条命令)
+- ✅ 冲突检测 + 工具清单
+- ✅ 配置文件管理
+- ✅ 撤销/重做 (10 种操作)
+- ✅ 中英双语界面
+- ✅ CI/CD 自动化
+
+### v4.x 系列
+
+- ✅ Tauri 2.x 重写
+- ✅ 路径验证 (红色/橙色标记)
+- ✅ 导入/导出 JSON/CSV/TXT
+- ✅ 深色/浅色模式
+- ✅ 全局键盘快捷键
+
+---
+
+欢迎通过 [Issues](https://github.com/LHY0125/PathEditor/issues) 提交功能建议!
diff --git a/SUPPORT.md b/SUPPORT.md
new file mode 100644
index 0000000..0edf910
--- /dev/null
+++ b/SUPPORT.md
@@ -0,0 +1,45 @@
+# 获取帮助
+
+## 📖 文档
+
+- [README](README.md) — 项目简介、功能列表、安装指南
+- [CONTRIBUTING](CONTRIBUTING.md) — 贡献指南
+- [CHANGELOG](CHANGELOG.md) — 版本变更记录
+- [ROADMAP](ROADMAP.md) — 未来规划
+- [SECURITY](SECURITY.md) — 安全政策
+
+## 🐛 报告 Bug
+
+1. 先搜索 [Issues](https://github.com/LHY0125/PathEditor/issues) 确认未被报告
+2. 使用 **Bug Report** 模板创建新 Issue
+3. 提供系统信息(Windows 版本、PathEditor 版本)
+4. 附上复现步骤和截图
+
+## 💡 功能建议
+
+1. 检查 [ROADMAP](ROADMAP.md) 确认不在已有计划中
+2. 使用 **Feature Request** 模板创建新 Issue
+3. 描述使用场景和期望行为
+
+## ❓ 常见问题
+
+### CLI 命令找不到?
+
+```bash
+patheditor --help
+```
+
+确保已通过 `cargo install --path cli` 安装,且 `~/.cargo/bin` 在 PATH 中。
+
+### 提示权限不足?
+
+编辑系统 PATH 需要管理员权限。右键以管理员身份运行,或使用 CLI `patheditor check-admin` 检测。
+
+### 保存后环境变量未生效?
+
+PathEditor 会自动广播 `WM_SETTINGCHANGE`,但部分程序需要手动重启才能识别新 PATH。
+
+## 📧 联系
+
+- GitHub Issues: [LHY0125/PathEditor](https://github.com/LHY0125/PathEditor/issues)
+- 安全问题: 参见 [SECURITY.md](SECURITY.md)
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 0000000..c4c9714
--- /dev/null
+++ b/commitlint.config.js
@@ -0,0 +1,12 @@
+/** @type {import('@commitlint/types').UserConfig} */
+export default {
+ extends: ['@commitlint/config-conventional'],
+ rules: {
+ 'type-enum': [
+ 2,
+ 'always',
+ ['feat', 'fix', 'refactor', 'docs', 'test', 'chore', 'perf', 'ci', 'style', 'revert'],
+ ],
+ 'subject-case': [0],
+ },
+};
diff --git a/docs/screenshots/.gitkeep b/docs/screenshots/.gitkeep
new file mode 100644
index 0000000..a44d07a
--- /dev/null
+++ b/docs/screenshots/.gitkeep
@@ -0,0 +1 @@
+# 截图目录
diff --git a/docs/screenshots/cli-demo.png b/docs/screenshots/cli-demo.png
new file mode 100644
index 0000000..48ea000
Binary files /dev/null and b/docs/screenshots/cli-demo.png differ
diff --git a/docs/screenshots/conflict-analysis.png b/docs/screenshots/conflict-analysis.png
new file mode 100644
index 0000000..7ce4493
Binary files /dev/null and b/docs/screenshots/conflict-analysis.png differ
diff --git a/docs/screenshots/main-window.png b/docs/screenshots/main-window.png
new file mode 100644
index 0000000..4212d51
Binary files /dev/null and b/docs/screenshots/main-window.png differ
diff --git a/docs/screenshots/path-edit.png b/docs/screenshots/path-edit.png
new file mode 100644
index 0000000..ec8303a
Binary files /dev/null and b/docs/screenshots/path-edit.png differ
diff --git a/e2e/tests/search-clean.spec.ts b/e2e/tests/search-clean.spec.ts
index b37b5f7..461d4c5 100644
--- a/e2e/tests/search-clean.spec.ts
+++ b/e2e/tests/search-clean.spec.ts
@@ -2,11 +2,13 @@ import { test, expect } from '@playwright/test';
import { createIpcMock } from '../mocks/ipc';
test.beforeEach(async ({ page }) => {
- await page.addInitScript(createIpcMock({
- load_system_paths: ['C:\\Windows', 'invalid_path', 'C:\\Temp'],
- load_user_paths: [],
- validate_path: false,
- }));
+ await page.addInitScript(
+ createIpcMock({
+ load_system_paths: ['C:\\Windows', 'invalid_path', 'C:\\Temp'],
+ load_user_paths: [],
+ validate_path: false,
+ }),
+ );
await page.goto('/');
});
diff --git a/index.html b/index.html
index 6333e4f..2b48773 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
- PathEditor v4.0
+ PathEditor v5.1
diff --git a/package-lock.json b/package-lock.json
index bc50bcf..566df7c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,9 +21,12 @@
"zustand": "^5.0.13"
},
"devDependencies": {
+ "@commitlint/cli": "^21.0.2",
+ "@commitlint/config-conventional": "^21.0.2",
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.60.0",
"@tauri-apps/cli": "^2.11.2",
+ "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
@@ -34,13 +37,23 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
+ "husky": "^9.1.7",
"jsdom": "^29.1.1",
+ "lint-staged": "^16.4.0",
+ "prettier": "^3.8.4",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12",
"vitest": "^4.1.7"
}
},
+ "node_modules/@adobe/css-tools": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmmirror.com/@adobe/css-tools/-/css-tools-4.5.0.tgz",
+ "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@asamuzakjp/css-color": {
"version": "5.1.11",
"resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
@@ -364,6 +377,333 @@
"specificity": "bin/cli.js"
}
},
+ "node_modules/@commitlint/cli": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/cli/-/cli-21.0.2.tgz",
+ "integrity": "sha512-YMmfLbqBg+ZRvvmPhc+cilSQFrh/AgzVgCT1U/OifmUZEwPbvCtA8rN//YNaF9d5eoZphxVMGYtmwA2QgQORgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/format": "^21.0.1",
+ "@commitlint/lint": "^21.0.2",
+ "@commitlint/load": "^21.0.2",
+ "@commitlint/read": "^21.0.2",
+ "@commitlint/types": "^21.0.1",
+ "tinyexec": "^1.0.0",
+ "yargs": "^18.0.0"
+ },
+ "bin": {
+ "commitlint": "cli.js"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/config-conventional": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-21.0.2.tgz",
+ "integrity": "sha512-P/ZRhryQmkj0Z0dY9FOoRwe3xkwJyyAdtXwt01NT2kuZttcG2CNYp1q5Ci3u+nDT2jcbJRw2kt13Czl1qKNPfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^21.0.1",
+ "conventional-changelog-conventionalcommits": "^9.2.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/config-validator": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.1.tgz",
+ "integrity": "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^21.0.1",
+ "ajv": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/config-validator/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@commitlint/ensure": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-21.0.1.tgz",
+ "integrity": "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^21.0.1",
+ "es-toolkit": "^1.46.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/execute-rule": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-21.0.1.tgz",
+ "integrity": "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/format": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.1.tgz",
+ "integrity": "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^21.0.1",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/is-ignored": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-21.0.2.tgz",
+ "integrity": "sha512-H5z4t8PC9tUsmZ/o+EptM3Nq8sTFtskAShdcqxCoyzklW5eaVT5xbrDAET2uypzir9Vsj4ZZmBtyKjYe2XqgeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^21.0.1",
+ "semver": "^7.6.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/is-ignored/node_modules/semver": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz",
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@commitlint/lint": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/lint/-/lint-21.0.2.tgz",
+ "integrity": "sha512-PnUmLYGeGLfW8oVatR9KpNxSHYAnJOEWlMZzfdeFOUq6WUrFx1fGQaWCWJqMoIll/xPM+GdfJV+tKHZVHhl0Fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/is-ignored": "^21.0.2",
+ "@commitlint/parse": "^21.0.2",
+ "@commitlint/rules": "^21.0.2",
+ "@commitlint/types": "^21.0.1"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/load": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/load/-/load-21.0.2.tgz",
+ "integrity": "sha512-lwUE70hN0/qE/ZRROhbaX65ly/FF12DrqfReLCESo37M0OQCFAf2jRS+2tSCSORq+bm4Kdju7qNDj46uc1QzTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/config-validator": "^21.0.1",
+ "@commitlint/execute-rule": "^21.0.1",
+ "@commitlint/resolve-extends": "^21.0.1",
+ "@commitlint/types": "^21.0.1",
+ "cosmiconfig": "^9.0.1",
+ "cosmiconfig-typescript-loader": "^6.1.0",
+ "es-toolkit": "^1.46.0",
+ "is-plain-obj": "^4.1.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/message": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/message/-/message-21.0.2.tgz",
+ "integrity": "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/parse": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/parse/-/parse-21.0.2.tgz",
+ "integrity": "sha512-QVZJhGHTm+oiuWyEKOCTQ0ZM3mfJ0eGWFeHuj7WzSKEth+UukcCHac9GD8pgdFlg/qGkFWOtyaNd1T8REgagaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^21.0.1",
+ "conventional-changelog-angular": "^8.2.0",
+ "conventional-commits-parser": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/read": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/read/-/read-21.0.2.tgz",
+ "integrity": "sha512-BtsrnLVycSSKf4Q0gMch4giCj5NNlmcbhc8ra5vONgGtP2IjRDo33bEFtr5Pm+2N+5fXGWb2MksWPrspPfdhdw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/top-level": "^21.0.2",
+ "@commitlint/types": "^21.0.1",
+ "git-raw-commits": "^5.0.0",
+ "tinyexec": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/resolve-extends": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz",
+ "integrity": "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/config-validator": "^21.0.1",
+ "@commitlint/types": "^21.0.1",
+ "es-toolkit": "^1.46.0",
+ "global-directory": "^5.0.0",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/rules": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/rules/-/rules-21.0.2.tgz",
+ "integrity": "sha512-k6tQ69Td7t2qUSIbik8D3TL1q3ZJpkEbV+yLogDzCRAdOxJm4ndhtBNREsLA1/puRfWvzS9eioF2w43WT+hHgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/ensure": "^21.0.1",
+ "@commitlint/message": "^21.0.2",
+ "@commitlint/to-lines": "^21.0.1",
+ "@commitlint/types": "^21.0.1"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/to-lines": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.1.tgz",
+ "integrity": "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/top-level": {
+ "version": "21.0.2",
+ "resolved": "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-21.0.2.tgz",
+ "integrity": "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@commitlint/types": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.1.tgz",
+ "integrity": "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "conventional-commits-parser": "^6.3.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/@conventional-changelog/git-client": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmmirror.com/@conventional-changelog/git-client/-/git-client-2.7.0.tgz",
+ "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@simple-libs/child-process-utils": "^1.0.0",
+ "@simple-libs/stream-utils": "^1.2.0",
+ "semver": "^7.5.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "conventional-commits-filter": "^5.0.0",
+ "conventional-commits-parser": "^6.4.0"
+ },
+ "peerDependenciesMeta": {
+ "conventional-commits-filter": {
+ "optional": true
+ },
+ "conventional-commits-parser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@conventional-changelog/git-client/node_modules/semver": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.4.tgz",
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/@csstools/color-helpers": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
@@ -1083,6 +1423,35 @@
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"license": "MIT"
},
+ "node_modules/@simple-libs/child-process-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz",
+ "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@simple-libs/stream-utils": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/dangreen"
+ }
+ },
+ "node_modules/@simple-libs/stream-utils": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz",
+ "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/dangreen"
+ }
+ },
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -1631,6 +2000,33 @@
"node": ">=18"
}
},
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@testing-library/react": {
"version": "16.3.2",
"resolved": "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.2.tgz",
@@ -2199,6 +2595,22 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-escapes": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
+ "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -2224,17 +2636,30 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
},
+ "node_modules/array-ify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz",
+ "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -2344,6 +2769,16 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001793",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
@@ -2375,6 +2810,143 @@
"node": ">=18"
}
},
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz",
+ "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^8.0.0",
+ "string-width": "^8.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmmirror.com/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/compare-func": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/compare-func/-/compare-func-2.0.0.tgz",
+ "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-ify": "^1.0.0",
+ "dot-prop": "^5.1.0"
+ }
+ },
+ "node_modules/conventional-changelog-angular": {
+ "version": "8.3.1",
+ "resolved": "https://registry.npmmirror.com/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz",
+ "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "compare-func": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/conventional-changelog-conventionalcommits": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmmirror.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz",
+ "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "compare-func": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/conventional-commits-parser": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmmirror.com/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz",
+ "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@simple-libs/stream-utils": "^1.2.0",
+ "meow": "^13.0.0"
+ },
+ "bin": {
+ "conventional-commits-parser": "dist/cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2382,6 +2954,61 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cosmiconfig": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
+ "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cosmiconfig-typescript-loader": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz",
+ "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jiti": "2.6.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "cosmiconfig": ">=9",
+ "typescript": ">=5"
+ }
+ },
+ "node_modules/cosmiconfig-typescript-loader/node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2411,6 +3038,13 @@
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
@@ -2470,7 +3104,6 @@
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6"
}
@@ -2492,6 +3125,19 @@
"license": "MIT",
"peer": true
},
+ "node_modules/dot-prop": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-5.3.0.tgz",
+ "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.361",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz",
@@ -2499,6 +3145,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/enhanced-resolve": {
"version": "5.22.0",
"resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz",
@@ -2525,6 +3178,39 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
"node_modules/es-module-lexer": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
@@ -2532,6 +3218,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/es-toolkit": {
+ "version": "1.47.1",
+ "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.47.1.tgz",
+ "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
@@ -2747,6 +3444,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz",
@@ -2778,6 +3482,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz",
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
@@ -2870,6 +3591,46 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
+ "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/git-raw-commits": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/git-raw-commits/-/git-raw-commits-5.0.1.tgz",
+ "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@conventional-changelog/git-client": "^2.6.0",
+ "meow": "^13.0.0"
+ },
+ "bin": {
+ "git-raw-commits": "src/cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2883,6 +3644,22 @@
"node": ">=10.13.0"
}
},
+ "node_modules/global-directory": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/global-directory/-/global-directory-5.0.0.tgz",
+ "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "6.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/globals": {
"version": "17.6.0",
"resolved": "https://registry.npmmirror.com/globals/-/globals-17.6.0.tgz",
@@ -2958,6 +3735,22 @@
"void-elements": "3.1.0"
}
},
+ "node_modules/husky": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz",
+ "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "husky": "bin.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
"node_modules/i18next": {
"version": "26.2.0",
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-26.2.0.tgz",
@@ -3005,6 +3798,33 @@
"node": ">= 4"
}
},
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -3015,6 +3835,33 @@
"node": ">=0.8.19"
}
},
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ini": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/ini/-/ini-6.0.0.tgz",
+ "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3025,6 +3872,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+ "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
@@ -3038,6 +3901,29 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -3107,6 +3993,29 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/jsdom": {
"version": "29.1.1",
"resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz",
@@ -3178,6 +4087,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3478,6 +4394,55 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lint-staged": {
+ "version": "16.4.0",
+ "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-16.4.0.tgz",
+ "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^14.0.3",
+ "listr2": "^9.0.5",
+ "picomatch": "^4.0.3",
+ "string-argv": "^0.3.2",
+ "tinyexec": "^1.0.4",
+ "yaml": "^2.8.2"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmmirror.com/listr2/-/listr2-9.0.5.tgz",
+ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^5.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
@@ -3494,6 +4459,56 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/log-update": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz",
+ "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3572,6 +4587,42 @@
"dev": true,
"license": "CC0-1.0"
},
+ "node_modules/meow": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz",
+ "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz",
@@ -3641,6 +4692,22 @@
],
"license": "MIT"
},
+ "node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
@@ -3691,6 +4758,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz",
@@ -3834,6 +4933,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.8.4",
+ "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.4.tgz",
+ "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz",
@@ -3916,6 +5031,20 @@
"license": "MIT",
"peer": true
},
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -3926,6 +5055,40 @@
"node": ">=0.10.0"
}
},
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/rolldown": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.2.tgz",
@@ -4018,6 +5181,49 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz",
+ "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.3",
+ "is-fullwidth-code-point": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4041,6 +5247,75 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz",
+ "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.5.0",
+ "strip-ansi": "^7.1.2"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
@@ -4574,6 +5849,55 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
@@ -4591,6 +5915,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
@@ -4598,6 +5932,68 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+ "devOptional": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmmirror.com/yargs/-/yargs-18.0.0.tgz",
+ "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^7.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 6238a70..838a3ab 100644
--- a/package.json
+++ b/package.json
@@ -3,17 +3,29 @@
"private": true,
"version": "5.1.0",
"type": "module",
+ "lint-staged": {
+ "*.{ts,tsx}": [
+ "prettier --write",
+ "eslint --fix"
+ ],
+ "*.{json,md,css,html}": [
+ "prettier --write"
+ ]
+ },
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
- "format": "cargo fmt",
+ "format": "prettier --write \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\" \"e2e/**/*.ts\"",
+ "format:check": "prettier --check \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\" \"e2e/**/*.ts\"",
+ "format:rust": "cargo fmt",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
- "test:e2e": "playwright test --config e2e/playwright.config.ts"
+ "test:e2e": "playwright test --config e2e/playwright.config.ts",
+ "prepare": "husky"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
@@ -29,9 +41,12 @@
"zustand": "^5.0.13"
},
"devDependencies": {
+ "@commitlint/cli": "^21.0.2",
+ "@commitlint/config-conventional": "^21.0.2",
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.60.0",
"@tauri-apps/cli": "^2.11.2",
+ "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
@@ -42,7 +57,10 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
+ "husky": "^9.1.7",
"jsdom": "^29.1.1",
+ "lint-staged": "^16.4.0",
+ "prettier": "^3.8.4",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12",
diff --git a/src/components/dialogs/AnalyzeDialog.tsx b/src/components/dialogs/AnalyzeDialog.tsx
index 3dc5565..a00f862 100644
--- a/src/components/dialogs/AnalyzeDialog.tsx
+++ b/src/components/dialogs/AnalyzeDialog.tsx
@@ -37,7 +37,10 @@ export function AnalyzeDialog({ open, onClose }: Props) {
const prevOpen = useRef(false);
useEffect(() => {
- if (!open) { prevOpen.current = false; return; }
+ if (!open) {
+ prevOpen.current = false;
+ return;
+ }
if (prevOpen.current) return;
prevOpen.current = true;
setLoading(true);
@@ -67,7 +70,10 @@ export function AnalyzeDialog({ open, onClose }: Props) {
{/* 标题栏 */}
-
+
{t('analyze.title')}
{(['conflicts', 'tools'] as TabType[]).map((tb) => (
@@ -89,7 +95,10 @@ export function AnalyzeDialog({ open, onClose }: Props) {
{/* 内容 */}
{loading ? (
-
+
{t('analyze.scanning')}
) : tab === 'conflicts' ? (
@@ -215,5 +224,7 @@ function EmptyHint({ text }: { text: string }) {
function getEnabledPaths(): string[] {
const { sysPaths, userPaths } = useAppStore.getState();
- return [...sysPaths.filter((e) => e.enabled), ...userPaths.filter((e) => e.enabled)].map((e) => e.path);
+ return [...sysPaths.filter((e) => e.enabled), ...userPaths.filter((e) => e.enabled)].map(
+ (e) => e.path,
+ );
}
diff --git a/src/components/dialogs/HelpDialog.tsx b/src/components/dialogs/HelpDialog.tsx
index 7428d6e..9602216 100644
--- a/src/components/dialogs/HelpDialog.tsx
+++ b/src/components/dialogs/HelpDialog.tsx
@@ -1,7 +1,10 @@
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/ui/Modal';
-interface HelpDialogProps { open: boolean; onClose: () => void; }
+interface HelpDialogProps {
+ open: boolean;
+ onClose: () => void;
+}
export function HelpDialog({ open, onClose }: HelpDialogProps) {
const { t } = useTranslation();
@@ -9,9 +12,15 @@ export function HelpDialog({ open, onClose }: HelpDialogProps) {
return (
{t('dialog.helpTitle')}
- {t('help.content')}
+
+ {t('help.content')}
+
-
diff --git a/src/components/dialogs/ImportDialog.tsx b/src/components/dialogs/ImportDialog.tsx
index f6a0679..985030c 100644
--- a/src/components/dialogs/ImportDialog.tsx
+++ b/src/components/dialogs/ImportDialog.tsx
@@ -9,7 +9,13 @@ interface ImportDialogProps {
onCancel: () => void;
}
-export function ImportDialog({ open, systemCount, userCount, onSelect, onCancel }: ImportDialogProps) {
+export function ImportDialog({
+ open,
+ systemCount,
+ userCount,
+ onSelect,
+ onCancel,
+}: ImportDialogProps) {
const { t } = useTranslation();
return (
@@ -21,10 +27,40 @@ export function ImportDialog({ open, systemCount, userCount, onSelect, onCancel
{userCount > 0 && t('dialog.importUserCount', { count: userCount })}
- {systemCount > 0 && onSelect('system')}>{t('dialog.importSystem')}}
- {userCount > 0 && onSelect('user')}>{t('dialog.importUser')}}
- {systemCount > 0 && userCount > 0 && onSelect('both')}>{t('dialog.importBoth')}}
- {t('dialog.cancel')}
+ {systemCount > 0 && (
+ onSelect('system')}
+ >
+ {t('dialog.importSystem')}
+
+ )}
+ {userCount > 0 && (
+ onSelect('user')}
+ >
+ {t('dialog.importUser')}
+
+ )}
+ {systemCount > 0 && userCount > 0 && (
+ onSelect('both')}
+ >
+ {t('dialog.importBoth')}
+
+ )}
+
+ {t('dialog.cancel')}
+
);
diff --git a/src/components/dialogs/PathEditDialog.tsx b/src/components/dialogs/PathEditDialog.tsx
index d31e17f..acb33af 100644
--- a/src/components/dialogs/PathEditDialog.tsx
+++ b/src/components/dialogs/PathEditDialog.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/ui/Modal';
@@ -10,30 +10,54 @@ interface PathEditDialogProps {
onCancel: () => void;
}
-export function PathEditDialog({ open, title, initialValue, onConfirm, onCancel }: PathEditDialogProps) {
+export function PathEditDialog({
+ open,
+ title,
+ initialValue,
+ onConfirm,
+ onCancel,
+}: PathEditDialogProps) {
const { t } = useTranslation();
const [value, setValue] = useState(initialValue);
+ const prevOpen = useRef(open);
- // 对话框打开时重置输入值 — 此模式不会导致级联渲染
- // eslint-disable-next-line react-hooks/set-state-in-effect
- useEffect(() => { if (open) setValue(initialValue); }, [open, initialValue]);
+ useEffect(() => {
+ if (open && !prevOpen.current) setValue(initialValue);
+ prevOpen.current = open;
+ }, [open, initialValue]);
return (
{title}
setValue(e.target.value)}
- onKeyDown={(e) => { if (e.key === 'Enter') onConfirm(value); }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') onConfirm(value);
+ }}
className="w-full min-w-[400px] px-3 py-2 rounded border text-sm outline-none"
- style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }}
+ style={{
+ backgroundColor: 'var(--app-list-bg)',
+ color: 'var(--app-fg)',
+ borderColor: 'var(--app-border)',
+ }}
/>
-
+
{t('dialog.cancel')}
- onConfirm(value)}>
+ onConfirm(value)}
+ >
{t('dialog.confirm')}
diff --git a/src/components/dialogs/ProfileDialog.tsx b/src/components/dialogs/ProfileDialog.tsx
index b59615b..fc61c57 100644
--- a/src/components/dialogs/ProfileDialog.tsx
+++ b/src/components/dialogs/ProfileDialog.tsx
@@ -65,20 +65,23 @@ export function ProfileDialog({ open, onClose }: Props) {
if (!selected || !selectedData) return;
if (!window.confirm(t('profile.applyConfirm', { name: selected }))) return;
useAppStore.getState().replaceBothPaths(
- selectedData.sys.map(e => e.path),
- selectedData.user.map(e => e.path),
+ selectedData.sys.map((e) => e.path),
+ selectedData.user.map((e) => e.path),
);
// 同步 disabled 状态
await invoke('save_disabled_state', {
- system: selectedData.sys.filter(e => !e.enabled).map(e => e.path),
- user: selectedData.user.filter(e => !e.enabled).map(e => e.path),
+ system: selectedData.sys.filter((e) => !e.enabled).map((e) => e.path),
+ user: selectedData.user.filter((e) => !e.enabled).map((e) => e.path),
});
const result = await useAppStore.getState().savePaths();
if (result.kind === 'success') {
onClose();
} else if (result.kind === 'warning') {
const { ask } = await import('@tauri-apps/plugin-dialog');
- const confirmed = await ask(t('status.saveWarningLongPaths'), { title: t('dialog.backupTitle'), kind: 'warning' });
+ const confirmed = await ask(t('status.saveWarningLongPaths'), {
+ title: t('dialog.backupTitle'),
+ kind: 'warning',
+ });
if (confirmed) {
const forceResult = await useAppStore.getState().savePaths(true);
if (forceResult.kind === 'success') {
@@ -91,7 +94,10 @@ export function ProfileDialog({ open, onClose }: Props) {
const handleDelete = async (name: string) => {
if (!window.confirm(`删除配置文件 "${name}"?`)) return;
await invoke('delete_profile', { name });
- if (selected === name) { setSelected(null); setSelectedData(null); }
+ if (selected === name) {
+ setSelected(null);
+ setSelectedData(null);
+ }
refreshProfiles();
};
@@ -106,16 +112,23 @@ export function ProfileDialog({ open, onClose }: Props) {
return (
-
+
{t('profile.title')}
setNewName(e.target.value)}
+ onChange={(e) => setNewName(e.target.value)}
placeholder={t('profile.namePlaceholder')}
className="px-2 py-1 text-sm rounded border outline-none w-44"
- style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }}
+ style={{
+ backgroundColor: 'var(--app-list-bg)',
+ color: 'var(--app-fg)',
+ borderColor: 'var(--app-border)',
+ }}
/>
{/* 左侧:列表 */}
-
+
{profiles.length === 0 ? (
-
{t('profile.noProfiles')}
+
+ {t('profile.noProfiles')}
+
) : (
- profiles.map(p => (
+ profiles.map((p) => (
handleLoad(p.name)}
@@ -168,7 +186,9 @@ export function ProfileDialog({ open, onClose }: Props) {
{selectedData.name}
- {selectedData.modified}
+
+ {selectedData.modified}
+
@@ -182,7 +202,10 @@ export function ProfileDialog({ open, onClose }: Props) {
{ setRenameOpen(true); setRenameValue(selectedData.name); }}
+ onClick={() => {
+ setRenameOpen(true);
+ setRenameValue(selectedData.name);
+ }}
>
{t('profile.rename')}
@@ -200,18 +223,32 @@ export function ProfileDialog({ open, onClose }: Props) {
setRenameValue(e.target.value)}
+ onChange={(e) => setRenameValue(e.target.value)}
className="px-2 py-1 text-xs rounded border outline-none"
- style={{ backgroundColor: 'var(--app-list-bg)', color: 'var(--app-fg)', borderColor: 'var(--app-border)' }}
+ style={{
+ backgroundColor: 'var(--app-list-bg)',
+ color: 'var(--app-fg)',
+ borderColor: 'var(--app-border)',
+ }}
/>
-
+
{t('button.save')}
)}
-
-
+
+
)}
@@ -225,9 +262,13 @@ function PathSection({ title, paths }: { title: string; paths: PathEntry[] }) {
const { t } = useTranslation();
return (
-
{title}
+
+ {title}
+
{paths.length === 0 ? (
-
{t('profile.empty')}
+
+ {t('profile.empty')}
+
) : (
{paths.map((e) => (
diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx
index abfb008..c41e67d 100644
--- a/src/components/layout/AppShell.tsx
+++ b/src/components/layout/AppShell.tsx
@@ -28,19 +28,32 @@ export function AppShell() {
const setSelectedIndices = useAppStore((s) => s.setSelectedIndices);
const [editDialog, setEditDialog] = useState
({
- open: false, index: -1, value: '', target: TargetType.SYSTEM,
+ open: false,
+ index: -1,
+ value: '',
+ target: TargetType.SYSTEM,
});
const [newDialog, setNewDialog] = useState(false);
const [helpOpen, setHelpOpen] = useState(false);
const [importDialog, setImportDialog] = useState({
- open: false, system: [], user: [],
+ open: false,
+ system: [],
+ user: [],
});
const [analyzeOpen, setAnalyzeOpen] = useState(false);
const [profilesOpen, setProfilesOpen] = useState(false);
const actions = useAppActions(activeTab, {
- editDialog, newDialog, helpOpen, importDialog,
- setEditDialog, setNewDialog, setHelpOpen, setImportDialog, setAnalyzeOpen, setProfilesOpen,
+ editDialog,
+ newDialog,
+ helpOpen,
+ importDialog,
+ setEditDialog,
+ setNewDialog,
+ setHelpOpen,
+ setImportDialog,
+ setAnalyzeOpen,
+ setProfilesOpen,
});
const tabConfig: { id: TabId; label: string }[] = [
@@ -50,14 +63,20 @@ export function AppShell() {
];
return (
-
+
{tabConfig.map((tab) => (
{ setActiveTab(tab.id); setSelectedIndices([]); }}
+ onClick={() => {
+ setActiveTab(tab.id);
+ setSelectedIndices([]);
+ }}
className={`px-4 py-1.5 text-sm font-medium transition-colors ${activeTab === tab.id ? 'tab-active' : 'opacity-60'}`}
style={{ color: activeTab === tab.id ? '#3b82f6' : 'var(--app-fg)' }}
>
@@ -96,7 +115,10 @@ export function AppShell() {
{ e.preventDefault(); e.dataTransfer.dropEffect = 'link'; }}
+ onDragOver={(e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'link';
+ }}
onDrop={(e) => {
e.preventDefault();
if (activeTab === 'merged') return;
@@ -104,20 +126,47 @@ export function AppShell() {
const entry = e.dataTransfer.items[i].webkitGetAsEntry();
if (entry?.isDirectory) {
const file = e.dataTransfer.files[i] as TauriFile;
- if (file.path) useAppStore.getState().addPath(file.path, activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM);
+ if (file.path)
+ useAppStore
+ .getState()
+ .addPath(file.path, activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM);
}
}
}}
>
- {activeTab === 'merged' ?
:
}
+ {activeTab === 'merged' ? (
+
+ ) : (
+
+ )}
- setNewDialog(false)} />
- setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM })} />
+ setNewDialog(false)}
+ />
+
+ setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM })
+ }
+ />
setHelpOpen(false)} />
- setImportDialog({ open: false, system: [], user: [] })} />
+ setImportDialog({ open: false, system: [], user: [] })}
+ />
setAnalyzeOpen(false)} />
setProfilesOpen(false)} />
diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx
index 3aa9a0f..941b614 100644
--- a/src/components/layout/ErrorBoundary.tsx
+++ b/src/components/layout/ErrorBoundary.tsx
@@ -1,7 +1,12 @@
import { Component, type ReactNode } from 'react';
-interface Props { children: ReactNode; }
-interface State { hasError: boolean; error: string; }
+interface Props {
+ children: ReactNode;
+}
+interface State {
+ hasError: boolean;
+ error: string;
+}
export class ErrorBoundary extends Component
{
state: State = { hasError: false, error: '' };
@@ -18,7 +23,10 @@ export class ErrorBoundary extends Component {
render() {
if (this.state.hasError) {
return (
-
+
应用出错
{this.state.error}
diff --git a/src/components/layout/TitleBar.tsx b/src/components/layout/TitleBar.tsx
index 5a3f1dc..b2f2129 100644
--- a/src/components/layout/TitleBar.tsx
+++ b/src/components/layout/TitleBar.tsx
@@ -11,9 +11,7 @@ export function TitleBar() {
className="flex items-center justify-between px-4 py-2 border-b select-none"
style={{ borderColor: 'var(--app-border)' }}
>
-
- {isAdmin ? t('app.name') : t('app.nameReadonly')}
-
+
{isAdmin ? t('app.name') : t('app.nameReadonly')}
v{version}
);
diff --git a/src/components/path-list/PathTable.tsx b/src/components/path-list/PathTable.tsx
index c73cebf..992b75f 100644
--- a/src/components/path-list/PathTable.tsx
+++ b/src/components/path-list/PathTable.tsx
@@ -37,7 +37,8 @@ export function PathTable({ tabId }: PathTableProps) {
const result: PathRow[] = [];
for (let i = 0; i < paths.length; i++) {
const p = paths[i];
- if (p.path.toLowerCase().includes(q)) result.push({ path: p.path, index: i, enabled: p.enabled });
+ if (p.path.toLowerCase().includes(q))
+ result.push({ path: p.path, index: i, enabled: p.enabled });
}
return result;
}, [paths, searchQuery]);
@@ -141,7 +142,10 @@ export function PathTable({ tabId }: PathTableProps) {
: 'var(--app-list-alt)',
}}
>
-
+
{index + 1}
diff --git a/src/components/toolbar/ToolBar.tsx b/src/components/toolbar/ToolBar.tsx
index f45b412..4964054 100644
--- a/src/components/toolbar/ToolBar.tsx
+++ b/src/components/toolbar/ToolBar.tsx
@@ -36,12 +36,7 @@ export function ToolBar(props: ToolBarProps) {
-
+
{t('button.import')}
diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx
index ab4321c..3473552 100644
--- a/src/components/ui/Modal.tsx
+++ b/src/components/ui/Modal.tsx
@@ -9,7 +9,9 @@ interface ModalProps {
export function Modal({ open, onClose, children }: ModalProps) {
useEffect(() => {
if (!open) return;
- const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
+ const handler = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose();
+ };
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [open, onClose]);
diff --git a/src/components/ui/buttons.ts b/src/components/ui/buttons.ts
index dad96d2..56150c9 100644
--- a/src/components/ui/buttons.ts
+++ b/src/components/ui/buttons.ts
@@ -1,4 +1,5 @@
-export const btnClass = 'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
+export const btnClass =
+ 'px-3 py-1 text-sm rounded border transition-colors disabled:opacity-40 disabled:cursor-not-allowed';
export const btnStyle: React.CSSProperties = {
backgroundColor: 'var(--app-bg)',
diff --git a/src/core/import-export.ts b/src/core/import-export.ts
index aa5fd1c..4a2558b 100644
--- a/src/core/import-export.ts
+++ b/src/core/import-export.ts
@@ -28,8 +28,8 @@ export function exportToJson(data: ExportData): string {
const obj = {
version,
timestamp: new Date().toISOString(),
- system: data.system.map(e => ({ path: e.path, enabled: e.enabled })),
- user: data.user.map(e => ({ path: e.path, enabled: e.enabled })),
+ system: data.system.map((e) => ({ path: e.path, enabled: e.enabled })),
+ user: data.user.map((e) => ({ path: e.path, enabled: e.enabled })),
};
return JSON.stringify(obj, null, 2);
}
@@ -179,10 +179,14 @@ export function importFromJson(content: string): ImportResult {
};
if (Array.isArray(obj.system)) {
- result.system = obj.system.map(parseEntry).filter((e): e is { path: string; enabled: boolean } => e !== null);
+ result.system = obj.system
+ .map(parseEntry)
+ .filter((e): e is { path: string; enabled: boolean } => e !== null);
}
if (Array.isArray(obj.user)) {
- result.user = obj.user.map(parseEntry).filter((e): e is { path: string; enabled: boolean } => e !== null);
+ result.user = obj.user
+ .map(parseEntry)
+ .filter((e): e is { path: string; enabled: boolean } => e !== null);
}
return result;
@@ -210,10 +214,7 @@ export function importFromTxt(content: string): PathEntry[] {
// ── 自动检测导入 ──
-export function importFromContent(
- content: string,
- filepath: string,
-): ImportResult {
+export function importFromContent(content: string, filepath: string): ImportResult {
const lower = filepath.toLowerCase();
if (lower.endsWith('.csv')) {
return importFromCsv(content);
diff --git a/src/core/path-manager.ts b/src/core/path-manager.ts
index e4a335e..73a58bf 100644
--- a/src/core/path-manager.ts
+++ b/src/core/path-manager.ts
@@ -21,7 +21,11 @@ export function analyzePaths(
const lower = entry.path.toLowerCase();
const isDuplicate = seen.has(lower);
seen.add(lower);
- result.push({ isValid: validateFn(entry.path), isDuplicate, isEnvVar: entry.path.includes('%') });
+ result.push({
+ isValid: validateFn(entry.path),
+ isDuplicate,
+ isEnvVar: entry.path.includes('%'),
+ });
}
return result;
diff --git a/src/core/undo-redo.ts b/src/core/undo-redo.ts
index edc54e2..775e68e 100644
--- a/src/core/undo-redo.ts
+++ b/src/core/undo-redo.ts
@@ -5,7 +5,16 @@
import type { PathEntry } from './path-entry';
export const OperationType = {
- ADD: 0, DELETE: 1, EDIT: 2, MOVE_UP: 3, MOVE_DOWN: 4, CLEAN: 5, CLEAR: 6, IMPORT: 7, TOGGLE: 8, IMPORT_BOTH: 9,
+ ADD: 0,
+ DELETE: 1,
+ EDIT: 2,
+ MOVE_UP: 3,
+ MOVE_DOWN: 4,
+ CLEAN: 5,
+ CLEAR: 6,
+ IMPORT: 7,
+ TOGGLE: 8,
+ IMPORT_BOTH: 9,
} as const;
export type OperationType = (typeof OperationType)[keyof typeof OperationType];
@@ -47,7 +56,10 @@ export class UndoRedoManager {
this.current = this.records.length - 1;
}
- undo(sysPaths: readonly PathEntry[], userPaths: readonly PathEntry[]): [PathEntry[], PathEntry[]] | null {
+ undo(
+ sysPaths: readonly PathEntry[],
+ userPaths: readonly PathEntry[],
+ ): [PathEntry[], PathEntry[]] | null {
if (this.current < 0) return null;
const rec = this.records[this.current];
@@ -103,7 +115,10 @@ export class UndoRedoManager {
return [sys, user];
}
- redo(sysPaths: readonly PathEntry[], userPaths: readonly PathEntry[]): [PathEntry[], PathEntry[]] | null {
+ redo(
+ sysPaths: readonly PathEntry[],
+ userPaths: readonly PathEntry[],
+ ): [PathEntry[], PathEntry[]] | null {
if (this.current >= this.records.length - 1) return null;
this.current++;
@@ -159,8 +174,17 @@ export class UndoRedoManager {
return [sys, user];
}
- canUndo(): boolean { return this.current >= 0; }
- canRedo(): boolean { return this.current < this.records.length - 1; }
- clear(): void { this.records = []; this.current = -1; }
- get historyLength(): number { return this.records.length; }
+ canUndo(): boolean {
+ return this.current >= 0;
+ }
+ canRedo(): boolean {
+ return this.current < this.records.length - 1;
+ }
+ clear(): void {
+ this.records = [];
+ this.current = -1;
+ }
+ get historyLength(): number {
+ return this.records.length;
+ }
}
diff --git a/src/hooks/use-app-actions.ts b/src/hooks/use-app-actions.ts
index 5b344e2..6479ddd 100644
--- a/src/hooks/use-app-actions.ts
+++ b/src/hooks/use-app-actions.ts
@@ -3,7 +3,12 @@ import { useAppStore } from '@/store/app-store';
import { TargetType } from '@/core/undo-redo';
import { open } from '@tauri-apps/plugin-dialog';
import { invoke } from '@tauri-apps/api/core';
-import { importFromContent, exportToJson, exportToCsv, flattenImportResult } from '@/core/import-export';
+import {
+ importFromContent,
+ exportToJson,
+ exportToCsv,
+ flattenImportResult,
+} from '@/core/import-export';
import type { PathEntry } from '@/core/path-entry';
import { is_valid_path_format } from '@/core/validation';
import { useKeyboard } from './use-keyboard';
@@ -38,9 +43,10 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
const idx = useAppStore.getState().selectedIndices[0];
if (idx === undefined) return;
const target = activeTab === 'user' ? TargetType.USER : TargetType.SYSTEM;
- const list = target === TargetType.SYSTEM
- ? useAppStore.getState().sysPaths
- : useAppStore.getState().userPaths;
+ const list =
+ target === TargetType.SYSTEM
+ ? useAppStore.getState().sysPaths
+ : useAppStore.getState().userPaths;
const entry = list[idx];
if (entry) setEditDialog({ open: true, index: idx, value: entry.path, target });
}, [activeTab, setEditDialog]);
@@ -71,14 +77,9 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
}, [getCurrentTarget]);
const handleClean = useCallback(() => {
- const removed = useAppStore.getState().cleanPaths(
- getCurrentTarget(),
- is_valid_path_format,
- );
+ const removed = useAppStore.getState().cleanPaths(getCurrentTarget(), is_valid_path_format);
if (removed.length > 0) {
- useAppStore.getState().setStatusMessage(
- i18n.t('status.deleted', { count: removed.length }),
- );
+ useAppStore.getState().setStatusMessage(i18n.t('status.deleted', { count: removed.length }));
}
}, [getCurrentTarget]);
@@ -95,9 +96,15 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
if (result.system.length > 0 && result.user.length > 0) {
setImportDialog({ open: true, system: result.system, user: result.user });
} else if (result.system.length > 0) {
- useAppStore.getState().replacePaths(TargetType.SYSTEM, result.system.map(e => e.path));
+ useAppStore.getState().replacePaths(
+ TargetType.SYSTEM,
+ result.system.map((e) => e.path),
+ );
} else if (result.user.length > 0) {
- useAppStore.getState().replacePaths(TargetType.USER, result.user.map(e => e.path));
+ useAppStore.getState().replacePaths(
+ TargetType.USER,
+ result.user.map((e) => e.path),
+ );
}
}, [setImportDialog]);
@@ -122,7 +129,10 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
if (result.kind === 'warning') {
// 长度超限,需要用户确认
const { ask } = await import('@tauri-apps/plugin-dialog');
- const confirmed = await ask(i18n.t('status.saveWarningLongPaths'), { title: i18n.t('dialog.backupTitle'), kind: 'warning' });
+ const confirmed = await ask(i18n.t('status.saveWarningLongPaths'), {
+ title: i18n.t('dialog.backupTitle'),
+ kind: 'warning',
+ });
if (confirmed) {
await useAppStore.getState().savePaths(true);
}
@@ -156,33 +166,62 @@ export function useAppActions(activeTab: TabId, dialogs: DialogState) {
// ── 弹窗确认 ──
- const handleNewConfirm = useCallback((value: string) => {
- setNewDialog(false);
- if (value.trim()) useAppStore.getState().addPath(value.trim(), getCurrentTarget());
- }, [getCurrentTarget, setNewDialog]);
+ const handleNewConfirm = useCallback(
+ (value: string) => {
+ setNewDialog(false);
+ if (value.trim()) useAppStore.getState().addPath(value.trim(), getCurrentTarget());
+ },
+ [getCurrentTarget, setNewDialog],
+ );
- const handleEditConfirm = useCallback((value: string) => {
- const d = dialogs.editDialog;
- setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
- if (value.trim()) useAppStore.getState().editPath(d.index, value.trim(), d.target);
- }, [dialogs.editDialog, setEditDialog]);
+ const handleEditConfirm = useCallback(
+ (value: string) => {
+ const d = dialogs.editDialog;
+ setEditDialog({ open: false, index: -1, value: '', target: TargetType.SYSTEM });
+ if (value.trim()) useAppStore.getState().editPath(d.index, value.trim(), d.target);
+ },
+ [dialogs.editDialog, setEditDialog],
+ );
- const handleImportSelect = useCallback((target: 'system' | 'user' | 'both') => {
- const { system, user } = dialogs.importDialog;
- const flat = flattenImportResult({ system, user }, target);
- if (target === 'both' && flat.system.length > 0 && flat.user.length > 0) {
- useAppStore.getState().replaceBothPaths(flat.system.map(e => e.path), flat.user.map(e => e.path));
- } else {
- if (flat.system.length > 0) useAppStore.getState().replacePaths(TargetType.SYSTEM, flat.system.map(e => e.path));
- if (flat.user.length > 0) useAppStore.getState().replacePaths(TargetType.USER, flat.user.map(e => e.path));
- }
- setImportDialog({ open: false, system: [], user: [] });
- }, [dialogs.importDialog, setImportDialog]);
+ const handleImportSelect = useCallback(
+ (target: 'system' | 'user' | 'both') => {
+ const { system, user } = dialogs.importDialog;
+ const flat = flattenImportResult({ system, user }, target);
+ if (target === 'both' && flat.system.length > 0 && flat.user.length > 0) {
+ useAppStore.getState().replaceBothPaths(
+ flat.system.map((e) => e.path),
+ flat.user.map((e) => e.path),
+ );
+ } else {
+ if (flat.system.length > 0)
+ useAppStore.getState().replacePaths(
+ TargetType.SYSTEM,
+ flat.system.map((e) => e.path),
+ );
+ if (flat.user.length > 0)
+ useAppStore.getState().replacePaths(
+ TargetType.USER,
+ flat.user.map((e) => e.path),
+ );
+ }
+ setImportDialog({ open: false, system: [], user: [] });
+ },
+ [dialogs.importDialog, setImportDialog],
+ );
return {
- handleNew, handleEdit, handleBrowse, handleDelete,
- handleMoveUp, handleMoveDown, handleClean,
- handleImport, handleExport, handleSave,
- handleNewConfirm, handleEditConfirm, handleImportSelect,
+ handleNew,
+ handleEdit,
+ handleBrowse,
+ handleDelete,
+ handleMoveUp,
+ handleMoveDown,
+ handleClean,
+ handleImport,
+ handleExport,
+ handleSave,
+ handleNewConfirm,
+ handleEditConfirm,
+ handleImportSelect,
};
}
diff --git a/src/hooks/use-path-validation.ts b/src/hooks/use-path-validation.ts
index e0818b2..8cd7f22 100644
--- a/src/hooks/use-path-validation.ts
+++ b/src/hooks/use-path-validation.ts
@@ -60,17 +60,15 @@ export function usePathValidation(paths: readonly PathEntry[]) {
const batch = toValidate.slice(0, 20);
Promise.all(
- batch.map(
- async (p): Promise<[string, ValidationState]> => {
- try {
- if (p.path.includes('%')) return [p.path, 'valid'];
- const valid: boolean = await invoke('validate_path', { path: p.path });
- return [p.path, valid ? 'valid' : 'invalid'];
- } catch {
- return [p.path, 'unknown'];
- }
- },
- ),
+ batch.map(async (p): Promise<[string, ValidationState]> => {
+ try {
+ if (p.path.includes('%')) return [p.path, 'valid'];
+ const valid: boolean = await invoke('validate_path', { path: p.path });
+ return [p.path, valid ? 'valid' : 'invalid'];
+ } catch {
+ return [p.path, 'unknown'];
+ }
+ }),
).then((results) => {
if (cancelled) return;
for (const [p] of results) validatedRef.current.add(p);
@@ -89,23 +87,19 @@ export function usePathValidation(paths: readonly PathEntry[]) {
// 异步展开环境变量(setState 在 .then() 回调中)
useEffect(() => {
let cancelled = false;
- const toExpand = paths.filter(
- (p) => p.path.includes('%') && !expandedRef.current.has(p.path),
- );
+ const toExpand = paths.filter((p) => p.path.includes('%') && !expandedRef.current.has(p.path));
if (toExpand.length === 0) return;
const batch = toExpand.slice(0, 20);
Promise.all(
- batch.map(
- async (p): Promise<[string, string]> => {
- try {
- const expanded: string = await invoke('expand_env_vars', { path: p.path });
- return [p.path, expanded !== p.path ? expanded : ''];
- } catch {
- return [p.path, ''];
- }
- },
- ),
+ batch.map(async (p): Promise<[string, string]> => {
+ try {
+ const expanded: string = await invoke('expand_env_vars', { path: p.path });
+ return [p.path, expanded !== p.path ? expanded : ''];
+ } catch {
+ return [p.path, ''];
+ }
+ }),
).then((results) => {
if (cancelled) return;
for (const [p] of results) expandedRef.current.add(p);
diff --git a/src/store/app-store.ts b/src/store/app-store.ts
index e39907a..055c740 100644
--- a/src/store/app-store.ts
+++ b/src/store/app-store.ts
@@ -54,11 +54,12 @@ interface AppState {
loadPaths: () => Promise;
savePaths: (force?: boolean) => Promise;
initialize: () => Promise;
-
}
function arraysEqual(a: readonly PathEntry[], b: readonly PathEntry[]): boolean {
- return a.length === b.length && a.every((v, i) => v.path === b[i].path && v.enabled === b[i].enabled);
+ return (
+ a.length === b.length && a.every((v, i) => v.path === b[i].path && v.enabled === b[i].enabled)
+ );
}
export const useAppStore = create((set, get) => {
@@ -69,335 +70,405 @@ export const useAppStore = create((set, get) => {
return {
sysPaths: [],
- userPaths: [],
- undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
- _savedSys: [],
- _savedUser: [],
+ userPaths: [],
+ undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
+ _savedSys: [],
+ _savedUser: [],
- activeTab: 'system',
- searchQuery: '',
- selectedIndices: [],
- isAdmin: false,
- statusMessage: '',
- isModified: false,
- isLoading: true,
- isSaving: false,
+ activeTab: 'system',
+ searchQuery: '',
+ selectedIndices: [],
+ isAdmin: false,
+ statusMessage: '',
+ isModified: false,
+ isLoading: true,
+ isSaving: false,
- setActiveTab: (tab) => set({ activeTab: tab }),
- setSearchQuery: (query) => set({ searchQuery: query }),
- setSelectedIndices: (indices) => set({ selectedIndices: indices }),
- setStatusMessage: (msg) => set({ statusMessage: msg }),
+ setActiveTab: (tab) => set({ activeTab: tab }),
+ setSearchQuery: (query) => set({ searchQuery: query }),
+ setSelectedIndices: (indices) => set({ selectedIndices: indices }),
+ setStatusMessage: (msg) => set({ statusMessage: msg }),
- addPath: (path, target) => {
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- const entry: PathEntry = { path, enabled: true };
- const newList = [...list, entry];
- state.undoRedo.push({
- type: OperationType.ADD, target, index: newList.length - 1, count: 1,
- oldPaths: [], newPaths: [entry],
- });
- if (target === TargetType.SYSTEM) set({ sysPaths: newList });
- else set({ userPaths: newList });
- markDirty();
- },
-
- editPath: (index, newPath, target) => {
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- const oldEntry = list[index];
- if (!oldEntry) return;
- const newEntry: PathEntry = { path: newPath, enabled: oldEntry.enabled };
- state.undoRedo.push({
- type: OperationType.EDIT, target, index, count: 1,
- oldPaths: [oldEntry], newPaths: [newEntry],
- });
- const newList = [...list];
- newList[index] = newEntry;
- if (target === TargetType.SYSTEM) set({ sysPaths: newList });
- else set({ userPaths: newList });
- markDirty();
- },
-
- deletePaths: (indices, target) => {
- if (indices.length === 0) return;
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- const sortedDesc = [...indices].sort((a, b) => b - a);
- const sortedAsc = [...indices].sort((a, b) => a - b);
- const oldPaths = sortedAsc.map((i) => list[i]);
-
- state.undoRedo.push({
- type: OperationType.DELETE, target,
- index: sortedAsc[0], count: sortedAsc.length,
- oldPaths, newPaths: [],
- indices: sortedAsc,
- });
-
- const toRemove = new Set(sortedDesc);
- const newList = list.filter((_, i) => !toRemove.has(i));
- if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] });
- else set({ userPaths: newList, selectedIndices: [] });
- markDirty();
- },
-
- moveUp: (index, target) => {
- if (index <= 0) return;
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- state.undoRedo.push({
- type: OperationType.MOVE_UP, target, index, count: 1, oldPaths: [], newPaths: [],
- });
- const newList = [...list];
- [newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
- if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1] });
- else set({ userPaths: newList, selectedIndices: [index - 1] });
- markDirty();
- },
-
- moveDown: (index, target) => {
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- if (index >= list.length - 1) return;
- state.undoRedo.push({
- type: OperationType.MOVE_DOWN, target, index, count: 1, oldPaths: [], newPaths: [],
- });
- const newList = [...list];
- [newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
- if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1] });
- else set({ userPaths: newList, selectedIndices: [index + 1] });
- markDirty();
- },
-
- cleanPaths: (target, validateFn) => {
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- const [kept, removed] = pathClean(list, validateFn);
-
- if (removed.length > 0) {
+ addPath: (path, target) => {
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ const entry: PathEntry = { path, enabled: true };
+ const newList = [...list, entry];
state.undoRedo.push({
- type: OperationType.CLEAN, target, index: 0, count: removed.length,
- oldPaths: [...list], newPaths: kept,
+ type: OperationType.ADD,
+ target,
+ index: newList.length - 1,
+ count: 1,
+ oldPaths: [],
+ newPaths: [entry],
});
- if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [] });
- else set({ userPaths: kept, selectedIndices: [] });
+ if (target === TargetType.SYSTEM) set({ sysPaths: newList });
+ else set({ userPaths: newList });
markDirty();
- }
+ },
- return removed.map(e => e.path);
- },
-
- replacePaths: (target, newPaths) => {
- if (newPaths.length === 0) return;
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- const entries: PathEntry[] = newPaths.map(p => ({ path: p, enabled: true }));
-
- state.undoRedo.push({
- type: OperationType.IMPORT, target, index: 0, count: entries.length,
- oldPaths: [...list], newPaths: [...entries],
- });
-
- if (target === TargetType.SYSTEM) set({ sysPaths: [...entries], selectedIndices: [] });
- else set({ userPaths: [...entries], selectedIndices: [] });
- markDirty();
- },
-
- replaceBothPaths: (sysPaths, userPaths) => {
- const state = get();
- const sysEntries: PathEntry[] = sysPaths.map(p => ({ path: p, enabled: true }));
- const usrEntries: PathEntry[] = userPaths.map(p => ({ path: p, enabled: true }));
- state.undoRedo.push({
- type: OperationType.IMPORT_BOTH, target: TargetType.SYSTEM, index: 0,
- count: sysEntries.length + usrEntries.length,
- oldPaths: [...state.sysPaths], newPaths: [...sysEntries],
- oldPathsOther: [...state.userPaths], newPathsOther: [...usrEntries],
- });
- set({ sysPaths: [...sysEntries], userPaths: [...usrEntries], selectedIndices: [] });
- markDirty();
- },
-
- clearPaths: (target) => {
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- if (list.length === 0) return;
-
- state.undoRedo.push({
- type: OperationType.CLEAR, target, index: 0, count: list.length,
- oldPaths: [...list], newPaths: [],
- });
-
- if (target === TargetType.SYSTEM) set({ sysPaths: [] });
- else set({ userPaths: [] });
- markDirty();
- },
-
- togglePath: (index, target) => {
- const state = get();
- const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
- const oldEntry = list[index];
- if (!oldEntry) return;
- const newEntry: PathEntry = { path: oldEntry.path, enabled: !oldEntry.enabled };
-
- state.undoRedo.push({
- type: OperationType.TOGGLE, target, index, count: 1,
- oldPaths: [oldEntry], newPaths: [newEntry],
- });
-
- const newList = [...list];
- newList[index] = newEntry;
- if (target === TargetType.SYSTEM) set({ sysPaths: newList });
- else set({ userPaths: newList });
- markDirty();
-
- // 即时保存禁用状态
- const { sysPaths: sys, userPaths: usr } = get();
- const sysDisabled = sys.filter(e => !e.enabled).map(e => e.path);
- const usrDisabled = usr.filter(e => !e.enabled).map(e => e.path);
- invoke('save_disabled_state', { system: sysDisabled, user: usrDisabled })
- .catch((e) => console.warn('保存禁用状态失败:', e));
- },
-
- undo: () => {
- const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
- const result = undoRedo.undo(sysPaths, userPaths);
- if (result) {
- set({
- sysPaths: result[0], userPaths: result[1], selectedIndices: [],
- // 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
- isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
+ editPath: (index, newPath, target) => {
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ const oldEntry = list[index];
+ if (!oldEntry) return;
+ const newEntry: PathEntry = { path: newPath, enabled: oldEntry.enabled };
+ state.undoRedo.push({
+ type: OperationType.EDIT,
+ target,
+ index,
+ count: 1,
+ oldPaths: [oldEntry],
+ newPaths: [newEntry],
});
- // 同步持久化 disabled 状态,与 togglePath 保持一致
- invoke('save_disabled_state', {
- system: result[0].filter(e => !e.enabled).map(e => e.path),
- user: result[1].filter(e => !e.enabled).map(e => e.path),
- }).catch((e) => console.warn('保存禁用状态失败:', e));
- }
- },
+ const newList = [...list];
+ newList[index] = newEntry;
+ if (target === TargetType.SYSTEM) set({ sysPaths: newList });
+ else set({ userPaths: newList });
+ markDirty();
+ },
- redo: () => {
- const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
- const result = undoRedo.redo(sysPaths, userPaths);
- if (result) {
- set({
- sysPaths: result[0], userPaths: result[1], selectedIndices: [],
- // 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
- isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
+ deletePaths: (indices, target) => {
+ if (indices.length === 0) return;
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ const sortedDesc = [...indices].sort((a, b) => b - a);
+ const sortedAsc = [...indices].sort((a, b) => a - b);
+ const oldPaths = sortedAsc.map((i) => list[i]);
+
+ state.undoRedo.push({
+ type: OperationType.DELETE,
+ target,
+ index: sortedAsc[0],
+ count: sortedAsc.length,
+ oldPaths,
+ newPaths: [],
+ indices: sortedAsc,
});
- // 同步持久化 disabled 状态,与 togglePath 保持一致
- invoke('save_disabled_state', {
- system: result[0].filter(e => !e.enabled).map(e => e.path),
- user: result[1].filter(e => !e.enabled).map(e => e.path),
- }).catch((e) => console.warn('保存禁用状态失败:', e));
- }
- },
- loadPaths: async () => {
- try {
- set({ isLoading: true });
- const [sysArr, userArr] = await Promise.all([
- invoke('load_system_paths'),
- invoke('load_user_paths'),
+ const toRemove = new Set(sortedDesc);
+ const newList = list.filter((_, i) => !toRemove.has(i));
+ if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [] });
+ else set({ userPaths: newList, selectedIndices: [] });
+ markDirty();
+ },
+
+ moveUp: (index, target) => {
+ if (index <= 0) return;
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ state.undoRedo.push({
+ type: OperationType.MOVE_UP,
+ target,
+ index,
+ count: 1,
+ oldPaths: [],
+ newPaths: [],
+ });
+ const newList = [...list];
+ [newList[index - 1], newList[index]] = [newList[index], newList[index - 1]];
+ if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index - 1] });
+ else set({ userPaths: newList, selectedIndices: [index - 1] });
+ markDirty();
+ },
+
+ moveDown: (index, target) => {
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ if (index >= list.length - 1) return;
+ state.undoRedo.push({
+ type: OperationType.MOVE_DOWN,
+ target,
+ index,
+ count: 1,
+ oldPaths: [],
+ newPaths: [],
+ });
+ const newList = [...list];
+ [newList[index], newList[index + 1]] = [newList[index + 1], newList[index]];
+ if (target === TargetType.SYSTEM) set({ sysPaths: newList, selectedIndices: [index + 1] });
+ else set({ userPaths: newList, selectedIndices: [index + 1] });
+ markDirty();
+ },
+
+ cleanPaths: (target, validateFn) => {
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ const [kept, removed] = pathClean(list, validateFn);
+
+ if (removed.length > 0) {
+ state.undoRedo.push({
+ type: OperationType.CLEAN,
+ target,
+ index: 0,
+ count: removed.length,
+ oldPaths: [...list],
+ newPaths: kept,
+ });
+ if (target === TargetType.SYSTEM) set({ sysPaths: kept, selectedIndices: [] });
+ else set({ userPaths: kept, selectedIndices: [] });
+ markDirty();
+ }
+
+ return removed.map((e) => e.path);
+ },
+
+ replacePaths: (target, newPaths) => {
+ if (newPaths.length === 0) return;
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ const entries: PathEntry[] = newPaths.map((p) => ({ path: p, enabled: true }));
+
+ state.undoRedo.push({
+ type: OperationType.IMPORT,
+ target,
+ index: 0,
+ count: entries.length,
+ oldPaths: [...list],
+ newPaths: [...entries],
+ });
+
+ if (target === TargetType.SYSTEM) set({ sysPaths: [...entries], selectedIndices: [] });
+ else set({ userPaths: [...entries], selectedIndices: [] });
+ markDirty();
+ },
+
+ replaceBothPaths: (sysPaths, userPaths) => {
+ const state = get();
+ const sysEntries: PathEntry[] = sysPaths.map((p) => ({ path: p, enabled: true }));
+ const usrEntries: PathEntry[] = userPaths.map((p) => ({ path: p, enabled: true }));
+ state.undoRedo.push({
+ type: OperationType.IMPORT_BOTH,
+ target: TargetType.SYSTEM,
+ index: 0,
+ count: sysEntries.length + usrEntries.length,
+ oldPaths: [...state.sysPaths],
+ newPaths: [...sysEntries],
+ oldPathsOther: [...state.userPaths],
+ newPathsOther: [...usrEntries],
+ });
+ set({ sysPaths: [...sysEntries], userPaths: [...usrEntries], selectedIndices: [] });
+ markDirty();
+ },
+
+ clearPaths: (target) => {
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ if (list.length === 0) return;
+
+ state.undoRedo.push({
+ type: OperationType.CLEAR,
+ target,
+ index: 0,
+ count: list.length,
+ oldPaths: [...list],
+ newPaths: [],
+ });
+
+ if (target === TargetType.SYSTEM) set({ sysPaths: [] });
+ else set({ userPaths: [] });
+ markDirty();
+ },
+
+ togglePath: (index, target) => {
+ const state = get();
+ const list = target === TargetType.SYSTEM ? state.sysPaths : state.userPaths;
+ const oldEntry = list[index];
+ if (!oldEntry) return;
+ const newEntry: PathEntry = { path: oldEntry.path, enabled: !oldEntry.enabled };
+
+ state.undoRedo.push({
+ type: OperationType.TOGGLE,
+ target,
+ index,
+ count: 1,
+ oldPaths: [oldEntry],
+ newPaths: [newEntry],
+ });
+
+ const newList = [...list];
+ newList[index] = newEntry;
+ if (target === TargetType.SYSTEM) set({ sysPaths: newList });
+ else set({ userPaths: newList });
+ markDirty();
+
+ // 即时保存禁用状态
+ const { sysPaths: sys, userPaths: usr } = get();
+ const sysDisabled = sys.filter((e) => !e.enabled).map((e) => e.path);
+ const usrDisabled = usr.filter((e) => !e.enabled).map((e) => e.path);
+ invoke('save_disabled_state', { system: sysDisabled, user: usrDisabled }).catch((e) =>
+ console.warn('保存禁用状态失败:', e),
+ );
+ },
+
+ undo: () => {
+ const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
+ const result = undoRedo.undo(sysPaths, userPaths);
+ if (result) {
+ set({
+ sysPaths: result[0],
+ userPaths: result[1],
+ selectedIndices: [],
+ // 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
+ isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
+ });
+ // 同步持久化 disabled 状态,与 togglePath 保持一致
+ invoke('save_disabled_state', {
+ system: result[0].filter((e) => !e.enabled).map((e) => e.path),
+ user: result[1].filter((e) => !e.enabled).map((e) => e.path),
+ }).catch((e) => console.warn('保存禁用状态失败:', e));
+ }
+ },
+
+ redo: () => {
+ const { undoRedo, sysPaths, userPaths, _savedSys, _savedUser } = get();
+ const result = undoRedo.redo(sysPaths, userPaths);
+ if (result) {
+ set({
+ sysPaths: result[0],
+ userPaths: result[1],
+ selectedIndices: [],
+ // 内联计算 isModified 而非调用 markDirty(),避免两次 set() 导致额外渲染
+ isModified: !(arraysEqual(result[0], _savedSys) && arraysEqual(result[1], _savedUser)),
+ });
+ // 同步持久化 disabled 状态,与 togglePath 保持一致
+ invoke('save_disabled_state', {
+ system: result[0].filter((e) => !e.enabled).map((e) => e.path),
+ user: result[1].filter((e) => !e.enabled).map((e) => e.path),
+ }).catch((e) => console.warn('保存禁用状态失败:', e));
+ }
+ },
+
+ loadPaths: async () => {
+ try {
+ set({ isLoading: true });
+ const [sysArr, userArr] = await Promise.all([
+ invoke('load_system_paths'),
+ invoke('load_user_paths'),
+ ]);
+
+ // 加载禁用状态(文件不存在时返回空)
+ let sysDisabled: string[] = [];
+ let usrDisabled: string[] = [];
+ try {
+ const result = await invoke<[string[], string[]]>('load_disabled_state');
+ sysDisabled = result[0];
+ usrDisabled = result[1];
+ } catch {
+ // 文件不存在或损坏,忽略
+ }
+
+ const sysSet = new Set(sysDisabled);
+ const usrSet = new Set(usrDisabled);
+
+ const sysEntries: PathEntry[] = sysArr.map((p) => ({ path: p, enabled: !sysSet.has(p) }));
+ const usrEntries: PathEntry[] = userArr.map((p) => ({ path: p, enabled: !usrSet.has(p) }));
+
+ set({
+ sysPaths: sysEntries,
+ userPaths: usrEntries,
+ _savedSys: [...sysEntries],
+ _savedUser: [...usrEntries],
+ undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
+ isLoading: false,
+ isModified: false,
+ statusMessage: i18n.t('status.loaded', {
+ sysCount: sysArr.length,
+ userCount: userArr.length,
+ }),
+ });
+ } catch (e) {
+ set({ isLoading: false, statusMessage: `${i18n.t('status.error')}: ${String(e)}` });
+ }
+ },
+
+ savePaths: async (force?: boolean) => {
+ const state = get();
+ if (state.isSaving) return { kind: 'blocked' };
+ set({ isSaving: true, statusMessage: i18n.t('status.saving') });
+
+ // 只保存 enabled 的路径到注册表
+ const sysPaths = state.sysPaths.filter((e) => e.enabled).map((e) => e.path);
+ const userPaths = state.userPaths.filter((e) => e.enabled).map((e) => e.path);
+ const sysJoined = sysPaths.join(';');
+ const userJoined = userPaths.join(';');
+
+ // 长度检查:非强制模式下返回警告,由 UI 层确认
+ const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
+ if (
+ !force &&
+ (sysJoined.length > maxSystemLength ||
+ userJoined.length > maxUserLength ||
+ (sysJoined + userJoined).length > maxCombinedLength)
+ ) {
+ set({ isSaving: false, statusMessage: i18n.t('status.saveWarningLongPaths') });
+ return { kind: 'warning', reason: 'lengthExceeded' };
+ }
+
+ // 备份当前注册表(保存前备份旧值,失败仅警告不中断)
+ let backupFailed = false;
+ await invoke('backup_registry', { customDir: null }).catch(() => {
+ backupFailed = true;
+ });
+
+ const origSys = state._savedSys.filter((e) => e.enabled).map((e) => e.path);
+ const origUser = state._savedUser.filter((e) => e.enabled).map((e) => e.path);
+
+ const [sysResult, userResult] = await Promise.allSettled([
+ invoke('save_system_paths', { paths: sysPaths, original: origSys }),
+ invoke('save_user_paths', { paths: userPaths, original: origUser }),
]);
- // 加载禁用状态(文件不存在时返回空)
- let sysDisabled: string[] = [];
- let usrDisabled: string[] = [];
- try {
- const result = await invoke<[string[], string[]]>('load_disabled_state');
- sysDisabled = result[0];
- usrDisabled = result[1];
- } catch {
- // 文件不存在或损坏,忽略
- }
+ const sysOk = sysResult.status === 'fulfilled';
+ const userOk = userResult.status === 'fulfilled';
- const sysSet = new Set(sysDisabled);
- const usrSet = new Set(usrDisabled);
-
- const sysEntries: PathEntry[] = sysArr.map(p => ({ path: p, enabled: !sysSet.has(p) }));
- const usrEntries: PathEntry[] = userArr.map(p => ({ path: p, enabled: !usrSet.has(p) }));
-
- set({
- sysPaths: sysEntries, userPaths: usrEntries,
- _savedSys: [...sysEntries], _savedUser: [...usrEntries],
- undoRedo: new UndoRedoManager(appConfig.undo.maxHistory),
- isLoading: false, isModified: false,
- statusMessage: i18n.t('status.loaded', { sysCount: sysArr.length, userCount: userArr.length }),
- });
- } catch (e) {
- set({ isLoading: false, statusMessage: `${i18n.t('status.error')}: ${String(e)}` });
- }
- },
-
- savePaths: async (force?: boolean) => {
- const state = get();
- if (state.isSaving) return { kind: 'blocked' };
- set({ isSaving: true, statusMessage: i18n.t('status.saving') });
-
- // 只保存 enabled 的路径到注册表
- const sysPaths = state.sysPaths.filter(e => e.enabled).map(e => e.path);
- const userPaths = state.userPaths.filter(e => e.enabled).map(e => e.path);
- const sysJoined = sysPaths.join(';');
- const userJoined = userPaths.join(';');
-
- // 长度检查:非强制模式下返回警告,由 UI 层确认
- const { maxSystemLength, maxUserLength, maxCombinedLength } = appConfig.path;
- if (!force && (sysJoined.length > maxSystemLength || userJoined.length > maxUserLength || (sysJoined + userJoined).length > maxCombinedLength)) {
- set({ isSaving: false, statusMessage: i18n.t('status.saveWarningLongPaths') });
- return { kind: 'warning', reason: 'lengthExceeded' };
- }
-
- // 备份当前注册表(保存前备份旧值,失败仅警告不中断)
- let backupFailed = false;
- await invoke('backup_registry', { customDir: null })
- .catch(() => { backupFailed = true; });
-
- const origSys = state._savedSys.filter(e => e.enabled).map(e => e.path);
- const origUser = state._savedUser.filter(e => e.enabled).map(e => e.path);
-
- const [sysResult, userResult] = await Promise.allSettled([
- invoke('save_system_paths', { paths: sysPaths, original: origSys }),
- invoke('save_user_paths', { paths: userPaths, original: origUser }),
- ]);
-
- const sysOk = sysResult.status === 'fulfilled';
- const userOk = userResult.status === 'fulfilled';
-
- if (sysOk && userOk) {
- invoke('broadcast_env_change').catch(() => {});
- const savedSys = [...state.sysPaths], savedUser = [...state.userPaths];
- set({ isModified: false, isSaving: false,
- statusMessage: backupFailed ? i18n.t('status.saved_without_backup') : i18n.t('status.saved'),
- _savedSys: savedSys, _savedUser: savedUser });
- return { kind: 'success' };
- } else {
- const sysErr = (!sysOk && sysResult.status === 'rejected') ? String(sysResult.reason) : '';
- const usrErr = (!userOk && userResult.status === 'rejected') ? String(userResult.reason) : '';
- const parts = [sysErr, usrErr].filter(Boolean);
-
- const msg = sysOk ? `用户 PATH 保存失败: ${usrErr}` : userOk ? `系统 PATH 保存失败: ${sysErr}` : `保存失败: ${parts.join('; ')}`;
-
- if (sysOk || userOk) {
- // partial success
- set({ isSaving: false });
- await get().loadPaths(); // reload to avoid state drift
- set({ statusMessage: msg }); // restore the error message overwritten by loadPaths
- return { kind: 'partial', message: msg };
+ if (sysOk && userOk) {
+ invoke('broadcast_env_change').catch(() => {});
+ const savedSys = [...state.sysPaths],
+ savedUser = [...state.userPaths];
+ set({
+ isModified: false,
+ isSaving: false,
+ statusMessage: backupFailed
+ ? i18n.t('status.saved_without_backup')
+ : i18n.t('status.saved'),
+ _savedSys: savedSys,
+ _savedUser: savedUser,
+ });
+ return { kind: 'success' };
} else {
- set({ isSaving: false, statusMessage: msg });
- return { kind: 'failure', message: msg };
- }
- }
- },
+ const sysErr = !sysOk && sysResult.status === 'rejected' ? String(sysResult.reason) : '';
+ const usrErr = !userOk && userResult.status === 'rejected' ? String(userResult.reason) : '';
+ const parts = [sysErr, usrErr].filter(Boolean);
- initialize: async () => {
- try {
- const isAdmin: boolean = await invoke('check_admin');
- set({ isAdmin });
- if (!isAdmin) set({ statusMessage: i18n.t('status.readonly') });
- } catch {
- set({ isAdmin: false, statusMessage: i18n.t('status.readonly') });
- }
- await get().loadPaths();
- },
-};});
+ const msg = sysOk
+ ? `用户 PATH 保存失败: ${usrErr}`
+ : userOk
+ ? `系统 PATH 保存失败: ${sysErr}`
+ : `保存失败: ${parts.join('; ')}`;
+
+ if (sysOk || userOk) {
+ // partial success
+ set({ isSaving: false });
+ await get().loadPaths(); // reload to avoid state drift
+ set({ statusMessage: msg }); // restore the error message overwritten by loadPaths
+ return { kind: 'partial', message: msg };
+ } else {
+ set({ isSaving: false, statusMessage: msg });
+ return { kind: 'failure', message: msg };
+ }
+ }
+ },
+
+ initialize: async () => {
+ try {
+ const isAdmin: boolean = await invoke('check_admin');
+ set({ isAdmin });
+ if (!isAdmin) set({ statusMessage: i18n.t('status.readonly') });
+ } catch {
+ set({ isAdmin: false, statusMessage: i18n.t('status.readonly') });
+ }
+ await get().loadPaths();
+ },
+ };
+});
diff --git a/tests/unit/analyze-dialog.test.tsx b/tests/unit/analyze-dialog.test.tsx
index 0e3f87c..0726343 100644
--- a/tests/unit/analyze-dialog.test.tsx
+++ b/tests/unit/analyze-dialog.test.tsx
@@ -26,9 +26,7 @@ vi.mock('@/i18n', () => ({
describe('AnalyzeDialog', () => {
it('渲染冲突检测和工具清单标签页,不崩溃', () => {
- const { container } = render(
- {}} />,
- );
+ const { container } = render( {}} />);
const text = container.textContent || '';
expect(text).toContain('analyze.conflicts');
expect(text).toContain('analyze.tools');
diff --git a/tests/unit/app-store.test.ts b/tests/unit/app-store.test.ts
index 36bcb92..83b53f6 100644
--- a/tests/unit/app-store.test.ts
+++ b/tests/unit/app-store.test.ts
@@ -7,16 +7,19 @@ vi.mock('@tauri-apps/api/core', () => ({
// Mock i18n
vi.mock('@/i18n', () => ({
- default: { t: vi.fn((key: string, opts?: Record) => {
- if (key === 'status.loaded') return `已加载 ${opts?.sysCount} 条系统 PATH,${opts?.userCount} 条用户 PATH`;
- if (key === 'status.error') return '加载失败';
- if (key === 'status.saving') return '正在保存...';
- if (key === 'status.saved') return '保存成功';
- if (key === 'status.warning_backup') return '备份失败,但保存继续';
- if (key === 'status.readonly') return '只读模式';
- if (key === 'status.deleted') return `已删除 ${opts?.count} 条路径`;
- return key;
- }) },
+ default: {
+ t: vi.fn((key: string, opts?: Record) => {
+ if (key === 'status.loaded')
+ return `已加载 ${opts?.sysCount} 条系统 PATH,${opts?.userCount} 条用户 PATH`;
+ if (key === 'status.error') return '加载失败';
+ if (key === 'status.saving') return '正在保存...';
+ if (key === 'status.saved') return '保存成功';
+ if (key === 'status.warning_backup') return '备份失败,但保存继续';
+ if (key === 'status.readonly') return '只读模式';
+ if (key === 'status.deleted') return `已删除 ${opts?.count} 条路径`;
+ return key;
+ }),
+ },
}));
import type { PathEntry } from '../../src/core/path-entry';
@@ -56,7 +59,7 @@ describe('app-store CRUD', () => {
it('addPath 追加到 sysPaths', () => {
useAppStore.getState().addPath('C:\\test', TargetType.SYSTEM);
const s = useAppStore.getState();
- expect(s.sysPaths.map(e => e.path)).toEqual(['C:\\test']);
+ expect(s.sysPaths.map((e) => e.path)).toEqual(['C:\\test']);
expect(s.isModified).toBe(true);
expect(s.undoRedo.historyLength).toBe(1);
});
@@ -64,7 +67,7 @@ describe('app-store CRUD', () => {
it('addPath 追加到 userPaths', () => {
useAppStore.getState().addPath('D:\\user', TargetType.USER);
const s = useAppStore.getState();
- expect(s.userPaths.map(e => e.path)).toEqual(['D:\\user']);
+ expect(s.userPaths.map((e) => e.path)).toEqual(['D:\\user']);
expect(s.sysPaths).toEqual([]);
});
@@ -72,7 +75,7 @@ describe('app-store CRUD', () => {
const store = useAppStore.getState();
store.addPath('C:\\old', TargetType.SYSTEM);
store.editPath(0, 'C:\\new', TargetType.SYSTEM);
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\new']);
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\new']);
});
it('editPath 越界 index 无崩溃', () => {
@@ -87,7 +90,7 @@ describe('app-store CRUD', () => {
store.addPath('B', TargetType.SYSTEM);
store.addPath('C', TargetType.SYSTEM);
store.deletePaths([1], TargetType.SYSTEM);
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A', 'C']);
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['A', 'C']);
expect(useAppStore.getState().selectedIndices).toEqual([]);
});
@@ -98,7 +101,7 @@ describe('app-store CRUD', () => {
store.addPath('C', TargetType.USER);
store.addPath('D', TargetType.USER);
store.deletePaths([1, 3], TargetType.USER);
- expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['A', 'C']);
+ expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['A', 'C']);
});
it('deletePaths 非连续多选删除后可 undo 恢复到正确位置', () => {
@@ -108,16 +111,16 @@ describe('app-store CRUD', () => {
store.addPath('C', TargetType.SYSTEM);
store.addPath('D', TargetType.SYSTEM);
store.deletePaths([1, 3], TargetType.SYSTEM);
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A', 'C']);
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['A', 'C']);
useAppStore.getState().undo();
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A', 'B', 'C', 'D']);
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['A', 'B', 'C', 'D']);
});
it('moveUp index=0 无操作', () => {
const store = useAppStore.getState();
store.addPath('A', TargetType.SYSTEM);
store.moveUp(0, TargetType.SYSTEM);
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['A']);
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['A']);
});
it('moveUp 正常交换位置', () => {
@@ -125,7 +128,7 @@ describe('app-store CRUD', () => {
store.addPath('A', TargetType.SYSTEM);
store.addPath('B', TargetType.SYSTEM);
store.moveUp(1, TargetType.SYSTEM);
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['B', 'A']);
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['B', 'A']);
expect(useAppStore.getState().selectedIndices).toEqual([0]);
});
@@ -133,7 +136,7 @@ describe('app-store CRUD', () => {
const store = useAppStore.getState();
store.addPath('A', TargetType.USER);
store.moveDown(0, TargetType.USER);
- expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['A']);
+ expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['A']);
});
it('cleanPaths 移除无效路径并返回 removed', () => {
@@ -143,7 +146,7 @@ describe('app-store CRUD', () => {
// is_valid_path_format 拒绝全标点路径
const removed = store.cleanPaths(TargetType.SYSTEM, (p) => !p.includes(':::'));
expect(removed).toEqual([':::invalid:::']);
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\valid']);
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\valid']);
});
it('replacePaths 整体替换列表', () => {
@@ -151,7 +154,7 @@ describe('app-store CRUD', () => {
store.addPath('old1', TargetType.USER);
store.addPath('old2', TargetType.USER);
store.replacePaths(TargetType.USER, ['new1', 'new2', 'new3']);
- expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['new1', 'new2', 'new3']);
+ expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['new1', 'new2', 'new3']);
});
it('clearPaths 清空列表', () => {
@@ -187,7 +190,7 @@ describe('undo/redo', () => {
store.addPath('test', TargetType.SYSTEM);
store.undo();
store.redo();
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['test']);
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['test']);
});
it('undo/redo 正确更新 isModified', () => {
@@ -214,8 +217,8 @@ describe('loadPaths', () => {
mockedInvoke.mockResolvedValueOnce(['D:\\usr1']);
await useAppStore.getState().loadPaths();
const s = useAppStore.getState();
- expect(s.sysPaths.map(e => e.path)).toEqual(['C:\\sys1', 'C:\\sys2']);
- expect(s.userPaths.map(e => e.path)).toEqual(['D:\\usr1']);
+ expect(s.sysPaths.map((e) => e.path)).toEqual(['C:\\sys1', 'C:\\sys2']);
+ expect(s.userPaths.map((e) => e.path)).toEqual(['D:\\usr1']);
expect(s.isLoading).toBe(false);
expect(s.isModified).toBe(false);
});
@@ -249,13 +252,13 @@ describe('savePaths', () => {
it('部分失败时报告具体 hive 并回读', async () => {
mockedInvoke
- .mockResolvedValueOnce(undefined) // backup_registry
- .mockResolvedValueOnce(undefined) // save_system_paths
- .mockRejectedValueOnce('权限不足') // save_user_paths
+ .mockResolvedValueOnce(undefined) // backup_registry
+ .mockResolvedValueOnce(undefined) // save_system_paths
+ .mockRejectedValueOnce('权限不足') // save_user_paths
// 以下为 partial 触发的 loadPaths 调用
- .mockResolvedValueOnce(['A']) // load_system_paths
- .mockResolvedValueOnce(['B']) // load_user_paths
- .mockResolvedValueOnce([[], []]); // load_disabled_state
+ .mockResolvedValueOnce(['A']) // load_system_paths
+ .mockResolvedValueOnce(['B']) // load_user_paths
+ .mockResolvedValueOnce([[], []]); // load_disabled_state
const result = await useAppStore.getState().savePaths();
expect(result.kind).toBe('partial');
@@ -266,7 +269,9 @@ describe('savePaths', () => {
it('isSaving 守卫:并发第二次调用直接返回', async () => {
let resolveAll: (v: unknown) => void;
- const pending = new Promise((r) => { resolveAll = r; });
+ const pending = new Promise((r) => {
+ resolveAll = r;
+ });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mockedInvoke.mockReturnValue(pending as any);
@@ -292,21 +297,21 @@ describe('initialize', () => {
it('管理员模式初始化', async () => {
mockedInvoke
- .mockResolvedValueOnce(true) // check_admin
+ .mockResolvedValueOnce(true) // check_admin
.mockResolvedValueOnce(['S1']) // load_system_paths
.mockResolvedValueOnce(['U1']); // load_user_paths
await useAppStore.getState().initialize();
const s = useAppStore.getState();
expect(s.isAdmin).toBe(true);
- expect(s.sysPaths.map(e => e.path)).toEqual(['S1']);
- expect(s.userPaths.map(e => e.path)).toEqual(['U1']);
+ expect(s.sysPaths.map((e) => e.path)).toEqual(['S1']);
+ expect(s.userPaths.map((e) => e.path)).toEqual(['U1']);
});
it('非管理员初始化进入只读模式', async () => {
mockedInvoke
- .mockResolvedValueOnce(false) // check_admin
- .mockResolvedValueOnce([]) // load_system_paths
- .mockResolvedValueOnce([]); // load_user_paths
+ .mockResolvedValueOnce(false) // check_admin
+ .mockResolvedValueOnce([]) // load_system_paths
+ .mockResolvedValueOnce([]); // load_user_paths
await useAppStore.getState().initialize();
expect(useAppStore.getState().isAdmin).toBe(false);
// statusMessage 被后续 loadPaths 覆盖为加载完成消息,但 isAdmin=false 不变
diff --git a/tests/unit/import-export.test.ts b/tests/unit/import-export.test.ts
index aec495c..0d8a9bd 100644
--- a/tests/unit/import-export.test.ts
+++ b/tests/unit/import-export.test.ts
@@ -26,8 +26,12 @@ describe('exportToJson', () => {
const parsed = JSON.parse(json);
expect(parsed.version).toBe('5.1.0');
expect(parsed.timestamp).toBeDefined();
- expect(parsed.system.map((e: { path: string }) => e.path)).toEqual(sampleData.system.map(e => e.path));
- expect(parsed.user.map((e: { path: string }) => e.path)).toEqual(sampleData.user.map(e => e.path));
+ expect(parsed.system.map((e: { path: string }) => e.path)).toEqual(
+ sampleData.system.map((e) => e.path),
+ );
+ expect(parsed.user.map((e: { path: string }) => e.path)).toEqual(
+ sampleData.user.map((e) => e.path),
+ );
expect(parsed.system[0].enabled).toBe(true);
expect(parsed.user[0].enabled).toBe(true);
});
@@ -36,8 +40,8 @@ describe('exportToJson', () => {
describe('importFromJson', () => {
it('正确导入 JSON', () => {
const json = JSON.stringify({
- system: sampleData.system.map(e => e.path),
- user: sampleData.user.map(e => e.path),
+ system: sampleData.system.map((e) => e.path),
+ user: sampleData.user.map((e) => e.path),
});
const result = importFromJson(json);
expect(result.system).toEqual(sampleData.system);
diff --git a/tests/unit/import-parity.test.ts b/tests/unit/import-parity.test.ts
index 1d26945..393bbc4 100644
--- a/tests/unit/import-parity.test.ts
+++ b/tests/unit/import-parity.test.ts
@@ -5,27 +5,27 @@ describe('导入一致性(TS 端)', () => {
it('JSON 含 system + user', () => {
const json = JSON.stringify({ system: ['C:\\a', 'C:\\b'], user: ['D:\\c'] });
const r = importFromJson(json);
- expect(r.system.map(e => e.path)).toEqual(['C:\\a', 'C:\\b']);
- expect(r.user.map(e => e.path)).toEqual(['D:\\c']);
+ expect(r.system.map((e) => e.path)).toEqual(['C:\\a', 'C:\\b']);
+ expect(r.user.map((e) => e.path)).toEqual(['D:\\c']);
});
it('CSV system/user 分类', () => {
const csv = 'type,path\nsystem,C:\\sys\nuser,D:\\usr\n';
const r = importFromCsv(csv);
- expect(r.system.map(e => e.path)).toEqual(['C:\\sys']);
- expect(r.user.map(e => e.path)).toEqual(['D:\\usr']);
+ expect(r.system.map((e) => e.path)).toEqual(['C:\\sys']);
+ expect(r.user.map((e) => e.path)).toEqual(['D:\\usr']);
});
it('CSV 含 BOM + header', () => {
const csv = 'type,path\nsystem,C:\\x\n';
const r = importFromCsv(csv);
- expect(r.system.map(e => e.path)).toEqual(['C:\\x']);
+ expect(r.system.map((e) => e.path)).toEqual(['C:\\x']);
});
it('TXT 逐行读取,跳过注释', () => {
const txt = '# comment\nC:\\a\n\nD:\\b\n';
const r = importFromTxt(txt);
- expect(r.map(e => e.path)).toEqual(['C:\\a', 'D:\\b']);
+ expect(r.map((e) => e.path)).toEqual(['C:\\a', 'D:\\b']);
});
it('JSON 空数据不崩溃', () => {
diff --git a/tests/unit/merge-preview.test.tsx b/tests/unit/merge-preview.test.tsx
index 596f2eb..e6d854a 100644
--- a/tests/unit/merge-preview.test.tsx
+++ b/tests/unit/merge-preview.test.tsx
@@ -9,9 +9,7 @@ vi.mock('@/store/app-store', () => ({
{ path: 'C:\\Windows', enabled: true },
{ path: 'C:\\Disabled', enabled: false },
],
- userPaths: [
- { path: 'D:\\UserApp', enabled: true },
- ],
+ userPaths: [{ path: 'D:\\UserApp', enabled: true }],
searchQuery: '',
};
return selector(state);
@@ -19,7 +17,7 @@ vi.mock('@/store/app-store', () => ({
}));
vi.mock('@tanstack/react-virtual', () => ({
- useVirtualizer: (options: any) => ({
+ useVirtualizer: (options: Record) => ({
getVirtualItems: () => {
// return an array of objects to mock virtual items
return Array.from({ length: options.count }).map((_, index) => ({
diff --git a/tests/unit/path-manager.test.ts b/tests/unit/path-manager.test.ts
index cd601a7..2303e9f 100644
--- a/tests/unit/path-manager.test.ts
+++ b/tests/unit/path-manager.test.ts
@@ -36,13 +36,19 @@ describe('analyzePaths', () => {
describe('pathClean', () => {
it('移除无效路径', () => {
- const [kept, removed] = pathClean([pe('C:\\Valid'), pe('C:\\Invalid'), pe('D:\\Valid')], validateFn);
- expect(kept.map(e => e.path)).toEqual(['C:\\Valid', 'D:\\Valid']);
- expect(removed.map(e => e.path)).toEqual(['C:\\Invalid']);
+ const [kept, removed] = pathClean(
+ [pe('C:\\Valid'), pe('C:\\Invalid'), pe('D:\\Valid')],
+ validateFn,
+ );
+ expect(kept.map((e) => e.path)).toEqual(['C:\\Valid', 'D:\\Valid']);
+ expect(removed.map((e) => e.path)).toEqual(['C:\\Invalid']);
});
it('移除重复路径保留第一个', () => {
- const [kept, removed] = pathClean([pe('C:\\Valid'), pe('C:\\Valid'), pe('D:\\Valid')], alwaysValid);
+ const [kept, removed] = pathClean(
+ [pe('C:\\Valid'), pe('C:\\Valid'), pe('D:\\Valid')],
+ alwaysValid,
+ );
expect(kept.length).toBe(2);
expect(removed.length).toBe(1);
});
@@ -56,7 +62,7 @@ describe('pathClean', () => {
it('全部有效无变化', () => {
const [kept, removed] = pathClean([pe('C:\\a'), pe('D:\\b')], alwaysValid);
- expect(kept.map(e => e.path)).toEqual(['C:\\a', 'D:\\b']);
+ expect(kept.map((e) => e.path)).toEqual(['C:\\a', 'D:\\b']);
expect(removed.length).toBe(0);
});
diff --git a/tests/unit/undo-redo.test.ts b/tests/unit/undo-redo.test.ts
index 6d3848e..444308a 100644
--- a/tests/unit/undo-redo.test.ts
+++ b/tests/unit/undo-redo.test.ts
@@ -1,12 +1,24 @@
import { describe, it, expect, beforeEach } from 'vitest';
-import { UndoRedoManager, OperationType, TargetType, type OpRecord } from '../../src/core/undo-redo';
+import {
+ UndoRedoManager,
+ OperationType,
+ TargetType,
+ type OpRecord,
+} from '../../src/core/undo-redo';
import type { PathEntry } from '../../src/core/path-entry';
function pe(s: string, enabled: boolean = true): PathEntry {
return { path: s, enabled };
}
-function makeRecord(type: OperationType, target: TargetType, index: number, count: number, oldPaths: PathEntry[], newPaths: PathEntry[]): OpRecord {
+function makeRecord(
+ type: OperationType,
+ target: TargetType,
+ index: number,
+ count: number,
+ oldPaths: PathEntry[],
+ newPaths: PathEntry[],
+): OpRecord {
return { type, target, index, count, oldPaths, newPaths };
}
@@ -31,10 +43,10 @@ describe('UndoRedoManager', () => {
mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 2, 1, [], [pe('C:\\NewPath')]));
const u = mgr.undo(sys, user)!;
- expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
+ expect(u[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
const r = mgr.redo(...u)!;
- expect(r[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files', 'C:\\NewPath']);
+ expect(r[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files', 'C:\\NewPath']);
});
it('DELETE 撤销/重做', () => {
@@ -46,11 +58,20 @@ describe('UndoRedoManager', () => {
expect(u[0][0].path).toBe(removed.path);
const r = mgr.redo(...u)!;
- expect(r[0].map(e => e.path)).toEqual(['C:\\Program Files']);
+ expect(r[0].map((e) => e.path)).toEqual(['C:\\Program Files']);
});
it('EDIT 撤销/重做', () => {
- mgr.push(makeRecord(OperationType.EDIT, TargetType.SYSTEM, 0, 1, [pe('C:\\Windows')], [pe('C:\\Edited')]));
+ mgr.push(
+ makeRecord(
+ OperationType.EDIT,
+ TargetType.SYSTEM,
+ 0,
+ 1,
+ [pe('C:\\Windows')],
+ [pe('C:\\Edited')],
+ ),
+ );
sys[0] = pe('C:\\Edited');
const u = mgr.undo(sys, user)!;
@@ -65,10 +86,10 @@ describe('UndoRedoManager', () => {
[sys[0], sys[1]] = [sys[1], sys[0]];
const u = mgr.undo(sys, user)!;
- expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
+ expect(u[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
const r = mgr.redo(...u)!;
- expect(r[0].map(e => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']);
+ expect(r[0].map((e) => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']);
});
it('MOVE_DOWN 撤销/重做', () => {
@@ -76,7 +97,7 @@ describe('UndoRedoManager', () => {
[sys[0], sys[1]] = [sys[1], sys[0]];
const u = mgr.undo(sys, user)!;
- expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
+ expect(u[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
});
it('CLEAN 撤销/重做', () => {
@@ -133,12 +154,12 @@ describe('UndoRedoManager', () => {
it('超出栈底/栈顶的安全处理', () => {
mgr.push(makeRecord(OperationType.ADD, TargetType.SYSTEM, 2, 1, [], [pe('C:\\NewPath')]));
sys.push(pe('C:\\NewPath'));
-
+
// undo一次
mgr.undo(sys, user);
// 再次undo,此时应到达底部返回null
expect(mgr.undo(sys, user)).toBeNull();
-
+
// redo一次
mgr.redo(sys, user);
// 再次redo,应到达顶部返回null
@@ -160,9 +181,12 @@ describe('UndoRedoManager', () => {
// 删除 indices [1, 3](C:\Program Files 和 C:\Extra2)
const removed = [sys[1], sys[3]];
mgr.push({
- type: OperationType.DELETE, target: TargetType.SYSTEM,
- index: 1, count: 2,
- oldPaths: removed, newPaths: [],
+ type: OperationType.DELETE,
+ target: TargetType.SYSTEM,
+ index: 1,
+ count: 2,
+ oldPaths: removed,
+ newPaths: [],
indices: [1, 3],
});
sys.splice(3, 1);
@@ -172,21 +196,29 @@ describe('UndoRedoManager', () => {
expect(u[0]).toEqual(old);
const r = mgr.redo(...u)!;
- expect(r[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Extra1']);
+ expect(r[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Extra1']);
});
it('操作 USER 路径', () => {
user.push(pe('C:\\NewUserPath'));
mgr.push(makeRecord(OperationType.ADD, TargetType.USER, 1, 1, [], [pe('C:\\NewUserPath')]));
const u = mgr.undo(sys, user)!;
- expect(u[1].map(e => e.path)).toEqual(['C:\\Users\\me\\AppData']);
- expect(u[0].map(e => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
+ expect(u[1].map((e) => e.path)).toEqual(['C:\\Users\\me\\AppData']);
+ expect(u[0].map((e) => e.path)).toEqual(['C:\\Windows', 'C:\\Program Files']);
});
it('TOGGLE 撤销/重做', () => {
sys[0] = pe('C:\\Windows', false);
- mgr.push(makeRecord(OperationType.TOGGLE, TargetType.SYSTEM, 0, 1,
- [pe('C:\\Windows', true)], [pe('C:\\Windows', false)]));
+ mgr.push(
+ makeRecord(
+ OperationType.TOGGLE,
+ TargetType.SYSTEM,
+ 0,
+ 1,
+ [pe('C:\\Windows', true)],
+ [pe('C:\\Windows', false)],
+ ),
+ );
const u = mgr.undo(sys, user)!;
expect(u[0][0].enabled).toBe(true);
@@ -204,9 +236,12 @@ describe('UndoRedoManager', () => {
mgr.push({
type: OperationType.IMPORT_BOTH,
target: TargetType.SYSTEM,
- index: 0, count: 0,
- oldPaths: oldSys, newPaths: newSys,
- oldPathsOther: oldUser, newPathsOther: newUser,
+ index: 0,
+ count: 0,
+ oldPaths: oldSys,
+ newPaths: newSys,
+ oldPathsOther: oldUser,
+ newPathsOther: newUser,
});
sys = newSys;
user = newUser;
diff --git a/tests/unit/use-app-actions.test.tsx b/tests/unit/use-app-actions.test.tsx
index a779123..a224ce8 100644
--- a/tests/unit/use-app-actions.test.tsx
+++ b/tests/unit/use-app-actions.test.tsx
@@ -14,11 +14,13 @@ vi.mock('@tauri-apps/plugin-dialog', () => ({
}));
vi.mock('@/i18n', () => ({
- default: { t: vi.fn((key: string, opts?: Record) => {
- if (key === 'status.deleted') return `已删除 ${opts?.count} 条`;
- if (key === 'status.saveWarningLongPaths') return 'PATH 长度超限';
- return key;
- }) },
+ default: {
+ t: vi.fn((key: string, opts?: Record) => {
+ if (key === 'status.deleted') return `已删除 ${opts?.count} 条`;
+ if (key === 'status.saveWarningLongPaths') return 'PATH 长度超限';
+ return key;
+ }),
+ },
}));
vi.mock('@/hooks/use-keyboard', () => ({
@@ -83,7 +85,9 @@ describe('useAppActions', () => {
it('handleNew 打开新建弹窗', async () => {
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleNew(); });
+ act(() => {
+ result.current.handleNew();
+ });
expect(dialogs.setNewDialog).toHaveBeenCalledWith(true);
});
@@ -93,9 +97,14 @@ describe('useAppActions', () => {
useAppStore.setState({ selectedIndices: [0] });
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleEdit(); });
+ act(() => {
+ result.current.handleEdit();
+ });
expect(dialogs.setEditDialog).toHaveBeenCalledWith({
- open: true, index: 0, value: 'C:\\Windows', target: TargetType.SYSTEM,
+ open: true,
+ index: 0,
+ value: 'C:\\Windows',
+ target: TargetType.SYSTEM,
});
});
@@ -103,7 +112,9 @@ describe('useAppActions', () => {
useAppStore.setState({ selectedIndices: [] });
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleEdit(); });
+ act(() => {
+ result.current.handleEdit();
+ });
expect(dialogs.setEditDialog).not.toHaveBeenCalled();
});
@@ -113,15 +124,19 @@ describe('useAppActions', () => {
useAppStore.setState({ selectedIndices: [0] });
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleDelete(); });
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\Program Files']);
+ act(() => {
+ result.current.handleDelete();
+ });
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\Program Files']);
});
it('handleDelete 无选中项不操作', async () => {
useAppStore.setState({ selectedIndices: [] });
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleDelete(); });
+ act(() => {
+ result.current.handleDelete();
+ });
expect(useAppStore.getState().sysPaths.length).toBe(2);
});
@@ -131,16 +146,26 @@ describe('useAppActions', () => {
useAppStore.setState({ selectedIndices: [1] });
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleMoveUp(); });
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']);
+ act(() => {
+ result.current.handleMoveUp();
+ });
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual([
+ 'C:\\Program Files',
+ 'C:\\Windows',
+ ]);
});
it('handleMoveDown 下移选中项', async () => {
useAppStore.setState({ selectedIndices: [0] });
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleMoveDown(); });
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\Program Files', 'C:\\Windows']);
+ act(() => {
+ result.current.handleMoveDown();
+ });
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual([
+ 'C:\\Program Files',
+ 'C:\\Windows',
+ ]);
});
// ── handleClean ──
@@ -149,8 +174,10 @@ describe('useAppActions', () => {
resetStore([pe('C:\\Windows'), pe('invalid_path!@#')]);
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleClean(); });
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\Windows']);
+ act(() => {
+ result.current.handleClean();
+ });
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\Windows']);
expect(useAppStore.getState().statusMessage).toContain('已删除 1 条');
});
@@ -159,15 +186,19 @@ describe('useAppActions', () => {
it('handleNewConfirm 添加新路径', async () => {
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleNewConfirm('C:\\New'); });
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toContain('C:\\New');
+ act(() => {
+ result.current.handleNewConfirm('C:\\New');
+ });
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toContain('C:\\New');
expect(dialogs.setNewDialog).toHaveBeenCalledWith(false);
});
it('handleNewConfirm 空白不添加', async () => {
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleNewConfirm(' '); });
+ act(() => {
+ result.current.handleNewConfirm(' ');
+ });
expect(useAppStore.getState().sysPaths.length).toBe(2);
});
@@ -177,7 +208,9 @@ describe('useAppActions', () => {
dialogs.editDialog = { open: true, index: 0, value: 'C:\\Windows', target: TargetType.SYSTEM };
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleEditConfirm('C:\\Edited'); });
+ act(() => {
+ result.current.handleEditConfirm('C:\\Edited');
+ });
expect(useAppStore.getState().sysPaths[0].path).toBe('C:\\Edited');
});
@@ -189,19 +222,27 @@ describe('useAppActions', () => {
dialogs.importDialog = { open: true, system: sysImport, user: usrImport };
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleImportSelect('both'); });
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\ImportSys']);
- expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['D:\\ImportUsr']);
+ act(() => {
+ result.current.handleImportSelect('both');
+ });
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\ImportSys']);
+ expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['D:\\ImportUsr']);
expect(dialogs.setImportDialog).toHaveBeenCalledWith({ open: false, system: [], user: [] });
});
it('handleImportSelect system 模式只替换 system', async () => {
- dialogs.importDialog = { open: true, system: [pe('C:\\ImportSys')], user: [pe('D:\\ImportUsr')] };
+ dialogs.importDialog = {
+ open: true,
+ system: [pe('C:\\ImportSys')],
+ user: [pe('D:\\ImportUsr')],
+ };
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- act(() => { result.current.handleImportSelect('system'); });
- expect(useAppStore.getState().sysPaths.map(e => e.path)).toEqual(['C:\\ImportSys']);
- expect(useAppStore.getState().userPaths.map(e => e.path)).toEqual(['D:\\User']); // 未变
+ act(() => {
+ result.current.handleImportSelect('system');
+ });
+ expect(useAppStore.getState().sysPaths.map((e) => e.path)).toEqual(['C:\\ImportSys']);
+ expect(useAppStore.getState().userPaths.map((e) => e.path)).toEqual(['D:\\User']); // 未变
});
// ── handleSave ──
@@ -211,7 +252,9 @@ describe('useAppActions', () => {
vi.spyOn(useAppStore.getState(), 'savePaths').mockResolvedValue({ kind: 'success' });
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- await act(async () => { await result.current.handleSave(); });
+ await act(async () => {
+ await result.current.handleSave();
+ });
// savePaths is called
expect(useAppStore.getState().savePaths).toHaveBeenCalled();
});
@@ -227,7 +270,9 @@ describe('useAppActions', () => {
});
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- await act(async () => { await result.current.handleSave(); });
+ await act(async () => {
+ await result.current.handleSave();
+ });
expect(callCount).toBe(2);
expect(mockAsk).toHaveBeenCalled();
});
@@ -240,7 +285,9 @@ describe('useAppActions', () => {
});
const { useAppActions } = await import('@/hooks/use-app-actions');
const { result } = renderHook(() => useAppActions('system', dialogs));
- await act(async () => { await result.current.handleSave(); });
+ await act(async () => {
+ await result.current.handleSave();
+ });
expect(callCount).toBe(1); // 仅调用一次,不重试
expect(mockAsk).not.toHaveBeenCalled();
});
diff --git a/vitest.config.ts b/vitest.config.ts
index 2c3e84f..c03aceb 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -12,8 +12,9 @@ export default defineConfig({
exclude: ['e2e/**', 'node_modules/**', 'gui/**'],
coverage: {
provider: 'v8',
- reporter: ['text', 'lcov'],
+ reporter: ['text', 'lcov', 'cobertura'],
include: ['src/core/**', 'src/store/**', 'src/hooks/**'],
+ exclude: ['src/main.tsx', 'src/vite-env.d.ts'],
thresholds: {
lines: 80,
},