clash-kit 1.1.1 → 1.1.5

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,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 '../service.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([])
77
+ if (!dnsResult.success) {
78
+ console.warn(chalk.yellow(`\n DNS 恢复失败 (可忽略,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
+ }
package/lib/kernel.js CHANGED
@@ -1,178 +1,197 @@
1
- import axios from 'axios'
2
- import fs from 'fs'
3
- import path from 'path'
4
- import os from 'os'
5
- import zlib from 'zlib'
6
- import AdmZip from 'adm-zip'
7
- import { fileURLToPath } from 'url'
8
- import ora from 'ora'
9
- import cliProgress from 'cli-progress'
10
- import chalk from 'chalk'
11
- import { execSync } from 'child_process'
12
-
13
- const __filename = fileURLToPath(import.meta.url)
14
- const __dirname = path.dirname(__filename)
15
-
16
- const MIHOMO_VERSION_URL = 'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
17
-
18
- const PLATFORM_MAP = {
19
- 'win32-x64': 'mihomo-windows-amd64-compatible',
20
- 'win32-ia32': 'mihomo-windows-386',
21
- 'win32-arm64': 'mihomo-windows-arm64',
22
- 'darwin-x64': 'mihomo-darwin-amd64-compatible',
23
- 'darwin-arm64': 'mihomo-darwin-arm64',
24
- 'linux-x64': 'mihomo-linux-amd64-compatible',
25
- 'linux-arm64': 'mihomo-linux-arm64',
26
- }
27
-
28
- export async function downloadClash(targetDir) {
29
- const platform = os.platform()
30
- const arch = os.arch()
31
- const key = `${platform}-${arch}`
32
- const name = PLATFORM_MAP[key]
33
-
34
- if (!name) {
35
- throw new Error(`不支持的平台: ${platform}-${arch}`)
36
- }
37
-
38
- // 1. 获取最新版本
39
- const spinner = ora('正在获取最新 Mihomo 版本信息...').start()
40
- let version
41
- try {
42
- const { data } = await axios.get(MIHOMO_VERSION_URL)
43
- version = data.trim()
44
- spinner.succeed(`检测到最新版本: ${version}`)
45
- } catch (e) {
46
- spinner.fail(`获取版本信息失败: ${e.message}`)
47
- throw new Error(`获取版本信息失败: ${e.message}`)
48
- }
49
-
50
- // 2. 构建下载 URL
51
- const isWin = platform === 'win32'
52
- const urlExt = isWin ? 'zip' : 'gz'
53
- const downloadUrl = `https://github.com/MetaCubeX/mihomo/releases/download/${version}/${name}-${version}.${urlExt}`
54
- const tempFileName = `mihomo-temp.${urlExt}`
55
- const tempPath = path.join(targetDir, tempFileName)
56
- const targetBinName = `clash-kit${isWin ? '.exe' : ''}`
57
- const targetBinPath = path.join(targetDir, targetBinName)
58
-
59
- // 3. 下载文件
60
- console.log(`正在下载: ${downloadUrl}`)
61
- const bar = new cliProgress.SingleBar(
62
- {
63
- format: '下载进度 | {bar} | {percentage}% | {valueFormatted}/{totalFormatted} MB',
64
- hideCursor: true,
65
- },
66
- cliProgress.Presets.shades_classic
67
- )
68
-
69
- try {
70
- let started = false
71
- let totalMB = '0.0'
72
- const response = await axios({
73
- url: downloadUrl,
74
- method: 'GET',
75
- responseType: 'arraybuffer',
76
- onDownloadProgress: progressEvent => {
77
- if (progressEvent.total) {
78
- const loadedMB = (progressEvent.loaded / 1024 / 1024).toFixed(1)
79
- if (!started) {
80
- totalMB = (progressEvent.total / 1024 / 1024).toFixed(1)
81
- bar.start(progressEvent.total, 0, {
82
- valueFormatted: '0.0',
83
- totalFormatted: totalMB,
84
- })
85
- started = true
86
- }
87
- bar.update(progressEvent.loaded, {
88
- valueFormatted: loadedMB,
89
- totalFormatted: totalMB,
90
- })
91
- }
92
- },
93
- })
94
- bar.stop()
95
-
96
- fs.writeFileSync(tempPath, response.data)
97
- spinner.start(chalk.green('下载完成,正在解压...'))
98
- } catch (e) {
99
- bar.stop()
100
- throw new Error(`下载失败: ${e.message}`)
101
- }
102
-
103
- // 4. 解压文件
104
- try {
105
- if (isWin) {
106
- // ZIP 解压
107
- const zip = new AdmZip(tempPath)
108
- const zipEntries = zip.getEntries()
109
- // 查找可执行文件(通常名字包含 mihoo 或 name)
110
- const entry = zipEntries.find(e => e.entryName.includes(name) || e.entryName.endsWith('.exe'))
111
- if (!entry) {
112
- throw new Error('压缩包中未找到可执行文件')
113
- }
114
-
115
- // 1. 解压文件 (使用原文件名)
116
- zip.extractEntryTo(entry, targetDir, false, true)
117
-
118
- // 2. 重命名为目标文件名
119
- const extractedPath = path.join(targetDir, entry.entryName)
120
- if (targetBinName !== entry.entryName) {
121
- // 确保目标文件不存在
122
- if (fs.existsSync(targetBinPath)) {
123
- fs.unlinkSync(targetBinPath)
124
- }
125
- fs.renameSync(extractedPath, targetBinPath)
126
- }
127
- } else {
128
- // GZ 解压
129
- const fileContents = fs.readFileSync(tempPath)
130
- const unzipped = zlib.gunzipSync(fileContents)
131
- fs.writeFileSync(targetBinPath, unzipped)
132
- }
133
- } catch (e) {
134
- spinner.fail(chalk.red(`解压失败: ${e.message}`))
135
- throw new Error(`解压失败: ${e.message}`)
136
- } finally {
137
- // 清理临时文件
138
- fs.unlinkSync(tempPath)
139
- spinner.succeed(chalk.green(`解压完成: ${targetBinPath}`))
140
- }
141
-
142
- // 5. 设置权限
143
- if (!isWin) {
144
- spinner.start('正在设置执行权限...')
145
- fs.chmodSync(targetBinPath, 0o755)
146
- spinner.succeed(chalk.green('设置执行权限完成'))
147
- }
148
-
149
- return targetBinPath
150
- }
151
-
152
- export function killClashProcess() {
153
- try {
154
- if (process.platform === 'win32') {
155
- execSync('taskkill /F /IM clash-kit.exe', { stdio: 'ignore' })
156
- } else {
157
- execSync('pkill -f clash-kit', { stdio: 'ignore' })
158
- }
159
- return true
160
- } catch (e) {
161
- return false
162
- }
163
- }
164
-
165
- function testDownload() {
166
- // 测试下载功能
167
- const rootDir = path.join(__dirname, '..')
168
- downloadClash(rootDir)
169
- .then(binPath => {
170
- console.log(`Mihomo 内核已下载并解压到: ${binPath}`)
171
- })
172
- .catch(err => {
173
- console.error(`下载失败: ${err.message}`)
174
- })
175
- }
176
-
177
- // 如果直接运行此文件,则执行测试
178
- if (import.meta.url === `file://${process.argv[1]}`) testDownload()
1
+ import axios from 'axios'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+ import os from 'os'
5
+ import zlib from 'zlib'
6
+ import AdmZip from 'adm-zip'
7
+ import { fileURLToPath } from 'url'
8
+ import ora from 'ora'
9
+ import cliProgress from 'cli-progress'
10
+ import chalk from 'chalk'
11
+ import { execSync } from 'child_process'
12
+
13
+ const __filename = fileURLToPath(import.meta.url)
14
+ const __dirname = path.dirname(__filename)
15
+
16
+ const MIHOMO_VERSION_URL = 'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
17
+
18
+ const PLATFORM_MAP = {
19
+ 'win32-x64': 'mihomo-windows-amd64-compatible',
20
+ 'win32-ia32': 'mihomo-windows-386',
21
+ 'win32-arm64': 'mihomo-windows-arm64',
22
+ 'darwin-x64': 'mihomo-darwin-amd64-compatible',
23
+ 'darwin-arm64': 'mihomo-darwin-arm64',
24
+ 'linux-x64': 'mihomo-linux-amd64-compatible',
25
+ 'linux-arm64': 'mihomo-linux-arm64',
26
+ }
27
+
28
+ export async function downloadClash(targetDir) {
29
+ const platform = os.platform()
30
+ const arch = os.arch()
31
+ const key = `${platform}-${arch}`
32
+ const name = PLATFORM_MAP[key]
33
+
34
+ if (!name) {
35
+ throw new Error(`不支持的平台: ${platform}-${arch}`)
36
+ }
37
+
38
+ // 1. 获取最新版本
39
+ const spinner = ora('正在获取最新 Mihomo 版本信息...').start()
40
+ let version
41
+ try {
42
+ const { data } = await axios.get(MIHOMO_VERSION_URL)
43
+ version = data.trim()
44
+ spinner.succeed(`检测到最新版本: ${version}`)
45
+ } catch (e) {
46
+ spinner.fail(`获取版本信息失败: ${e.message}`)
47
+ throw new Error(`获取版本信息失败: ${e.message}`)
48
+ }
49
+
50
+ // 2. 构建下载 URL
51
+ const isWin = platform === 'win32'
52
+ const urlExt = isWin ? 'zip' : 'gz'
53
+ const downloadUrl = `https://github.com/MetaCubeX/mihomo/releases/download/${version}/${name}-${version}.${urlExt}`
54
+ const tempFileName = `mihomo-temp.${urlExt}`
55
+ const tempPath = path.join(targetDir, tempFileName)
56
+ const targetBinName = `clash-kit${isWin ? '.exe' : ''}`
57
+ const targetBinPath = path.join(targetDir, targetBinName)
58
+
59
+ // 3. 下载文件
60
+ console.log(`正在下载: ${downloadUrl}`)
61
+ const bar = new cliProgress.SingleBar(
62
+ {
63
+ format: '下载进度 | {bar} | {percentage}% | {valueFormatted}/{totalFormatted} MB',
64
+ hideCursor: true,
65
+ },
66
+ cliProgress.Presets.shades_classic
67
+ )
68
+
69
+ try {
70
+ let started = false
71
+ let totalMB = '0.0'
72
+ const response = await axios({
73
+ url: downloadUrl,
74
+ method: 'GET',
75
+ responseType: 'arraybuffer',
76
+ onDownloadProgress: progressEvent => {
77
+ if (progressEvent.total) {
78
+ const loadedMB = (progressEvent.loaded / 1024 / 1024).toFixed(1)
79
+ if (!started) {
80
+ totalMB = (progressEvent.total / 1024 / 1024).toFixed(1)
81
+ bar.start(progressEvent.total, 0, {
82
+ valueFormatted: '0.0',
83
+ totalFormatted: totalMB,
84
+ })
85
+ started = true
86
+ }
87
+ bar.update(progressEvent.loaded, {
88
+ valueFormatted: loadedMB,
89
+ totalFormatted: totalMB,
90
+ })
91
+ }
92
+ },
93
+ })
94
+ bar.stop()
95
+
96
+ fs.writeFileSync(tempPath, response.data)
97
+ spinner.start(chalk.green('下载完成,正在解压...'))
98
+ } catch (e) {
99
+ bar.stop()
100
+ throw new Error(`下载失败: ${e.message}`)
101
+ }
102
+
103
+ // 4. 解压文件
104
+ try {
105
+ if (isWin) {
106
+ // ZIP 解压
107
+ const zip = new AdmZip(tempPath)
108
+ const zipEntries = zip.getEntries()
109
+ // 查找可执行文件(通常名字包含 mihoo 或 name)
110
+ const entry = zipEntries.find(e => e.entryName.includes(name) || e.entryName.endsWith('.exe'))
111
+ if (!entry) {
112
+ throw new Error('压缩包中未找到可执行文件')
113
+ }
114
+
115
+ // 1. 解压文件 (使用原文件名)
116
+ zip.extractEntryTo(entry, targetDir, false, true)
117
+
118
+ // 2. 重命名为目标文件名
119
+ const extractedPath = path.join(targetDir, entry.entryName)
120
+ if (targetBinName !== entry.entryName) {
121
+ // 确保目标文件不存在
122
+ if (fs.existsSync(targetBinPath)) {
123
+ fs.unlinkSync(targetBinPath)
124
+ }
125
+ fs.renameSync(extractedPath, targetBinPath)
126
+ }
127
+ } else {
128
+ // GZ 解压
129
+ const fileContents = fs.readFileSync(tempPath)
130
+ const unzipped = zlib.gunzipSync(fileContents)
131
+ fs.writeFileSync(targetBinPath, unzipped)
132
+ }
133
+ } catch (e) {
134
+ spinner.fail(chalk.red(`解压失败: ${e.message}`))
135
+ throw new Error(`解压失败: ${e.message}`)
136
+ } finally {
137
+ // 清理临时文件
138
+ fs.unlinkSync(tempPath)
139
+ spinner.succeed(chalk.green(`解压完成: ${targetBinPath}`))
140
+ }
141
+
142
+ // 5. 设置权限
143
+ if (!isWin) {
144
+ spinner.start('正在设置执行权限...')
145
+ fs.chmodSync(targetBinPath, 0o755)
146
+ spinner.succeed(chalk.green('设置执行权限完成'))
147
+ }
148
+
149
+ return targetBinPath
150
+ }
151
+
152
+ export function getClashProcessInfo() {
153
+ try {
154
+ let pid
155
+ if (process.platform === 'win32') {
156
+ const command = "tasklist | findstr clash-kit.exe"
157
+ const output = execSync(command, { encoding: 'utf-8' })
158
+ const match = output.match(/(\d+)/)
159
+ pid = match ? match[0] : null
160
+ } else {
161
+ const command = "pgrep -f clash-kit"
162
+ pid = execSync(command, { encoding: 'utf-8' }).trim()
163
+ }
164
+ return pid ? { pid } : null
165
+ } catch (e) {
166
+ // pgrep/findstr throws an error if no process is found
167
+ return null
168
+ }
169
+ }
170
+
171
+ export function killClashProcess() {
172
+ try {
173
+ if (process.platform === 'win32') {
174
+ execSync('taskkill /F /IM clash-kit.exe', { stdio: 'ignore' })
175
+ } else {
176
+ execSync('pkill -f clash-kit', { stdio: 'ignore' })
177
+ }
178
+ return true
179
+ } catch (e) {
180
+ return false
181
+ }
182
+ }
183
+
184
+ function testDownload() {
185
+ // 测试下载功能
186
+ const rootDir = path.join(__dirname, '..')
187
+ downloadClash(rootDir)
188
+ .then(binPath => {
189
+ console.log(`Mihomo 内核已下载并解压到: ${binPath}`)
190
+ })
191
+ .catch(err => {
192
+ console.error(`下载失败: ${err.message}`)
193
+ })
194
+ }
195
+
196
+ // 如果直接运行此文件,则执行测试
197
+ if (import.meta.url === `file://${process.argv[1]}`) testDownload()