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.
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')
156
- } else {
157
- execSync('pkill -f clash-kit')
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()
package/lib/port.js CHANGED
@@ -1,115 +1,115 @@
1
- import net from 'net'
2
- import { execSync } from 'child_process'
3
- import os from 'os'
4
-
5
- /**
6
- * 检查端口是否被占用 (true = 空闲, false = 被占用)
7
- * @param {number} port
8
- * @returns {Promise<boolean>}
9
- */
10
- export function isPortOpen(port) {
11
- return new Promise((resolve) => {
12
- const server = net.createServer()
13
- server.once('error', (err) => {
14
- resolve(false) // 端口被占用
15
- })
16
- server.once('listening', () => {
17
- server.close()
18
- resolve(true) // 端口可用
19
- })
20
- server.listen(port, '127.0.0.1')
21
- })
22
- }
23
-
24
- /**
25
- * 获取占用端口的进程信息
26
- * @param {number} port
27
- * @returns {string|null} 进程名称(PID),例如 "mihomo (PID: 1234)"
28
- */
29
- export function getPortOccupier(port) {
30
- try {
31
- const platform = os.platform()
32
- if (platform === 'win32') {
33
- // Windows 实现
34
- try {
35
- const output = execSync(`netstat -ano | findstr :${port}`).toString()
36
- const lines = output.trim().split('\n')
37
- if(lines.length > 0) {
38
- const parts = lines[0].trim().split(/\s+/)
39
- const pid = parts[parts.length - 1]
40
- try {
41
- const tasklist = execSync(`tasklist /fi "pid eq ${pid}" /fo csv /nh`).toString().trim()
42
- // "Image Name","PID","Session Name","Session#","Mem Usage"
43
- // "node.exe","24424","Console","1","38,824 K"
44
- const match = tasklist.match(/^"([^"]+)"/);
45
- if (match) return `${match[1]} (PID: ${pid})`
46
- } catch (e) {}
47
- return `PID: ${pid}`
48
- }
49
- } catch (e) { return null }
50
- } else {
51
- // macOS / Linux
52
- // lsof output format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
53
- const output = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n`).toString().trim()
54
- const lines = output.split('\n')
55
- if (lines.length > 1) {
56
- const parts = lines[1].trim().split(/\s+/)
57
- return `${parts[0]} (PID: ${parts[1]})`
58
- }
59
- }
60
- } catch (e) {
61
- // lsof 找不到时会返回非 0 退出码,抛出错误
62
- return null
63
- }
64
- return null
65
- }
66
-
67
- /**
68
- * 寻找可用端口
69
- * @param {number} startPort
70
- * @returns {Promise<number>}
71
- */
72
- export function findAvailablePort(startPort) {
73
- return new Promise((resolve, reject) => {
74
- const server = net.createServer()
75
- server.on('error', (err) => {
76
- if (startPort <= 65535) {
77
- // 核心逻辑:如果报错,递归尝试 +1 的端口
78
- resolve(findAvailablePort(startPort + 1))
79
- } else {
80
- reject(err)
81
- }
82
- })
83
- server.on('listening', () => {
84
- server.close(() => {
85
- resolve(startPort)
86
- })
87
- })
88
- server.listen(startPort, '127.0.0.1')
89
- })
90
- }
91
-
92
- /**
93
- * 从配置值中提取端口号
94
- * @param {string|number} val - 例如 9090, ":9090", "127.0.0.1:9090"
95
- * @returns {number|null}
96
- */
97
- export function extractPort(val) {
98
- if (!val) return null
99
- if (typeof val === 'number') return val
100
- if (typeof val === 'string') {
101
- // 移除空白
102
- val = val.trim()
103
- // 如果是纯数字字符串
104
- if (/^\d+$/.test(val)) return parseInt(val, 10)
105
-
106
- // 如果包含冒号,取最后一部分
107
- if (val.includes(':')) {
108
- const parts = val.split(':')
109
- const lastPart = parts[parts.length - 1]
110
- const port = parseInt(lastPart, 10)
111
- return isNaN(port) ? null : port
112
- }
113
- }
114
- return null
115
- }
1
+ import net from 'net'
2
+ import { execSync } from 'child_process'
3
+ import os from 'os'
4
+
5
+ /**
6
+ * 检查端口是否被占用 (true = 空闲, false = 被占用)
7
+ * @param {number} port
8
+ * @returns {Promise<boolean>}
9
+ */
10
+ export function isPortOpen(port) {
11
+ return new Promise((resolve) => {
12
+ const server = net.createServer()
13
+ server.once('error', (err) => {
14
+ resolve(false) // 端口被占用
15
+ })
16
+ server.once('listening', () => {
17
+ server.close()
18
+ resolve(true) // 端口可用
19
+ })
20
+ server.listen(port, '127.0.0.1')
21
+ })
22
+ }
23
+
24
+ /**
25
+ * 获取占用端口的进程信息
26
+ * @param {number} port
27
+ * @returns {string|null} 进程名称(PID),例如 "mihomo (PID: 1234)"
28
+ */
29
+ export function getPortOccupier(port) {
30
+ try {
31
+ const platform = os.platform()
32
+ if (platform === 'win32') {
33
+ // Windows 实现
34
+ try {
35
+ const output = execSync(`netstat -ano | findstr :${port}`).toString()
36
+ const lines = output.trim().split('\n')
37
+ if(lines.length > 0) {
38
+ const parts = lines[0].trim().split(/\s+/)
39
+ const pid = parts[parts.length - 1]
40
+ try {
41
+ const tasklist = execSync(`tasklist /fi "pid eq ${pid}" /fo csv /nh`).toString().trim()
42
+ // "Image Name","PID","Session Name","Session#","Mem Usage"
43
+ // "node.exe","24424","Console","1","38,824 K"
44
+ const match = tasklist.match(/^"([^"]+)"/);
45
+ if (match) return `${match[1]} (PID: ${pid})`
46
+ } catch (e) {}
47
+ return `PID: ${pid}`
48
+ }
49
+ } catch (e) { return null }
50
+ } else {
51
+ // macOS / Linux
52
+ // lsof output format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
53
+ const output = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n`).toString().trim()
54
+ const lines = output.split('\n')
55
+ if (lines.length > 1) {
56
+ const parts = lines[1].trim().split(/\s+/)
57
+ return `${parts[0]} (PID: ${parts[1]})`
58
+ }
59
+ }
60
+ } catch (e) {
61
+ // lsof 找不到时会返回非 0 退出码,抛出错误
62
+ return null
63
+ }
64
+ return null
65
+ }
66
+
67
+ /**
68
+ * 寻找可用端口
69
+ * @param {number} startPort
70
+ * @returns {Promise<number>}
71
+ */
72
+ export function findAvailablePort(startPort) {
73
+ return new Promise((resolve, reject) => {
74
+ const server = net.createServer()
75
+ server.on('error', (err) => {
76
+ if (startPort <= 65535) {
77
+ // 核心逻辑:如果报错,递归尝试 +1 的端口
78
+ resolve(findAvailablePort(startPort + 1))
79
+ } else {
80
+ reject(err)
81
+ }
82
+ })
83
+ server.on('listening', () => {
84
+ server.close(() => {
85
+ resolve(startPort)
86
+ })
87
+ })
88
+ server.listen(startPort, '127.0.0.1')
89
+ })
90
+ }
91
+
92
+ /**
93
+ * 从配置值中提取端口号
94
+ * @param {string|number} val - 例如 9090, ":9090", "127.0.0.1:9090"
95
+ * @returns {number|null}
96
+ */
97
+ export function extractPort(val) {
98
+ if (!val) return null
99
+ if (typeof val === 'number') return val
100
+ if (typeof val === 'string') {
101
+ // 移除空白
102
+ val = val.trim()
103
+ // 如果是纯数字字符串
104
+ if (/^\d+$/.test(val)) return parseInt(val, 10)
105
+
106
+ // 如果包含冒号,取最后一部分
107
+ if (val.includes(':')) {
108
+ const parts = val.split(':')
109
+ const lastPart = parts[parts.length - 1]
110
+ const port = parseInt(lastPart, 10)
111
+ return isNaN(port) ? null : port
112
+ }
113
+ }
114
+ return null
115
+ }