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,81 +1,81 @@
1
- import { select, input } from '@inquirer/prompts'
2
- import path from 'path'
3
- import fs from 'fs'
4
- import { fileURLToPath } from 'url'
5
- import chalk from 'chalk'
6
- import * as sub from '../subscription.js'
7
- import { CLASH_BIN_PATH } from '../../index.js'
8
-
9
- const __filename = fileURLToPath(import.meta.url)
10
- const __dirname = path.dirname(__filename)
11
-
12
- async function handleAddSubscription(url, name) {
13
- const profiles = sub.listProfiles()
14
- // 没有找到可选的订阅,或没找到 config.yaml
15
- // __dirname here is lib/commands. Config is at ROOT/config.yaml. so join ../../config.yaml
16
- const isFirst = profiles.length === 0 || !fs.existsSync(path.join(__dirname, '../../config.yaml'))
17
-
18
- await sub.downloadSubscription(url, name)
19
-
20
- if (!isFirst) return
21
- console.log(`检测到这是第一个订阅,正在自动切换到 ${name}...`)
22
- await sub.useProfile(name)
23
- }
24
-
25
- export async function manageSub(options) {
26
- // 检查 clash-kit 二进制文件是否存在
27
- if (!fs.existsSync(CLASH_BIN_PATH)) {
28
- return console.error(chalk.red('\n找不到 Clash.Meta 内核文件,请先运行 clash init 命令初始化内核!\n'))
29
- }
30
-
31
- if (options.add) {
32
- if (!options.name) return console.error('错误: 添加订阅时必须指定名称 (-n)')
33
- try {
34
- await handleAddSubscription(options.add, options.name)
35
- } catch (err) {
36
- console.error(err.message)
37
- }
38
- } else if (options.list) {
39
- const profiles = sub.listProfiles()
40
- console.log(`${profiles.length ? '已添加的订阅:' : '暂无已添加的订阅'}`)
41
- profiles.forEach(p => console.log(`- ${p}`))
42
- } else if (options.use) {
43
- try {
44
- await sub.useProfile(options.use)
45
- } catch (err) {
46
- console.error(err.message)
47
- }
48
- } else {
49
- // 交互式模式
50
- const profiles = sub.listProfiles()
51
-
52
- const action = await select({
53
- message: '请选择操作:',
54
- choices: [
55
- { name: '切换订阅', value: 'switch' },
56
- { name: '添加订阅', value: 'add' },
57
- ],
58
- })
59
-
60
- if (action === 'switch') {
61
- if (profiles.length === 0) {
62
- console.log('暂无订阅,请先添加')
63
- return
64
- }
65
- const profile = await select({
66
- message: '选择要使用的订阅:',
67
- choices: profiles.map(p => ({ name: p, value: p })),
68
- })
69
- await sub.useProfile(profile)
70
- } else if (action === 'add') {
71
- const url = await input({ message: '请输入订阅链接:' })
72
- const name = await input({ message: '请输入订阅名称:' })
73
-
74
- try {
75
- await handleAddSubscription(url, name)
76
- } catch (err) {
77
- console.error(err.message)
78
- }
79
- }
80
- }
81
- }
1
+ import { select, input } from '@inquirer/prompts'
2
+ import path from 'path'
3
+ import fs from 'fs'
4
+ import { fileURLToPath } from 'url'
5
+ import chalk from 'chalk'
6
+ import * as sub from '../subscription.js'
7
+ import { CLASH_BIN_PATH } from '../../index.js'
8
+
9
+ const __filename = fileURLToPath(import.meta.url)
10
+ const __dirname = path.dirname(__filename)
11
+
12
+ async function handleAddSubscription(url, name) {
13
+ const profiles = sub.listProfiles()
14
+ // 没有找到可选的订阅,或没找到 config.yaml
15
+ // __dirname here is lib/commands. Config is at ROOT/config.yaml. so join ../../config.yaml
16
+ const isFirst = profiles.length === 0 || !fs.existsSync(path.join(__dirname, '../../config.yaml'))
17
+
18
+ await sub.downloadSubscription(url, name)
19
+
20
+ if (!isFirst) return
21
+ console.log(`检测到这是第一个订阅,正在自动切换到 ${name}...`)
22
+ await sub.useProfile(name)
23
+ }
24
+
25
+ export async function manageSub(options) {
26
+ // 检查 clash-kit 二进制文件是否存在
27
+ if (!fs.existsSync(CLASH_BIN_PATH)) {
28
+ return console.error(chalk.red('\n找不到 Clash.Meta 内核文件,请先运行 clash init 命令初始化内核!\n'))
29
+ }
30
+
31
+ if (options.add) {
32
+ if (!options.name) return console.error('错误: 添加订阅时必须指定名称 (-n)')
33
+ try {
34
+ await handleAddSubscription(options.add, options.name)
35
+ } catch (err) {
36
+ console.error(err.message)
37
+ }
38
+ } else if (options.list) {
39
+ const profiles = sub.listProfiles()
40
+ console.log(`${profiles.length ? '已添加的订阅:' : '暂无已添加的订阅'}`)
41
+ profiles.forEach(p => console.log(`- ${p}`))
42
+ } else if (options.use) {
43
+ try {
44
+ await sub.useProfile(options.use)
45
+ } catch (err) {
46
+ console.error(err.message)
47
+ }
48
+ } else {
49
+ // 交互式模式
50
+ const profiles = sub.listProfiles()
51
+
52
+ const action = await select({
53
+ message: '请选择操作:',
54
+ choices: [
55
+ { name: '切换订阅', value: 'switch' },
56
+ { name: '添加订阅', value: 'add' },
57
+ ],
58
+ })
59
+
60
+ if (action === 'switch') {
61
+ if (profiles.length === 0) {
62
+ console.log('暂无订阅,请先添加')
63
+ return
64
+ }
65
+ const profile = await select({
66
+ message: '选择要使用的订阅:',
67
+ choices: profiles.map(p => ({ name: p, value: p })),
68
+ })
69
+ await sub.useProfile(profile)
70
+ } else if (action === 'add') {
71
+ const url = await input({ message: '请输入订阅链接:' })
72
+ const name = await input({ message: '请输入订阅名称:' })
73
+
74
+ try {
75
+ await handleAddSubscription(url, name)
76
+ } catch (err) {
77
+ console.error(err.message)
78
+ }
79
+ }
80
+ }
81
+ }
@@ -1,24 +1,60 @@
1
- import { select } from '@inquirer/prompts'
2
- import * as sysproxy from '../sysproxy.js'
3
-
4
- export async function setSysProxy(action) {
5
- if (action === 'on') {
6
- await sysproxy.enableSystemProxy()
7
- } else if (action === 'off') {
8
- await sysproxy.disableSystemProxy()
9
- } else {
10
- // 交互式选择
11
- const answer = await select({
12
- message: '请选择系统代理操作:',
13
- choices: [
14
- { name: '开启系统代理', value: 'on' },
15
- { name: '关闭系统代理', value: 'off' },
16
- ],
17
- })
18
- if (answer === 'on') {
19
- await sysproxy.enableSystemProxy()
20
- } else {
21
- await sysproxy.disableSystemProxy()
22
- }
23
- }
24
- }
1
+ import { select } from '@inquirer/prompts'
2
+ import * as sysproxy from '../sysproxy.js'
3
+ import ora from 'ora'
4
+ import boxen from 'boxen'
5
+ import chalk from 'chalk'
6
+
7
+ async function handleAction(action) {
8
+ if (action === 'on') {
9
+ const spinner = ora('正在开启系统代理...').start()
10
+ const result = await sysproxy.enableSystemProxy()
11
+ if (result.success) {
12
+ spinner.stop()
13
+ const content = [`状态: ${chalk.green('已开启')}`, `地址: ${chalk.cyan(`${result.host}:${result.port}`)}`]
14
+ console.log(
15
+ boxen(content.join('\n'), {
16
+ title: '系统代理',
17
+ padding: 1,
18
+ margin: 1,
19
+ borderStyle: 'round',
20
+ borderColor: 'green',
21
+ }),
22
+ )
23
+ } else {
24
+ spinner.fail(`开启失败: ${result.error}`)
25
+ }
26
+ } else if (action === 'off') {
27
+ const spinner = ora('正在关闭系统代理...').start()
28
+ const result = await sysproxy.disableSystemProxy()
29
+ if (result.success) {
30
+ spinner.stop()
31
+ console.log(
32
+ boxen(chalk.yellow('状态: 已关闭'), {
33
+ title: '系统代理',
34
+ padding: 1,
35
+ margin: 1,
36
+ borderStyle: 'round',
37
+ borderColor: 'yellow',
38
+ }),
39
+ )
40
+ } else {
41
+ spinner.fail(`关闭失败: ${result.error}`)
42
+ }
43
+ }
44
+ }
45
+
46
+ export async function setSysProxy(action) {
47
+ if (action === 'on' || action === 'off') {
48
+ await handleAction(action)
49
+ } else {
50
+ // 交互式选择
51
+ const answer = await select({
52
+ message: '请选择系统代理操作:',
53
+ choices: [
54
+ { name: '开启系统代理', value: 'on' },
55
+ { name: '关闭系统代理', value: 'off' },
56
+ ],
57
+ })
58
+ await handleAction(answer)
59
+ }
60
+ }
@@ -1,70 +1,70 @@
1
- import chalk from 'chalk'
2
- import * as api from '../api.js'
3
-
4
- export async function test() {
5
- try {
6
- const proxies = await api.getProxies()
7
- // 默认测速 Proxy 组的所有节点
8
- const group = proxies['Proxy'] || Object.values(proxies).find(p => p.type === 'Selector')
9
-
10
- if (!group) {
11
- console.error('找不到 Proxy 组')
12
- return
13
- }
14
-
15
- console.log(`\n当前测速目标组: [${group.name}]`)
16
- console.log(`包含节点数: ${group.all.length}`)
17
- console.log('注意: 测速针对的是当前 Clash 正在运行的配置。')
18
- console.log('如果刚切换了订阅,请确保看到"配置已热重载生效"提示,或手动重启 clash start。\n')
19
-
20
- const results = []
21
- const concurrency = 10
22
- const queue = [...group.all]
23
- const total = group.all.length
24
- let completed = 0
25
-
26
- console.log(chalk.gray(`准备并发测速 (并发数: ${concurrency})...`))
27
-
28
- const worker = async () => {
29
- while (queue.length > 0) {
30
- const name = queue.shift()
31
- if (!name) break
32
-
33
- try {
34
- const delay = await api.getProxyDelay(name)
35
- completed++
36
- const progress = `[${completed}/${total}]`
37
-
38
- if (delay > 0) {
39
- const color = delay < 200 ? chalk.green : delay < 800 ? chalk.yellow : chalk.red
40
- console.log(`${chalk.gray(progress)} ${chalk.cyan(name)}: ${color(delay + 'ms')}`)
41
- results.push({ name, delay })
42
- } else {
43
- console.log(`${chalk.gray(progress)} ${chalk.cyan(name)}: ${chalk.red('超时')}`)
44
- results.push({ name, delay: 99999 })
45
- }
46
- } catch (err) {
47
- completed++
48
- results.push({ name, delay: 99999 })
49
- }
50
- }
51
- }
52
-
53
- await Promise.all(Array.from({ length: Math.min(concurrency, total) }, () => worker()))
54
-
55
- console.log(chalk.bold.blue('\n=== 测速结果 (Top 5) ==='))
56
- results.sort((a, b) => a.delay - b.delay)
57
- results.slice(0, 5).forEach((r, i) => {
58
- let delayInfo
59
- if (r.delay === 99999) {
60
- delayInfo = chalk.red('超时')
61
- } else {
62
- const color = r.delay < 200 ? chalk.green : r.delay < 800 ? chalk.yellow : chalk.red
63
- delayInfo = color(`${r.delay}ms`)
64
- }
65
- console.log(`${chalk.gray(i + 1 + '.')} ${chalk.bold(r.name)}: ${delayInfo}`)
66
- })
67
- } catch (err) {
68
- console.error(err.message)
69
- }
70
- }
1
+ import chalk from 'chalk'
2
+ import * as api from '../api.js'
3
+
4
+ export async function test() {
5
+ try {
6
+ const proxies = await api.getProxies()
7
+ // 默认测速 Proxy 组的所有节点
8
+ const group = proxies['Proxy'] || Object.values(proxies).find(p => p.type === 'Selector')
9
+
10
+ if (!group) {
11
+ console.error('找不到 Proxy 组')
12
+ return
13
+ }
14
+
15
+ console.log(`\n[${group.name}]${group.all.length}个节点, 当前选中: ${group.now}\n`)
16
+
17
+ const results = []
18
+ const concurrency = 10 // 并发数量
19
+ const queue = [...group.all]
20
+ const total = group.all.length
21
+ let completed = 0
22
+ const current = group.now // 当前选中节点
23
+
24
+ const worker = async () => {
25
+ while (queue.length > 0) {
26
+ const name = queue.shift()
27
+ if (!name) break
28
+
29
+ try {
30
+ const testUrl = name === 'DIRECT' ? 'http://connect.rom.miui.com/generate_204' : undefined
31
+ const delay = await api.getProxyDelay(name, testUrl)
32
+ completed++
33
+ const progress = `[${completed}/${total}]`
34
+ const isCurrent = name === current
35
+ const nameDisplay = isCurrent ? chalk.bold.bgCyan(name) : chalk.cyan(name)
36
+
37
+ if (delay > 0) {
38
+ const color = delay < 200 ? chalk.green : delay < 800 ? chalk.yellow : chalk.red
39
+ console.log(`${chalk.gray(progress)} ${nameDisplay}: ${color(delay + 'ms')}`)
40
+ results.push({ name, delay, isCurrent })
41
+ } else {
42
+ console.log(`${chalk.gray(progress)} ${nameDisplay}: ${chalk.red('超时')}`)
43
+ results.push({ name, delay: 99999, isCurrent })
44
+ }
45
+ } catch (err) {
46
+ completed++
47
+ results.push({ name, delay: 99999, isCurrent: name === current })
48
+ }
49
+ }
50
+ }
51
+
52
+ await Promise.all(Array.from({ length: Math.min(concurrency, total) }, () => worker()))
53
+
54
+ console.log(chalk.bold.blue('\n=== 测速结果 (Top 5) ==='))
55
+ results.sort((a, b) => a.delay - b.delay)
56
+ results.slice(0, 5).forEach((r, i) => {
57
+ let delayInfo
58
+ if (r.delay === 99999) {
59
+ delayInfo = chalk.red('超时')
60
+ } else {
61
+ const color = r.delay < 200 ? chalk.green : r.delay < 800 ? chalk.yellow : chalk.red
62
+ delayInfo = color(`${r.delay}ms`)
63
+ }
64
+ const nameDisplay = r.isCurrent ? chalk.bold.bgCyan(r.name) : chalk.cyan(r.name)
65
+ console.log(`${chalk.gray(i + 1 + '.')} ${nameDisplay}: ${delayInfo}`)
66
+ })
67
+ } catch (err) {
68
+ console.error(err.message)
69
+ }
70
+ }
@@ -1,95 +1,123 @@
1
- import { select } from '@inquirer/prompts'
2
- import chalk from 'chalk'
3
- import * as tun from '../tun.js'
4
- import * as sysnet from '../sysnet.js'
5
- import { main as startClashService } from '../../index.js'
6
-
7
- export async function setTun(action) {
8
- try {
9
- let shouldRestart = false
10
-
11
- if (action === 'on') {
12
- if (process.platform !== 'win32') {
13
- const hasPerm = tun.checkTunPermissions()
14
- const isRoot = process.getuid && process.getuid() === 0
15
-
16
- if (!hasPerm && !isRoot) {
17
- console.log(chalk.yellow('检测到内核缺少 SUID 权限,当前也非 Root 用户,TUN 模式可能无法启动。'))
18
- const confirm = await select({
19
- message: '是否自动授予内核 SUID 权限 (推荐)?',
20
- choices: [
21
- { name: ' (仅需输入一次 sudo 密码)', value: true },
22
- { name: '否 (之后需要 sudo clash start)', value: false },
23
- ],
24
- })
25
- if (confirm) {
26
- tun.setupPermissions()
27
- console.log(chalk.green('权限设置成功!'))
28
- shouldRestart = true
29
- }
30
- }
31
- }
32
-
33
- console.log('正在开启 TUN 模式...')
34
- await tun.enableTun()
35
- sysnet.setDNS(['223.5.5.5', '114.114.114.114'])
36
- console.log(chalk.green('TUN 模式配置已开启'))
37
-
38
- if (shouldRestart) {
39
- console.log(chalk.yellow('因为更改了权限,正在重启 Clash 服务以应用更改...'))
40
- startClashService()
41
- } else {
42
- console.log(chalk.gray('提示: 如果 TUN 模式未生效,请尝试运行 "clash start" 重启服务。'))
43
- }
44
- } else if (action === 'off') {
45
- console.log('正在关闭 TUN 模式...')
46
- await tun.disableTun()
47
- sysnet.setDNS([]) // 恢复系统默认 DNS
48
- console.log(chalk.green('TUN 模式配置已关闭'))
49
- console.log(chalk.gray('配置已热重载。'))
50
- } else {
51
- const isEnabled = await tun.isTunEnabled()
52
- const answer = await select({
53
- message: `请选择 TUN 模式操作 (当前状态: ${isEnabled ? chalk.green('开启') : chalk.gray('关闭')}):`,
54
- choices: [
55
- { name: '开启 TUN 模式', value: 'on' },
56
- { name: '关闭 TUN 模式', value: 'off' },
57
- ],
58
- })
59
- if (answer === 'on') {
60
- if (process.platform !== 'win32') {
61
- const hasPerm = tun.checkTunPermissions()
62
- const isRoot = process.getuid && process.getuid() === 0
63
- if (!hasPerm && !isRoot) {
64
- console.log(chalk.yellow('提示: 建议授予内核 SUID 权限以避免 sudo 启动。'))
65
- const confirm = await select({
66
- message: '是否授予权限?',
67
- choices: [
68
- { name: '是', value: true },
69
- { name: '否', value: false },
70
- ],
71
- })
72
- if (confirm) {
73
- tun.setupPermissions()
74
- shouldRestart = true
75
- }
76
- }
77
- }
78
- await tun.enableTun()
79
- sysnet.setDNS(['223.5.5.5', '114.114.114.114'])
80
- console.log(chalk.green('TUN 模式配置已开启'))
81
-
82
- if (shouldRestart) {
83
- console.log(chalk.yellow('正在重启 Clash 服务...'))
84
- startClashService()
85
- }
86
- } else {
87
- await tun.disableTun()
88
- sysnet.setDNS([])
89
- console.log(chalk.green('TUN 模式配置已关闭'))
90
- }
91
- }
92
- } catch (err) {
93
- console.error(chalk.red(`设置 TUN 模式失败: ${err.message}`))
94
- }
95
- }
1
+ import { select } from '@inquirer/prompts'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import boxen from 'boxen'
5
+ import * as tun from '../tun.js'
6
+ import * as sysnet from '../sysnet.js'
7
+ import { main as startClashService } from '../../index.js'
8
+
9
+ async function turnOn() {
10
+ const spinner = ora('正在配置 TUN 模式...').start()
11
+ let shouldRestart = false
12
+
13
+ try {
14
+ if (process.platform !== 'win32') {
15
+ spinner.text = '正在检查权限...'
16
+ const hasPerm = tun.checkTunPermissions()
17
+ const isRoot = process.getuid && process.getuid() === 0
18
+
19
+ if (!hasPerm && !isRoot) {
20
+ spinner.stop() // Stop spinner for user interaction
21
+ console.log(chalk.yellow('检测到内核缺少 SUID 权限,TUN 模式可能无法启动。'))
22
+ const confirm = await select({
23
+ message: '是否自动授予内核 SUID 权限 (推荐)?',
24
+ choices: [
25
+ { name: '是 (需要 sudo 密码)', value: true },
26
+ { name: '否 (之后可能需要 sudo 启动)', value: false },
27
+ ],
28
+ })
29
+ if (confirm) {
30
+ tun.setupPermissions() // This is noisy
31
+ shouldRestart = true
32
+ }
33
+ spinner.start('正在继续配置...')
34
+ }
35
+ }
36
+
37
+ spinner.text = '正在更新配置文件...'
38
+ await tun.enableTun()
39
+
40
+ spinner.text = '正在设置系统 DNS...'
41
+ sysnet.setDNS(['223.5.5.5', '114.114.114.114']) // This is noisy
42
+
43
+ spinner.stop()
44
+
45
+ const content = [`TUN 模式: ${chalk.green('已开启')}`, `DNS 设置: ${chalk.cyan('223.5.5.5, 114.114.114.114')}`]
46
+ console.log(
47
+ boxen(content.join('\n'), {
48
+ title: 'TUN 配置成功',
49
+ padding: 1,
50
+ margin: 1,
51
+ borderStyle: 'round',
52
+ borderColor: 'green',
53
+ }),
54
+ )
55
+
56
+ if (shouldRestart) {
57
+ console.log(chalk.yellow('权限已变更,正在重启 Clash 服务以应用...'))
58
+ await startClashService()
59
+ } else {
60
+ console.log(chalk.gray('提示: 配置已热重载。如 TUN 未生效, 可尝试 `clash restart`。'))
61
+ }
62
+ } catch (err) {
63
+ if (spinner.isSpinning) spinner.fail(`设置 TUN 失败: ${err.message}`)
64
+ else console.error(chalk.red(`设置 TUN 失败: ${err.message}`))
65
+ }
66
+ }
67
+
68
+ async function turnOff(options = {}) {
69
+ const silent = options.silent || false
70
+ const spinner = silent ? null : ora('正在关闭 TUN 模式...').start()
71
+ try {
72
+ if (spinner) spinner.text = '正在更新配置文件...'
73
+ await tun.disableTun()
74
+
75
+ if (spinner) spinner.text = '正在恢复系统 DNS...'
76
+ const dnsResult = sysnet.setDNS([]) // This is silent now
77
+ if (!dnsResult.success) {
78
+ throw new Error(`恢复 DNS 失败: ${dnsResult.error}`)
79
+ }
80
+
81
+ if (spinner) spinner.stop()
82
+ if (!silent) {
83
+ console.log(
84
+ boxen('TUN 模式: ' + chalk.yellow('已关闭'), {
85
+ title: 'TUN 配置成功',
86
+ padding: 1,
87
+ margin: 1,
88
+ borderStyle: 'round',
89
+ borderColor: 'yellow',
90
+ }),
91
+ )
92
+ console.log(chalk.gray('提示: 配置已热重载。'))
93
+ }
94
+ return { success: true }
95
+ } catch (err) {
96
+ if (spinner) spinner.fail(`关闭 TUN 失败: ${err.message}`)
97
+ return { success: false, error: err.message }
98
+ }
99
+ }
100
+
101
+ export async function setTun(action, options = {}) {
102
+ try {
103
+ if (action === 'on') {
104
+ await turnOn()
105
+ } else if (action === 'off') {
106
+ return await turnOff(options)
107
+ } else {
108
+ const isEnabled = await tun.isTunEnabled()
109
+ const answer = await select({
110
+ message: `请选择 TUN 模式操作 (当前状态: ${isEnabled ? chalk.green('开启') : chalk.gray('关闭')}):`,
111
+ choices: [
112
+ { name: '开启 TUN 模式', value: 'on' },
113
+ { name: '关闭 TUN 模式', value: 'off' },
114
+ ],
115
+ })
116
+ if (answer === 'on') await turnOn()
117
+ else await turnOff(options)
118
+ }
119
+ } catch (err) {
120
+ // Catch errors from select/prompts
121
+ console.error(chalk.red(`操作失败: ${err.message}`))
122
+ }
123
+ }