clash-kit 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,125 +1,128 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import axios from 'axios'
4
- import { fileURLToPath } from 'url'
5
- import ora from 'ora'
6
- import { reloadConfig, isClashRunning } from './api.js'
7
- import YAML from 'yaml'
8
-
9
- const __filename = fileURLToPath(import.meta.url)
10
- const __dirname = path.dirname(__filename)
11
-
12
- const PROFILES_DIR = path.join(__dirname, '../profiles')
13
- const CONFIG_PATH = path.join(__dirname, '../config.yaml')
14
- const CURRENT_PROFILE_PATH = path.join(__dirname, '../.current_profile')
15
-
16
- // 确保 profiles 目录存在
17
- if (!fs.existsSync(PROFILES_DIR)) {
18
- fs.mkdirSync(PROFILES_DIR)
19
- }
20
-
21
- export async function downloadSubscription(url, name) {
22
- const spinner = ora(`正在下载订阅 ${name}...`).start()
23
- try {
24
- const res = await axios.get(url, {
25
- responseType: 'text',
26
- headers: {
27
- 'User-Agent': 'Clash/1.0.0', // 伪装成 Clash 客户端,通常能直接获取 YAML 格式
28
- },
29
- })
30
-
31
- let content = res.data
32
-
33
- // 尝试解析 YAML,如果不是对象或者看起来不像配置,尝试 Base64 解码
34
- let isConfig = false
35
- try {
36
- const parsed = YAML.parse(content)
37
- if (parsed && typeof parsed === 'object' && (parsed.proxies || parsed.Proxy || parsed.port)) {
38
- isConfig = true
39
- }
40
- } catch (e) {
41
- console.warn('订阅服务商返回的不是有效 YAML,尝试 Base64 解码...')
42
- }
43
-
44
- if (!isConfig) {
45
- try {
46
- // 尝试 Base64 解码
47
- const decoded = Buffer.from(content, 'base64').toString('utf-8')
48
- // 再次检查解码后是否为有效 YAML 配置
49
- const parsedDecoded = YAML.parse(decoded)
50
- if (
51
- parsedDecoded &&
52
- typeof parsedDecoded === 'object' &&
53
- (parsedDecoded.proxies || parsedDecoded.Proxy || parsedDecoded.port)
54
- ) {
55
- content = decoded
56
- }
57
- } catch (e) {
58
- console.warn('Base64 解码失败,保留原始内容')
59
- }
60
- }
61
-
62
- const filePath = path.join(PROFILES_DIR, `${name}.yaml`)
63
- fs.writeFileSync(filePath, content)
64
- spinner.succeed(`订阅 ${name} 下载成功`)
65
- return filePath
66
- } catch (err) {
67
- spinner.fail(`下载订阅失败: ${err.message}`)
68
- throw new Error(`下载订阅失败: ${err.message}`)
69
- }
70
- }
71
-
72
- export function listProfiles() {
73
- return fs
74
- .readdirSync(PROFILES_DIR)
75
- .filter(f => f.endsWith('.yaml'))
76
- .map(f => f.replace('.yaml', ''))
77
- }
78
-
79
- export function getCurrentProfile() {
80
- if (fs.existsSync(CURRENT_PROFILE_PATH)) {
81
- return fs.readFileSync(CURRENT_PROFILE_PATH, 'utf8').trim()
82
- }
83
- return null
84
- }
85
-
86
- export async function useProfile(name) {
87
- const source = path.join(PROFILES_DIR, `${name}.yaml`)
88
- if (!fs.existsSync(source)) throw new Error(`配置文件 ${name} 不存在`)
89
-
90
- const spinner = ora(`正在切换到配置 ${name}...`).start()
91
-
92
- // 读取订阅配置
93
- const subscriptionConfig = YAML.parse(fs.readFileSync(source, 'utf8'))
94
-
95
- // 读取现有配置(如果存在)
96
- let existingConfig = {}
97
- if (fs.existsSync(CONFIG_PATH)) {
98
- existingConfig = YAML.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))
99
- }
100
-
101
- // 合并配置:保留用户自定义字段,更新订阅字段
102
- const mergedConfig = {
103
- ...subscriptionConfig,
104
- port: existingConfig['port'],
105
- 'bind-address': existingConfig['bind-address'],
106
- 'socks-port': existingConfig['socks-port'],
107
- 'allow-lan': existingConfig['allow-lan'],
108
- // 其他需要保留的自定义字段...
109
- }
110
-
111
- // 写入合并后的配置
112
- fs.writeFileSync(CONFIG_PATH, YAML.stringify(mergedConfig))
113
-
114
- // 尝试热重载
115
- if (await isClashRunning()) {
116
- try {
117
- await reloadConfig(CONFIG_PATH)
118
- spinner.succeed('Clash 配置已切换并热重载生效')
119
- } catch (err) {
120
- spinner.warn(`配置已切换,但热重载失败: ${err.message}`)
121
- }
122
- } else {
123
- spinner.succeed('配置已切换(Clash 未运行,将在下次启动时生效)')
124
- }
125
- }
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import axios from 'axios'
4
+ import { fileURLToPath } from 'url'
5
+ import ora from 'ora'
6
+ import * as api from './api.js'
7
+ import YAML from 'yaml'
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = path.dirname(__filename)
11
+
12
+ export const PROFILES_DIR = path.join(__dirname, '../profiles')
13
+ export const CONFIG_PATH = path.join(__dirname, '../config.yaml')
14
+ export const CURRENT_PROFILE_PATH = path.join(__dirname, '../.current_profile')
15
+
16
+ // 确保 profiles 目录存在
17
+ if (!fs.existsSync(PROFILES_DIR)) {
18
+ fs.mkdirSync(PROFILES_DIR)
19
+ }
20
+
21
+ export async function downloadSubscription(url, name) {
22
+ const spinner = ora(`正在下载订阅 ${name}...`).start()
23
+ try {
24
+ const res = await axios.get(url, {
25
+ responseType: 'text',
26
+ headers: {
27
+ 'User-Agent': 'Clash/1.0.0', // 伪装成 Clash 客户端,通常能直接获取 YAML 格式
28
+ },
29
+ })
30
+
31
+ let content = res.data
32
+
33
+ // 尝试解析 YAML,如果不是对象或者看起来不像配置,尝试 Base64 解码
34
+ let isConfig = false
35
+ try {
36
+ const parsed = YAML.parse(content)
37
+ if (parsed && typeof parsed === 'object' && (parsed.proxies || parsed.Proxy || parsed.port)) {
38
+ isConfig = true
39
+ }
40
+ } catch (e) {
41
+ console.warn('订阅服务商返回的不是有效 YAML,尝试 Base64 解码...')
42
+ }
43
+
44
+ if (!isConfig) {
45
+ try {
46
+ // 尝试 Base64 解码
47
+ const decoded = Buffer.from(content, 'base64').toString('utf-8')
48
+ // 再次检查解码后是否为有效 YAML 配置
49
+ const parsedDecoded = YAML.parse(decoded)
50
+ if (
51
+ parsedDecoded &&
52
+ typeof parsedDecoded === 'object' &&
53
+ (parsedDecoded.proxies || parsedDecoded.Proxy || parsedDecoded.port)
54
+ ) {
55
+ content = decoded
56
+ }
57
+ } catch (e) {
58
+ console.warn('Base64 解码失败,保留原始内容')
59
+ }
60
+ }
61
+
62
+ const filePath = path.join(PROFILES_DIR, `${name}.yaml`)
63
+ fs.writeFileSync(filePath, content)
64
+ spinner.succeed(`订阅 ${name} 下载成功`)
65
+ return filePath
66
+ } catch (err) {
67
+ spinner.fail(`下载订阅失败: ${err.message}`)
68
+ throw new Error(`下载订阅失败: ${err.message}`)
69
+ }
70
+ }
71
+
72
+ export function listProfiles() {
73
+ return fs
74
+ .readdirSync(PROFILES_DIR)
75
+ .filter(f => f.endsWith('.yaml'))
76
+ .map(f => f.replace('.yaml', ''))
77
+ }
78
+
79
+ export async function getCurrentProfile() {
80
+ if (fs.existsSync(CURRENT_PROFILE_PATH)) {
81
+ return fs.readFileSync(CURRENT_PROFILE_PATH, 'utf8').trim()
82
+ }
83
+ return null
84
+ }
85
+
86
+ export async function useProfile(name) {
87
+ const source = path.join(PROFILES_DIR, `${name}.yaml`)
88
+ if (!fs.existsSync(source)) throw new Error(`配置文件 ${name} 不存在`)
89
+
90
+ const spinner = ora(`正在切换到配置 ${name}...`).start()
91
+
92
+ // 读取订阅配置
93
+ const subscriptionConfig = YAML.parse(fs.readFileSync(source, 'utf8'))
94
+
95
+ // 读取现有配置(如果存在)
96
+ let existingConfig = {}
97
+ if (fs.existsSync(CONFIG_PATH)) {
98
+ existingConfig = YAML.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))
99
+ }
100
+
101
+ // 合并配置:保留用户自定义字段,更新订阅字段
102
+ const mergedConfig = {
103
+ ...subscriptionConfig,
104
+ port: existingConfig['port'],
105
+ 'bind-address': existingConfig['bind-address'],
106
+ 'socks-port': existingConfig['socks-port'],
107
+ 'allow-lan': existingConfig['allow-lan'],
108
+ // 其他需要保留的自定义字段...
109
+ }
110
+
111
+ // 写入合并后的配置
112
+ fs.writeFileSync(CONFIG_PATH, YAML.stringify(mergedConfig))
113
+
114
+ // 记录当前使用的配置文件
115
+ fs.writeFileSync(CURRENT_PROFILE_PATH, name)
116
+
117
+ // 尝试热重载
118
+ if (await api.isClashRunning()) {
119
+ try {
120
+ await api.reloadConfig(CONFIG_PATH)
121
+ spinner.succeed('Clash 配置已切换并热重载生效')
122
+ } catch (err) {
123
+ spinner.warn(`配置已切换,但热重载失败: ${err.message}`)
124
+ }
125
+ } else {
126
+ spinner.succeed('配置已切换(Clash 未运行,将在下次启动时生效)')
127
+ }
128
+ }
package/lib/sysnet.js CHANGED
@@ -1,52 +1,73 @@
1
- import { execSync } from 'child_process'
2
-
3
- /**
4
- * 获取 macOS 当前主要的网络服务名称 (Wi-Fi, Ethernet 等)
5
- */
6
- function getMainNetworkService() {
7
- try {
8
- // 这是一个简单的方法,通常第一行是默认路由接口
9
- // networksetup -listnetworkserviceorder
10
- // 更精准的方法是 route get default 但输出解析较复杂
11
- // 这里我们先假设最常见的几种情况
12
- const output = execSync('networksetup -listallnetworkservices', { encoding: 'utf-8' })
13
- const services = output.split('\n').filter(s => s && !s.includes('An asterisk'))
14
-
15
- // 优先尝试 Wi-Fi 和 Ethernet
16
- const wifi = services.find(s => s === 'Wi-Fi')
17
- const ethernet = services.find(s => s.includes('Ethernet') || s.includes('LAN'))
18
-
19
- // 简单粗暴:优先返回 Wi-Fi,其次 Ethernet,最后第一个
20
- // TODO: 更严谨的做法是检测哪个接口有 IP 且是 default route
21
- return wifi || ethernet || services[0]
22
- } catch (e) {
23
- console.error('获取网络服务失败:', e)
24
- return null
25
- }
26
- }
27
-
28
- /**
29
- * 设置系统 DNS
30
- * @param {string[]} servers DNS 服务器列表
31
- */
32
- export function setDNS(servers) {
33
- if (process.platform !== 'darwin') return // 目前仅支持 macOS
34
-
35
- const service = getMainNetworkService()
36
- if (!service) return
37
-
38
- try {
39
- if (servers.length === 0) {
40
- // 恢复默认 (Empty 会让 macOS 使用 DHCP 下发的 DNS)
41
- console.log(`正在恢复系统 DNS (${service})...`)
42
- execSync(`sudo networksetup -setdnsservers "${service}" "Empty"`)
43
- } else {
44
- console.log(`正在设置系统 DNS (${service}) -> ${servers.join(' ')}...`)
45
- execSync(`sudo networksetup -setdnsservers "${service}" ${servers.join(' ')}`)
46
- }
47
- } catch (e) {
48
- // 忽略错误,通常是因为需要 sudo 但在非交互模式下失败
49
- // 但我们的 CLI 主要是用户手动执行的。
50
- console.warn(`设置 DNS 失败 (可能需要 sudo): ${e.message}`)
51
- }
52
- }
1
+ import { execSync } from 'child_process'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = path.dirname(__filename)
8
+ const HELPER_BIN = path.join(__dirname, '../bin/dns-helper')
9
+
10
+ /**
11
+ * 获取 macOS 当前主要的网络服务名称 (Wi-Fi, Ethernet 等)
12
+ */
13
+ function getMainNetworkService() {
14
+ try {
15
+ // 这是一个简单的方法,通常第一行是默认路由接口
16
+ // networksetup -listnetworkserviceorder
17
+ // 更精准的方法是 route get default 但输出解析较复杂
18
+ // 这里我们先假设最常见的几种情况
19
+ const output = execSync('networksetup -listallnetworkservices', { encoding: 'utf-8' })
20
+ const services = output.split('\n').filter(s => s && !s.includes('An asterisk'))
21
+
22
+ // 优先尝试 Wi-Fi 和 Ethernet
23
+ const wifi = services.find(s => s === 'Wi-Fi')
24
+ const ethernet = services.find(s => s.includes('Ethernet') || s.includes('LAN'))
25
+
26
+ // 简单粗暴:优先返回 Wi-Fi,其次 Ethernet,最后第一个
27
+ // TODO: 更严谨的做法是检测哪个接口有 IP 且是 default route
28
+ return wifi || ethernet || services[0]
29
+ } catch (e) {
30
+ console.error('获取网络服务失败:', e)
31
+ return null
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 设置系统 DNS
37
+ * @param {string[]} servers DNS 服务器列表
38
+ */
39
+ export function setDNS(servers) {
40
+ if (process.platform !== 'darwin') {
41
+ return { success: true, message: 'Platform is not macOS, skipping DNS change.' }
42
+ }
43
+
44
+ const service = getMainNetworkService()
45
+ if (!service) {
46
+ return { success: false, error: 'Could not determine main network service.' }
47
+ }
48
+
49
+ // 检查是否存在 helper
50
+ let useHelper = false
51
+ try {
52
+ if (fs.existsSync(HELPER_BIN)) {
53
+ const stats = fs.statSync(HELPER_BIN)
54
+ if (stats.uid === 0 && (stats.mode & 0o4000)) {
55
+ useHelper = true
56
+ }
57
+ }
58
+ } catch (e) {
59
+ /* ignore */
60
+ }
61
+
62
+ const serversArg = servers.length > 0 ? servers.join(' ') : 'Empty'
63
+ const command = useHelper
64
+ ? `"${HELPER_BIN}" "${service}" ${serversArg}`
65
+ : `sudo networksetup -setdnsservers "${service}" ${serversArg}`
66
+
67
+ try {
68
+ execSync(command, { stdio: 'pipe' }) // Use 'pipe' to suppress output
69
+ return { success: true }
70
+ } catch (e) {
71
+ return { success: false, error: e.message }
72
+ }
73
+ }