diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 30b7aa948..27de2822c 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -5,7 +5,7 @@
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "24.21.0",
- "pnpmVersion": "12.10.1"
+ "pnpmVersion": "12.11.2"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "1.99.0",
diff --git a/.github/workflows/frontend-check.yml b/.github/workflows/frontend-check.yml
index 8cf1fdae4..937f9c89b 100644
--- a/.github/workflows/frontend-check.yml
+++ b/.github/workflows/frontend-check.yml
@@ -6,6 +6,7 @@ on:
permissions:
contents: read
+ pull-requests: read
env:
HUSKY: 0
@@ -99,14 +100,7 @@ jobs:
id: check_inputs
uses: dorny/paths-filter@v4
with:
- filters: |
- contract:
- - 'src/services/contract.ts'
- - 'scripts/gen-contract.mjs'
- - 'src-tauri/src/core/notify/**'
- - 'src-tauri/src/bin/**'
- - 'src-tauri/src/lib.rs'
- - '.github/workflows/frontend-check.yml'
+ filters: scripts/contract-inputs.json
- name: Skip if contract inputs unchanged
if: steps.check_inputs.outputs.contract != 'true'
@@ -159,8 +153,6 @@ jobs:
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
done
- - name: Regenerate contract and require empty diff
+ - name: Check generated contract without changing the checkout
if: steps.check_inputs.outputs.contract == 'true'
- run: |
- pnpm gen:contract
- git diff --exit-code -- src/services/contract.ts
+ run: pnpm gen:contract --check
diff --git a/.husky/pre-push b/.husky/pre-push
index 9396b29ff..c3b725ee9 100644
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -5,4 +5,5 @@ if ! command -v "cargo-make" >/dev/null 2>&1; then
cargo install --force cargo-make
fi
+node scripts/check-contract-changes.mjs --push "$1" || exit $?
cargo make pre-push
diff --git a/.tool-versions b/.tool-versions
index df95322ab..25be9ef6d 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,3 +1,3 @@
nodejs 24.21.0
-pnpm 12.10.1
+pnpm 12.11.2
rust 1.99.0
diff --git a/Cargo.lock b/Cargo.lock
index 962e3df28..32c453c88 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "Inflector"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
+
[[package]]
name = "adler2"
version = "2.0.1"
@@ -1135,6 +1141,8 @@ dependencies = [
"sha2 0.11.0",
"smartstring",
"socket2",
+ "specta",
+ "specta-typescript",
"sysproxy",
"tauri",
"tauri-build",
@@ -1155,6 +1163,7 @@ dependencies = [
"tauri-plugin-updater",
"tauri-plugin-wdio-webdriver",
"tauri-plugin-window-state",
+ "tauri-specta",
"tokio",
"tokio-rustls",
"tokio-stream",
@@ -1166,7 +1175,7 @@ dependencies = [
"windows-service",
"windows-sys 0.61.2",
"winreg 0.56.0",
- "zip 8.6.0",
+ "zip 9.0.1",
]
[[package]]
@@ -1213,6 +1222,7 @@ dependencies = [
"rust_iso3166",
"serde",
"serde_json",
+ "specta",
"tokio",
]
@@ -4038,9 +4048,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lzma-rust2"
-version = "0.16.5"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca93e534d1142d1d0dcca6d25fe302508a5dfb40b302802904577725ea0b695b"
+checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b"
dependencies = [
"sha2 0.11.0",
]
@@ -6804,6 +6814,58 @@ dependencies = [
"system-deps 6.2.2",
]
+[[package]]
+name = "specta"
+version = "2.0.0-rc.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
+dependencies = [
+ "paste",
+ "rustc_version 0.4.1",
+ "serde_json",
+ "specta-macros",
+]
+
+[[package]]
+name = "specta-macros"
+version = "2.0.0-rc.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
+dependencies = [
+ "Inflector",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "specta-serde"
+version = "0.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
+dependencies = [
+ "specta",
+ "specta-macros",
+]
+
+[[package]]
+name = "specta-typescript"
+version = "0.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
+dependencies = [
+ "specta",
+]
+
+[[package]]
+name = "specta-util"
+version = "0.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
+dependencies = [
+ "specta",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -7159,6 +7221,7 @@ dependencies = [
"serde_json",
"serde_repr",
"serialize-to-javascript",
+ "specta",
"swift-rs",
"tauri-build",
"tauri-macros",
@@ -7276,6 +7339,7 @@ dependencies = [
"libc",
"parking_lot",
"serde",
+ "specta",
"sysinfo",
"tauri",
"tauri-plugin-clipboard-manager",
@@ -7655,6 +7719,37 @@ dependencies = [
"wry",
]
+[[package]]
+name = "tauri-specta"
+version = "2.0.0-rc.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
+dependencies = [
+ "heck 0.5.0",
+ "serde",
+ "serde_json",
+ "specta",
+ "specta-serde",
+ "specta-typescript",
+ "specta-util",
+ "tauri",
+ "tauri-specta-macros",
+ "thiserror 2.0.20",
+]
+
+[[package]]
+name = "tauri-specta-macros"
+version = "2.0.0-rc.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
+dependencies = [
+ "darling",
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "tauri-utils"
version = "2.10.1"
@@ -10055,9 +10150,9 @@ dependencies = [
[[package]]
name = "zip"
-version = "8.6.0"
+version = "9.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
+checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692"
dependencies = [
"aes",
"bzip2",
@@ -10070,6 +10165,7 @@ dependencies = [
"indexmap 2.14.0",
"lzma-rust2",
"memchr",
+ "nix",
"pbkdf2",
"ppmd-rust",
"sha1 0.11.0",
diff --git a/Changelog.md b/Changelog.md
index c90fa75c4..a0ce9a6bb 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -3,6 +3,8 @@
🐞 修复问题
+- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
+- 修复「首页」网站测速失败后卡片一直显示测试中的问题
diff --git a/crates/clash-verge-media-unlock/Cargo.toml b/crates/clash-verge-media-unlock/Cargo.toml
index eec2c2e0c..2a1395b4e 100644
--- a/crates/clash-verge-media-unlock/Cargo.toml
+++ b/crates/clash-verge-media-unlock/Cargo.toml
@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
+specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
clash-verge-logging = { workspace = true }
chrono = { workspace = true }
log = { workspace = true }
diff --git a/crates/clash-verge-media-unlock/src/types.rs b/crates/clash-verge-media-unlock/src/types.rs
index f643cc3b9..58b41374c 100644
--- a/crates/clash-verge-media-unlock/src/types.rs
+++ b/crates/clash-verge-media-unlock/src/types.rs
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use super::utils::{country_code_to_emoji, get_local_date_string};
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
pub struct UnlockItem {
pub name: String,
pub status: String,
diff --git a/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml b/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml
index 1536d7096..aa5f1ba02 100644
--- a/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml
+++ b/crates/tauri-plugin-clash-verge-sysinfo/Cargo.toml
@@ -5,6 +5,7 @@ edition = "2024"
rust-version = "1.91"
[dependencies]
+specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true }
diff --git a/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs b/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs
index 0c2a8f56c..bb3fa3adb 100644
--- a/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs
+++ b/crates/tauri-plugin-clash-verge-sysinfo/src/commands.rs
@@ -1,53 +1,46 @@
use parking_lot::RwLock;
use serde::Serialize;
use tauri::{AppHandle, Runtime, State, command};
-use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error};
+use tauri_plugin_clipboard_manager::ClipboardExt as _;
use crate::Platform;
-#[derive(Serialize)]
+#[derive(Serialize, specta::Type)]
pub struct SystemInfo {
pub system_name: String,
pub system_version: String,
- pub system_kernel_version: String,
- pub system_arch: String,
- pub app_version: String,
- pub app_core_mode: String,
- pub app_is_admin: bool,
}
-impl From for SystemInfo {
- fn from(platform: Platform) -> Self {
+impl From<&Platform> for SystemInfo {
+ fn from(platform: &Platform) -> Self {
Self {
- system_name: platform.sysinfo.system_name,
- system_version: platform.sysinfo.system_version,
- system_kernel_version: platform.sysinfo.system_kernel_version,
- system_arch: platform.sysinfo.system_arch,
- app_version: platform.appinfo.app_version,
- app_core_mode: platform.appinfo.app_core_mode,
- app_is_admin: platform.appinfo.app_is_admin,
+ system_name: platform.sysinfo.system_name.clone(),
+ system_version: platform.sysinfo.system_version.clone(),
}
}
}
#[command]
-pub fn get_system_info(state: State<'_, RwLock>) -> Result {
+#[specta::specta]
+pub fn get_system_info(state: State<'_, RwLock>) -> Result {
let platform = state.inner().read();
- Ok(SystemInfo::from(platform.clone()))
+ Ok(SystemInfo::from(&*platform))
}
/// 获取应用的运行时间(毫秒)
#[command]
-pub fn get_app_uptime(state: State<'_, RwLock>) -> Result {
+#[specta::specta]
+pub fn get_app_uptime(state: State<'_, RwLock>) -> Result {
Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis())
}
#[command]
+#[specta::specta]
pub fn export_diagnostic_info(
app_handle: AppHandle,
state: State<'_, RwLock>,
-) -> Result<(), Error> {
+) -> Result<(), String> {
let info = state.inner().read().to_string();
let clipboard = app_handle.clipboard();
- clipboard.write_text(info)
+ clipboard.write_text(info).map_err(|error| error.to_string())
}
diff --git a/eslint.config.ts b/eslint.config.ts
index df7bd8deb..1e1840416 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -12,6 +12,92 @@ import pluginUnusedImports from 'eslint-plugin-unused-imports'
import globals from 'globals'
import tseslint from 'typescript-eslint'
+const ipcBoundaryRules = ({
+ allowCommands = false,
+ allowInvoke = false,
+ allowEvents = false,
+} = {}) => {
+ const paths = [
+ ...(!allowInvoke
+ ? [
+ {
+ name: '@tauri-apps/api/core',
+ importNames: ['invoke'],
+ allowTypeImports: true,
+ message: 'Invoke backend commands through services/cmds.',
+ },
+ ]
+ : []),
+ ...(!allowEvents
+ ? [
+ {
+ name: '@tauri-apps/api/event',
+ importNames: ['listen', 'emit', 'emitTo', 'once'],
+ allowTypeImports: true,
+ message:
+ 'The event bus owns Tauri event subscriptions and emissions.',
+ },
+ ]
+ : []),
+ ]
+ const patterns = allowCommands
+ ? []
+ : [
+ {
+ regex: '(^|/)bindings(\\.ts)?$',
+ importNames: ['commands'],
+ allowTypeImports: true,
+ message:
+ 'Only services/cmds may import generated command implementations.',
+ },
+ ]
+ const modules = [
+ ...(!allowInvoke ? ['@tauri-apps/api/core'] : []),
+ ...(!allowEvents ? ['@tauri-apps/api/event'] : []),
+ ]
+ const syntax = modules.flatMap((name) => [
+ {
+ selector: `ImportExpression[source.value='${name}']`,
+ message:
+ 'Use the owned IPC service boundary instead of dynamically importing Tauri APIs.',
+ },
+ {
+ selector: `CallExpression[callee.name='require'][arguments.0.value='${name}']`,
+ message:
+ 'Use the owned IPC service boundary instead of requiring Tauri APIs.',
+ },
+ {
+ selector: `TSImportEqualsDeclaration[moduleReference.expression.value='${name}']`,
+ message: 'Use the owned IPC service boundary for Tauri APIs.',
+ },
+ ])
+ if (!allowCommands) {
+ syntax.push(
+ {
+ selector: 'ImportExpression[source.value=/bindings(\\.ts)?$/]',
+ message:
+ 'Only services/cmds may load generated command implementations.',
+ },
+ {
+ selector:
+ "CallExpression[callee.name='require'][arguments.0.value=/bindings(\\.ts)?$/]",
+ message:
+ 'Only services/cmds may load generated command implementations.',
+ },
+ {
+ selector:
+ 'TSImportEqualsDeclaration[moduleReference.expression.value=/bindings(\\.ts)?$/]',
+ message:
+ 'Only services/cmds may load generated command implementations.',
+ },
+ )
+ }
+ return {
+ 'no-restricted-imports': ['error', { paths, patterns }],
+ 'no-restricted-syntax': ['error', ...syntax],
+ }
+}
+
export default defineConfig([
pluginESx.configs['flat/restrict-to-es2022'],
{
@@ -162,4 +248,33 @@ export default defineConfig([
},
},
},
+ {
+ files: ['src/**/*.{ts,tsx,js,jsx,mts,mjs}'],
+ rules: ipcBoundaryRules(),
+ },
+ {
+ files: ['src/services/cmds.ts'],
+ rules: ipcBoundaryRules({ allowCommands: true }),
+ },
+ {
+ files: ['src/services/bindings.ts'],
+ rules: ipcBoundaryRules({ allowCommands: true, allowInvoke: true }),
+ },
+ {
+ files: ['src/services/bus/**/*.{ts,tsx}'],
+ rules: ipcBoundaryRules({ allowEvents: true }),
+ },
+ {
+ files: [
+ 'src/components/proxy/proxy-chain.tsx',
+ 'src/components/proxy/proxy-groups-chain.tsx',
+ 'src/pages/proxies.tsx',
+ ],
+ rules: {
+ '@typescript-eslint/no-floating-promises': [
+ 'error',
+ { ignoreVoid: false },
+ ],
+ },
+ },
])
diff --git a/knip.json b/knip.json
index 9be0c22de..6ea552114 100644
--- a/knip.json
+++ b/knip.json
@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
- "entry": ["src/polyfills/*.js!"],
+ "entry": ["src/polyfills/*.js!", "scripts/check-contract-changes.mjs!"],
"project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"],
"include": [
"files",
@@ -18,7 +18,7 @@
"ignoreDependencies": ["lint-staged"],
"ignoreIssues": {
"src/types/generated/**": ["exports", "types"],
- "src/services/contract.ts": ["exports", "types"]
+ "src/services/bindings.ts": ["types"]
},
"cycles": {
"dynamicImports": true
diff --git a/package.json b/package.json
index 7f9ebe593..9ef745150 100644
--- a/package.json
+++ b/package.json
@@ -146,5 +146,5 @@
]
},
"type": "module",
- "packageManager": "pnpm@12.10.1"
+ "packageManager": "pnpm@12.11.2"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 448ca2839..ceac328bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,153 +7,153 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
- specifier: 12.10.1
- version: 12.10.1
+ specifier: 12.11.2
+ version: 12.11.2
packages:
- '@pnpm/exe.android-arm64@12.10.1':
- resolution: {integrity: sha512-CVTlHPwNvadQCJD9bGSMzWIhFX8QpjtqC2c27dhdnn/9iKzEjZ3qL62o3gHpKbIuo6qn4WxTKRoaokgNdReBig==}
+ '@pnpm/exe.android-arm64@12.11.2':
+ resolution: {integrity: sha512-owLw13OfPkEoY+hLBNeeG+dCFhywr7q2d/EHPz4mNL2RhKnv8gNwMARkjkiDQu234aItVwFNDeGnmQXm+ikkAg==}
cpu: [arm64]
os: [android]
- '@pnpm/exe.android-x64@12.10.1':
- resolution: {integrity: sha512-NFiYLIaodY7da0XIebwcU3JoP7IK/lzF7zgKSSP+9H+QcFnQ1GDfBkuBKCYJzZ+Xu0WH54neIPLBBCaOjaaauA==}
+ '@pnpm/exe.android-x64@12.11.2':
+ resolution: {integrity: sha512-GYPkVebl4iokOA/4MPSpGoUVfFlsth7uqOCe1XiaiM23LAdvUOMJBObqHJijr89ZqjwHm9pO6vO45GRwJ2U2qA==}
cpu: [x64]
os: [android]
- '@pnpm/exe.darwin-arm64@12.10.1':
- resolution: {integrity: sha512-0O8tdQ8ijlPJudXspLMTcitMkdBEz6uUflazL5uQE0WMs85oQIp0U+5hfEktMFB64QqEhjqlvvujTwbzdr9oMg==}
+ '@pnpm/exe.darwin-arm64@12.11.2':
+ resolution: {integrity: sha512-n9M4On66y8FDxUAKgMO1FXt0qihyG4e+56p3/F0uo8Bx9KplW8BwKVnXigzCJ8aNM6sVVPkOE6M9XXbeVIxnZA==}
cpu: [arm64]
os: [darwin]
- '@pnpm/exe.darwin-x64@12.10.1':
- resolution: {integrity: sha512-ud+1/uZOKEo8P/AsPynOJc/LNwfI0j6WwCElibj71rU9l4nIBnsSn+wWTIPZPkZ4BBSH5+82azSnAARMyJ1lNA==}
+ '@pnpm/exe.darwin-x64@12.11.2':
+ resolution: {integrity: sha512-oSW7n3sBT0c0cyk4kj10T2Y6Mpm+OFEiCAWozgX039tdoI6yqJF1uaFD3sw7ugLdu6VV3+uY0pa1TUZIPdxDWA==}
cpu: [x64]
os: [darwin]
- '@pnpm/exe.freebsd-x64@12.10.1':
- resolution: {integrity: sha512-mj9PE5rf8uAfQQ2K66HSATMSut+lNiB4eSqFd5+azFVCMnxNSy/32xpZQJT21PhnN3Y4qbK+iKHe5nWAxSuRhw==}
+ '@pnpm/exe.freebsd-x64@12.11.2':
+ resolution: {integrity: sha512-qbzc3KoDpa/Lkf8vs3jnDe6dUUbso+6eLIyssYFMiliLpYNxn8dirk11YoNnOQcdHVB50v5RFls1Yid6ttT5pg==}
cpu: [x64]
os: [freebsd]
- '@pnpm/exe.linux-arm64-musl@12.10.1':
- resolution: {integrity: sha512-xba+00wZJ8Um6rBJJjWl88uJj0tydB8dtomvjlCLvi0uL32vpa9CRP8Vv26FIXvbiZ1bo2RCzYt/eChxJVV52w==}
+ '@pnpm/exe.linux-arm64-musl@12.11.2':
+ resolution: {integrity: sha512-uMEYDwKRktGwANubQ+kYCwmhNwg3Ik6qK89ahsbQ5cP30R/5FVJb/T99vr9XRUBC/A7mZI6eeT92oyVas9ACyg==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@pnpm/exe.linux-arm64@12.10.1':
- resolution: {integrity: sha512-oBdMp69ScC9UnniG4qZAQVr2fBDSXjm4yk/72vkEVyrlwS23yMxcRQl7Qdo3sgXPAygY6TWnPRKm4rXzjoyW7w==}
+ '@pnpm/exe.linux-arm64@12.11.2':
+ resolution: {integrity: sha512-6e8ZAQUwFXAWHHHv5FesWqf/kEK04wuitBv5f/gxzkC0DiBfaixr+f+6JQede8SF4Ji5LcMwVFJbzAtffpWxEg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@pnpm/exe.linux-ppc64@12.10.1':
- resolution: {integrity: sha512-Yyb3wu09h3c7mLme270uHFiVaAIUHC411Ru9LIU2BKUIstrGlXa9OqqVWoXaRgYRdFdOFknkwG7GNn6ygwLbnw==}
+ '@pnpm/exe.linux-ppc64@12.11.2':
+ resolution: {integrity: sha512-Ui8A4nepkVupx2VpuFfysEluMvWK6gTB5rgVts7Ss/+8dj0RD28x3F/seNeH/zSypgC6yKdV6qsKVQh/uffTuQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@pnpm/exe.linux-riscv64@12.10.1':
- resolution: {integrity: sha512-PdkeUFFkybknnGkWP0H22eMpIiS4OZXL5BcQzF4lh+vmrW3IaC2HOkbANKgot/PKO0Kl77pzA/6PKFxgbgfXIQ==}
+ '@pnpm/exe.linux-riscv64@12.11.2':
+ resolution: {integrity: sha512-i1yLdQ5bu+wjQ+srtFoJAaY2gN/+oXHFdjul4llCeoDxHE0rTIG3R41NhHfCjyFPW4+CpbBT58933CL9sMOCnA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@pnpm/exe.linux-s390x@12.10.1':
- resolution: {integrity: sha512-1okKuHKJwJPAbofDxfhZWAUxEWtee4ZSf7Pah8kcvocaJkf2WxFJ5KVTiNIvDml5GLidDQsgf7Pcf/WHbCak7Q==}
+ '@pnpm/exe.linux-s390x@12.11.2':
+ resolution: {integrity: sha512-YS1/lhPjjTsN4QL50VaFZKRa3Uc/bDnhE8eKbJMFvuX06UuFS5YaiDg04APoZdfVSGqTfX27QAg7rfLMSWaqxA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@pnpm/exe.linux-x64-musl@12.10.1':
- resolution: {integrity: sha512-e6IqzVNkPa7srFKJKnY23uWJ+qz0LaRWiTUo/sHfFXRRMRzB5uNOr6qWWip/jqhdGwU91n9J7LrEYZ6e0tRczw==}
+ '@pnpm/exe.linux-x64-musl@12.11.2':
+ resolution: {integrity: sha512-7eIxQR05fvJxCujUSKp5KTtehJ7QqrgqP/HD1+BcB/SaV2Be4XI1PEHiOgA1zxsiMIeMvTF6E4G4dYHrFjeXRg==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@pnpm/exe.linux-x64@12.10.1':
- resolution: {integrity: sha512-ZByV0DRgWzGaDSjT5ddRBASdwz+r/qPMYh7o6RGmPQjtRGNFbSl94xY2DcAM8mqCOZJUUDtAXQ+wpBXcdpKlkg==}
+ '@pnpm/exe.linux-x64@12.11.2':
+ resolution: {integrity: sha512-FK7QKe24AYgppJwO3mpooYptduuvIGqKJPi5ac7MdnWkjUy46JBfYhCW0grnkWKC8YOf0x+vGviuQqJEj3SpQQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@pnpm/exe.win32-arm64@12.10.1':
- resolution: {integrity: sha512-I7QZHcjioG7orFeCRUFUHByJ1G0zFPnAlaGGyrUvb6MGkv5oeNwNPl7t+Gp+7zHfnMcgLGJ1a+Wfim0IDw3lOw==}
+ '@pnpm/exe.win32-arm64@12.11.2':
+ resolution: {integrity: sha512-PCTjVDdJb6VvraZxrEeFv6EqEA0hhIsAySx28e95J2Tqpi88hLpgOXgNqV0vpxq9A+b8GLQfyk/D+1UlJPv/YQ==}
cpu: [arm64]
os: [win32]
- '@pnpm/exe.win32-x64@12.10.1':
- resolution: {integrity: sha512-4WJFdp58EKlCj0zLobrXU0vpszSZIr4X/3y/uNbAQqGW0zE+VdruZB0gFc3L2kzsUzVwTYbaCn+XEhnCYSAlTg==}
+ '@pnpm/exe.win32-x64@12.11.2':
+ resolution: {integrity: sha512-8sgX86OvRjJ3yZs/TvGR2L31wH9ScPZ+f+geKI3ikuB0RRiK3i4LlTRO8OXN6pdRqj8epMde4312MCjj7JsJdA==}
cpu: [x64]
os: [win32]
- pnpm@12.10.1:
- resolution: {integrity: sha512-ukCjfrGjcNYP6oxM8dNkwTvMzvUY6YrSvO6VT1OdONo3RY5YJho/pV4GUjRQ6g//+fSXsTW+QfLBXBZCusrObA==}
+ pnpm@12.11.2:
+ resolution: {integrity: sha512-JaUWUqGWeFr18ovabf6NxYLr76SsHRgCIc4TkCMcX7dmN4IupLbkYzznSs3LCnf3Ja8RutOBd/3t2u6BipVcuQ==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
- '@pnpm/exe.android-arm64@12.10.1':
+ '@pnpm/exe.android-arm64@12.11.2':
optional: true
- '@pnpm/exe.android-x64@12.10.1':
+ '@pnpm/exe.android-x64@12.11.2':
optional: true
- '@pnpm/exe.darwin-arm64@12.10.1':
+ '@pnpm/exe.darwin-arm64@12.11.2':
optional: true
- '@pnpm/exe.darwin-x64@12.10.1':
+ '@pnpm/exe.darwin-x64@12.11.2':
optional: true
- '@pnpm/exe.freebsd-x64@12.10.1':
+ '@pnpm/exe.freebsd-x64@12.11.2':
optional: true
- '@pnpm/exe.linux-arm64-musl@12.10.1':
+ '@pnpm/exe.linux-arm64-musl@12.11.2':
optional: true
- '@pnpm/exe.linux-arm64@12.10.1':
+ '@pnpm/exe.linux-arm64@12.11.2':
optional: true
- '@pnpm/exe.linux-ppc64@12.10.1':
+ '@pnpm/exe.linux-ppc64@12.11.2':
optional: true
- '@pnpm/exe.linux-riscv64@12.10.1':
+ '@pnpm/exe.linux-riscv64@12.11.2':
optional: true
- '@pnpm/exe.linux-s390x@12.10.1':
+ '@pnpm/exe.linux-s390x@12.11.2':
optional: true
- '@pnpm/exe.linux-x64-musl@12.10.1':
+ '@pnpm/exe.linux-x64-musl@12.11.2':
optional: true
- '@pnpm/exe.linux-x64@12.10.1':
+ '@pnpm/exe.linux-x64@12.11.2':
optional: true
- '@pnpm/exe.win32-arm64@12.10.1':
+ '@pnpm/exe.win32-arm64@12.11.2':
optional: true
- '@pnpm/exe.win32-x64@12.10.1':
+ '@pnpm/exe.win32-x64@12.11.2':
optional: true
- pnpm@12.10.1:
+ pnpm@12.11.2:
optionalDependencies:
- '@pnpm/exe.android-arm64': 12.10.1
- '@pnpm/exe.android-x64': 12.10.1
- '@pnpm/exe.darwin-arm64': 12.10.1
- '@pnpm/exe.darwin-x64': 12.10.1
- '@pnpm/exe.freebsd-x64': 12.10.1
- '@pnpm/exe.linux-arm64': 12.10.1
- '@pnpm/exe.linux-arm64-musl': 12.10.1
- '@pnpm/exe.linux-ppc64': 12.10.1
- '@pnpm/exe.linux-riscv64': 12.10.1
- '@pnpm/exe.linux-s390x': 12.10.1
- '@pnpm/exe.linux-x64': 12.10.1
- '@pnpm/exe.linux-x64-musl': 12.10.1
- '@pnpm/exe.win32-arm64': 12.10.1
- '@pnpm/exe.win32-x64': 12.10.1
+ '@pnpm/exe.android-arm64': 12.11.2
+ '@pnpm/exe.android-x64': 12.11.2
+ '@pnpm/exe.darwin-arm64': 12.11.2
+ '@pnpm/exe.darwin-x64': 12.11.2
+ '@pnpm/exe.freebsd-x64': 12.11.2
+ '@pnpm/exe.linux-arm64': 12.11.2
+ '@pnpm/exe.linux-arm64-musl': 12.11.2
+ '@pnpm/exe.linux-ppc64': 12.11.2
+ '@pnpm/exe.linux-riscv64': 12.11.2
+ '@pnpm/exe.linux-s390x': 12.11.2
+ '@pnpm/exe.linux-x64': 12.11.2
+ '@pnpm/exe.linux-x64-musl': 12.11.2
+ '@pnpm/exe.win32-arm64': 12.11.2
+ '@pnpm/exe.win32-x64': 12.11.2
---
lockfileVersion: '9.0'
diff --git a/scripts/check-architecture.mjs b/scripts/check-architecture.mjs
index 429f413a6..78afa3c19 100644
--- a/scripts/check-architecture.mjs
+++ b/scripts/check-architecture.mjs
@@ -1,84 +1,34 @@
-import fs from 'node:fs'
import path from 'node:path'
-/**
- * Architecture grep gates for the frontend contract flow. Select gates via
- * argv:
- *
- * node scripts/check-architecture.mjs listen emit invoke
- *
- * - listen/emit: the Tauri event API may only be used under src/services/bus/**
- * (the event bus). Detected as a bare `listen(`/`emit(` call (member calls
- * like `this.emit(` are unrelated class methods) or as importing the name
- * from '@tauri-apps/api/event'.
- * - invoke: `invoke(` calls may exist only under src/services/**.
- */
+import { ESLint } from 'eslint'
const ROOT = path.resolve(import.meta.dirname, '..')
-const SRC = path.join(ROOT, 'src')
-
-const tauriEventImport = (name) =>
- new RegExp(
- `import\\s*\\{[^}]*\\b${name}\\b[^}]*\\}\\s*from\\s*'@tauri-apps/api/event'`,
- )
-
-const GATES = {
- listen: {
- patterns: [/(? file.startsWith('src/services/bus/'),
- description: 'Tauri listen outside the event bus (src/services/bus)',
- },
- emit: {
- patterns: [/(? file.startsWith('src/services/bus/'),
- description: 'Tauri emit outside the event bus (src/services/bus)',
- },
- invoke: {
- patterns: [/\binvoke\(/],
- allowed: (file) => file.startsWith('src/services/'),
- description: 'invoke( outside the services layer (src/services)',
- },
-}
-
-const walk = (dir) => {
- const entries = fs.readdirSync(dir, { withFileTypes: true })
- return entries.flatMap((entry) => {
- const full = path.join(dir, entry.name)
- if (entry.isDirectory()) return walk(full)
- return /\.(ts|tsx|js|jsx|mts|mjs)$/.test(entry.name) ? [full] : []
- })
-}
-
-const selected = process.argv.slice(2)
-if (selected.length === 0) {
+const gates = process.argv.slice(2)
+if (
+ !gates.length ||
+ gates.some((gate) => !['listen', 'emit', 'invoke'].includes(gate))
+) {
console.error('usage: check-architecture.mjs ... (listen|emit|invoke)')
process.exit(2)
}
-let violations = 0
-for (const gate of selected) {
- const rule = GATES[gate]
- if (!rule) {
- console.error(`unknown gate: ${gate}`)
- process.exit(2)
- }
- for (const file of walk(SRC)) {
- const relative = path.relative(ROOT, file).replaceAll('\\', '/')
- if (rule.allowed(relative)) continue
- const lines = fs.readFileSync(file, 'utf8').split('\n')
- lines.forEach((line, index) => {
- if (rule.patterns.some((pattern) => pattern.test(line))) {
- violations += 1
- console.error(
- `[arch] ${rule.description}: ${relative}:${index + 1}: ${line.trim()}`,
- )
- }
- })
- }
+// All IPC boundaries use the lint configuration, including when callers request a legacy gate.
+const eslint = new ESLint({ cwd: ROOT, allowInlineConfig: false })
+const results = await eslint.lintFiles(['src'])
+const boundaryRules = new Set([
+ 'no-restricted-imports',
+ 'no-restricted-syntax',
+ '@typescript-eslint/no-floating-promises',
+])
+const violations = results.flatMap((result) =>
+ result.messages
+ .filter((message) => message.fatal || boundaryRules.has(message.ruleId))
+ .map((message) => ({ file: result.filePath, ...message })),
+)
+for (const violation of violations) {
+ console.error(
+ `[arch] ${path.relative(ROOT, violation.file)}:${violation.line}: ${violation.message}`,
+ )
}
-
-if (violations > 0) {
- console.error(`[arch] ${violations} violation(s) found`)
- process.exit(1)
-}
-console.log('[arch] no violations')
+if (violations.length) process.exitCode = 1
+else console.log('[arch] no violations')
diff --git a/scripts/check-contract-changes.mjs b/scripts/check-contract-changes.mjs
new file mode 100644
index 000000000..20c86d9f3
--- /dev/null
+++ b/scripts/check-contract-changes.mjs
@@ -0,0 +1,69 @@
+import { execFileSync } from 'node:child_process'
+import fs from 'node:fs'
+import path from 'node:path'
+
+const ROOT = path.resolve(import.meta.dirname, '..')
+const git = (...args) =>
+ execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }).trim()
+const { contract } = JSON.parse(
+ fs.readFileSync(path.join(ROOT, 'scripts/contract-inputs.json'), 'utf8'),
+)
+const inputs = contract.map((pattern) => `:(glob)${pattern}`)
+const args = process.argv.slice(2)
+if (args.length !== 2 || args[0] !== '--push') {
+ console.error('usage: check-contract-changes.mjs --push ')
+ process.exit(2)
+}
+const hasChanges = (...range) =>
+ git('diff', '--name-only', ...range, '--', ...inputs) !== ''
+const assertCleanInputs = (...range) => {
+ if (
+ hasChanges(...range) ||
+ git('ls-files', '--others', '--exclude-standard', '--', ...inputs)
+ ) {
+ throw new Error(
+ 'Contract inputs differ from the pushed commit. Commit those changes before pushing.',
+ )
+ }
+}
+let relevant = false
+const head = git('rev-parse', 'HEAD')
+for (const line of fs
+ .readFileSync(0, 'utf8')
+ .trim()
+ .split('\n')
+ .filter(Boolean)) {
+ const fields = line.trim().split(/\s+/)
+ if (
+ fields.length !== 4 ||
+ ![fields[1], fields[3]].every((sha) => /^[a-f0-9]{40,64}$/.test(sha))
+ ) {
+ throw new Error('Invalid pre-push ref update')
+ }
+ const [, local, , remote] = fields
+ if (/^0+$/.test(local) || local === remote) continue
+ // A new remote branch has no upstream; use its fork point with the remote default branch.
+ const base = /^0+$/.test(remote)
+ ? git('merge-base', local, `refs/remotes/${args[1]}/HEAD`)
+ : remote
+ if (!hasChanges(base, local)) continue
+ const commit = git('rev-parse', `${local}^{commit}`)
+ if (commit !== head)
+ throw new Error(
+ 'Check out the pushed commit before validating its changed IPC contract.',
+ )
+ assertCleanInputs('HEAD')
+ relevant = true
+}
+if (relevant) {
+ execFileSync(
+ process.execPath,
+ [path.join(ROOT, 'scripts/gen-contract.mjs'), '--check'],
+ {
+ cwd: ROOT,
+ stdio: 'inherit',
+ },
+ )
+} else {
+ console.log('[contract] inputs unchanged; skipping Rust generation')
+}
diff --git a/scripts/contract-inputs.json b/scripts/contract-inputs.json
new file mode 100644
index 000000000..4e9f93100
--- /dev/null
+++ b/scripts/contract-inputs.json
@@ -0,0 +1,23 @@
+{
+ "contract": [
+ "src-tauri/**/*.rs",
+ "crates/**/*.rs",
+ "Cargo.toml",
+ "Cargo.lock",
+ "src-tauri/Cargo.toml",
+ "crates/**/Cargo.toml",
+ "rust-toolchain.toml",
+ "src-tauri/tauri.conf*.json",
+ "src/services/bindings.ts",
+ "scripts/gen-contract.mjs",
+ "scripts/check-contract-changes.mjs",
+ "biome.json",
+ "biome.jsonc",
+ "package.json",
+ "pnpm-lock.yaml",
+ "pnpm-workspace.yaml",
+ "scripts/contract-inputs.json",
+ ".github/workflows/frontend-check.yml",
+ ".husky/pre-push"
+ ]
+}
diff --git a/scripts/gen-contract.mjs b/scripts/gen-contract.mjs
index c2afba095..f1103d898 100644
--- a/scripts/gen-contract.mjs
+++ b/scripts/gen-contract.mjs
@@ -1,91 +1,55 @@
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
+import os from 'node:os'
import path from 'node:path'
-/**
- * Regenerates src/services/contract.ts from the backend wire contract.
- * Source of truth: `cargo run -p clash-verge --bin frontend-contract` inside
- * src-tauri, which serializes the same enum variants and event payloads the
- * app emits at runtime. CI reruns this and fails on a non-empty diff.
- */
-
const ROOT = path.resolve(import.meta.dirname, '..')
-const OUT = path.join(ROOT, 'src/services/contract.ts')
-
-const contract = JSON.parse(
+const BINDINGS = path.join(ROOT, 'src/services/bindings.ts')
+const args = process.argv.slice(2)
+if (args.length > 1 || (args.length === 1 && args[0] !== '--check')) {
+ console.error('usage: gen-contract.mjs [--check]')
+ process.exit(2)
+}
+const check = args[0] === '--check'
+const temporary = check
+ ? fs.mkdtempSync(path.join(os.tmpdir(), 'verge-contract-'))
+ : null
+const output = temporary ? path.join(temporary, 'bindings.ts') : BINDINGS
+try {
execFileSync(
'cargo',
- ['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '-q'],
- {
- cwd: path.join(ROOT, 'src-tauri'),
- encoding: 'utf8',
- maxBuffer: 16 * 1024 * 1024,
- },
- ),
-)
-
-if (contract.version !== 1) {
- throw new Error(`unsupported wire contract version: ${contract.version}`)
-}
-
-/** TS type per event, derived from the serialized sample payload's JSON shape. */
-const payloadType = (name, sample) => {
- if (sample === null) return 'null'
- if (typeof sample === 'string') return 'string'
- if (Array.isArray(sample)) {
- if (name === 'verge://notice-message') return '[NoticeStatus, string]'
- throw new Error(`unexpected array payload for ${name}`)
+ [
+ 'run',
+ '-p',
+ 'clash-verge',
+ '--bin',
+ 'frontend-contract',
+ '--locked',
+ '-q',
+ ...(check ? ['--', output] : []),
+ ],
+ { cwd: ROOT, stdio: 'inherit' },
+ )
+ const formatted = execFileSync(
+ process.execPath,
+ [
+ path.join(ROOT, 'node_modules/@biomejs/biome/bin/biome'),
+ 'format',
+ '--stdin-file-path',
+ BINDINGS,
+ ],
+ { cwd: ROOT, input: fs.readFileSync(output) },
+ )
+ if (check) {
+ if (!formatted.equals(fs.readFileSync(BINDINGS))) {
+ console.error(
+ 'IPC bindings are out of date. Run pnpm gen:contract and commit the generated file.',
+ )
+ process.exitCode = 1
+ }
+ } else {
+ fs.writeFileSync(BINDINGS, formatted)
}
- if (typeof sample === 'object') {
- const keys = Object.keys(sample)
- if (keys.length === 1 && keys[0] === 'uid') return '{ uid: string }'
- if (name === 'verge://run-state-changed') return 'RunState'
- throw new Error(`unexpected object payload for ${name}: ${keys.join(',')}`)
- }
- throw new Error(`unexpected payload for ${name}`)
+} finally {
+ if (temporary) fs.rmSync(temporary, { recursive: true, force: true })
}
-
-const needsRunState = contract.events.some(
- (e) => e.name === 'verge://run-state-changed',
-)
-if (!needsRunState)
- throw new Error('run-state-changed event missing from contract')
-
-const lines = []
-lines.push(
- '// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.',
-)
-lines.push('// Regen: pnpm gen:contract')
-lines.push('')
-lines.push("import type { RunState } from './cmds'")
-lines.push('')
-lines.push(
- '/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */',
-)
-lines.push('export type NoticeStatus =')
-for (const status of contract.noticeStatuses) {
- lines.push(` | ${JSON.stringify(status)}`)
-}
-lines.push('')
-lines.push('/** Event name → payload type for every backend-emitted event. */')
-lines.push('export interface VergeEventPayloads {')
-for (const event of contract.events) {
- const key = JSON.stringify(event.name)
- lines.push(` ${key}: ${payloadType(event.name, event.sample)}`)
-}
-lines.push('}')
-lines.push('')
-
-const content = `${lines.join('\n')}\n`
-fs.writeFileSync(OUT, content)
-
-// Match the repo's biome style so CI parity (`git diff --exit-code`) is stable.
-execFileSync(
- process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm',
- ['exec', 'biome', 'format', '--write', path.relative(ROOT, OUT)],
- { cwd: ROOT, stdio: 'ignore' },
-)
-
-console.log(
- `[gen-contract] wrote ${path.relative(ROOT, OUT)}: ${contract.events.length} events, ${contract.noticeStatuses.length} statuses`,
-)
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 73bb361b1..6a1bc7942 100755
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -80,7 +80,7 @@ tauri-plugin-fs = "2.6.0"
tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.1"
tauri-plugin-window-state = "2.5.0"
-zip = "8.6.0"
+zip = "9.0.1"
flate2 = "1.1.10"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
@@ -109,6 +109,9 @@ webpki-roots = "1.0.9"
dark-light = "3.0.0"
bytes = "1.12.1"
tokio-util = { version = "0.7.19", features = ["time"] }
+specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
+tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
+specta-typescript = "=0.0.12"
[target.'cfg(unix)'.dependencies]
libc = "0.2.190"
diff --git a/src-tauri/src/bin/frontend-contract.rs b/src-tauri/src/bin/frontend-contract.rs
index 2ce0ecede..ba205060d 100644
--- a/src-tauri/src/bin/frontend-contract.rs
+++ b/src-tauri/src/bin/frontend-contract.rs
@@ -1,8 +1,12 @@
-//! Prints the frontend wire contract as JSON to stdout. Zero new dependencies:
-//! the payload is derived from the same serializers the app uses at runtime.
-//! Consumed by scripts/gen-contract.mjs; CI regenerates and diffs the committed
-//! TypeScript artifact.
-
-fn main() {
- println!("{}", app_lib::frontend_wire_contract());
+fn main() -> Result<(), Box> {
+ let mut args = std::env::args_os().skip(1);
+ let output = args
+ .next()
+ .map(std::path::PathBuf::from)
+ .unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"));
+ if args.next().is_some() {
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "expected at most one output path").into());
+ }
+ app_lib::ipc::builder().export(specta_typescript::Typescript::default(), output)?;
+ Ok(())
}
diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs
index 425d31f7b..e043dbc4e 100644
--- a/src-tauri/src/cmd/app.rs
+++ b/src-tauri/src/cmd/app.rs
@@ -9,12 +9,14 @@ use smartstring::alias::String;
use tauri::{AppHandle, Manager as _, ipc::Channel};
#[tauri::command]
+#[specta::specta]
pub async fn open_app_dir() -> CmdResult<()> {
let app_dir = dirs::app_home_dir().stringify_err()?;
open::that(app_dir).stringify_err()
}
#[tauri::command]
+#[specta::specta]
pub async fn open_core_dir() -> CmdResult<()> {
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
@@ -22,12 +24,14 @@ pub async fn open_core_dir() -> CmdResult<()> {
}
#[tauri::command]
+#[specta::specta]
pub async fn open_logs_dir() -> CmdResult<()> {
let log_dir = dirs::app_logs_dir().stringify_err()?;
open::that(log_dir).stringify_err()
}
#[tauri::command]
+#[specta::specta]
pub fn open_devtools(app_handle: AppHandle) {
if let Some(window) = app_handle.get_webview_window("main") {
if !window.is_devtools_open() {
@@ -39,22 +43,26 @@ pub fn open_devtools(app_handle: AppHandle) {
}
#[tauri::command]
+#[specta::specta]
pub async fn exit_app() {
feat::quit().await;
}
#[tauri::command]
+#[specta::specta]
pub async fn restart_app() -> CmdResult<()> {
feat::restart_app().await;
Ok(())
}
#[tauri::command]
+#[specta::specta]
pub async fn install_update(
app_handle: AppHandle,
- version: String,
+ version: std::string::String,
on_event: Channel,
) -> CmdResult {
+ let version: String = version.into();
SilentUpdater::global()
.install_update(&app_handle, &version, |event| {
let _ = on_event.send(event);
@@ -64,22 +72,32 @@ pub async fn install_update(
}
#[tauri::command]
+#[specta::specta]
pub fn cancel_update_download() {
SilentUpdater::global().cancel_download();
}
#[tauri::command]
-pub fn get_app_dir() -> CmdResult {
+#[specta::specta]
+pub fn get_app_dir() -> CmdResult {
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
Ok(app_home_dir)
}
#[tauri::command]
-pub async fn download_icon_cache(url: String, name: String) -> CmdResult {
- feat::download_icon_cache(url, name).await
+#[specta::specta]
+pub async fn download_icon_cache(
+ url: std::string::String,
+ name: std::string::String,
+) -> CmdResult {
+ let url: String = url.into();
+ let name: String = name.into();
+ feat::download_icon_cache(url, name).await.map(Into::into)
}
#[tauri::command]
-pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult {
- feat::copy_icon_file(path, icon_info).await
+#[specta::specta]
+pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult {
+ let path: String = path.into();
+ feat::copy_icon_file(path, icon_info).await.map(Into::into)
}
diff --git a/src-tauri/src/cmd/backup.rs b/src-tauri/src/cmd/backup.rs
index 52aa2f0d9..349c869b5 100644
--- a/src-tauri/src/cmd/backup.rs
+++ b/src-tauri/src/cmd/backup.rs
@@ -4,33 +4,44 @@ use feat::LocalBackupFile;
use smartstring::alias::String;
#[tauri::command]
+#[specta::specta]
pub async fn create_local_backup() -> CmdResult<()> {
feat::create_local_backup().await.stringify_err()
}
#[tauri::command]
+#[specta::specta]
pub async fn list_local_backup() -> CmdResult> {
feat::list_local_backup().await.stringify_err()
}
#[tauri::command]
-pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
+#[specta::specta]
+pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
+ let filename: String = filename.into();
feat::delete_local_backup(filename).await.stringify_err()
}
#[tauri::command]
-pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
+#[specta::specta]
+pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
+ let filename: String = filename.into();
feat::restore_local_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
}
#[tauri::command]
-pub async fn import_local_backup(source: String) -> CmdResult {
- feat::import_local_backup(source).await.stringify_err()
+#[specta::specta]
+pub async fn import_local_backup(source: std::string::String) -> CmdResult {
+ let source: String = source.into();
+ feat::import_local_backup(source).await.map(Into::into).stringify_err()
}
#[tauri::command]
-pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
+#[specta::specta]
+pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
+ let filename: String = filename.into();
+ let destination: String = destination.into();
feat::export_local_backup(filename, destination).await.stringify_err()
}
diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs
index 2d5a567db..6fb9602ad 100644
--- a/src-tauri/src/cmd/clash.rs
+++ b/src-tauri/src/cmd/clash.rs
@@ -12,30 +12,33 @@ use crate::{
},
};
use clash_verge_logging::{Type, logging, logging_error};
-use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
#[tauri::command]
+#[specta::specta]
pub async fn copy_clash_env() -> CmdResult {
- feat::copy_clash_env().await;
- Ok(())
+ feat::copy_clash_env().await.stringify_err()
}
#[tauri::command]
+#[specta::specta]
pub async fn get_clash_info() -> CmdResult {
Ok(Config::clash().await.data_arc().get_client_info())
}
#[tauri::command]
-pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
- feat::patch_clash(&payload)
+#[specta::specta]
+pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
+ feat::patch_clash(&payload.0)
.await
.map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED"))
}
#[tauri::command]
-pub async fn patch_clash_mode(payload: String) -> CmdResult {
+#[specta::specta]
+pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
+ let payload: String = payload.into();
feat::change_clash_mode(payload)
.await
.with_error_code("CLASH_MODE_UPDATE_FAILED")
@@ -43,12 +46,15 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
#[tauri::command]
-pub async fn get_clash_mode() -> CmdResult