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/index.js CHANGED
@@ -1,218 +1,204 @@
1
- import { spawn, execSync } from 'child_process'
2
- import path from 'path'
3
- import fs from 'fs'
4
- import chalk from 'chalk'
5
- import axios from 'axios'
6
- import ora from 'ora'
7
- import YAML from 'yaml'
8
- import { fileURLToPath } from 'url'
9
- import { getApiBase, getProxyPort } from './lib/api.js'
10
- import * as sysproxy from './lib/sysproxy.js'
11
- import * as tun from './lib/tun.js'
12
- import { isPortOpen, extractPort, getPortOccupier } from './lib/port.js'
13
- import { killClashProcess } from './lib/kernel.js'
14
-
15
- const __filename = fileURLToPath(import.meta.url)
16
- const __dirname = path.dirname(__filename)
17
-
18
- // ---------------- 配置项 ----------------
19
- export const CLASH_BIN_PATH = path.join(__dirname, process.platform === 'win32' ? 'clash-kit.exe' : 'clash-kit') // 解压后的二进制文件路径
20
- export const CLASH_CONFIG_PATH = path.join(__dirname, 'config.yaml') // 配置文件路径
21
-
22
- async function checkPorts() {
23
- try {
24
- if (fs.existsSync(CLASH_CONFIG_PATH)) {
25
- const configContent = fs.readFileSync(CLASH_CONFIG_PATH, 'utf8')
26
- const config = YAML.parse(configContent)
27
-
28
- const checks = [
29
- { key: 'mixed-port', name: 'Mixed Port' },
30
- { key: 'port', name: 'HTTP Port' },
31
- { key: 'socks-port', name: 'SOCKS Port' },
32
- { key: 'external-controller', name: 'External Controller' },
33
- ]
34
-
35
- for (const check of checks) {
36
- const val = config[check.key]
37
- const port = extractPort(val)
38
- if (port) {
39
- const isOpen = await isPortOpen(port)
40
- if (!isOpen) {
41
- const occupier = getPortOccupier(port)
42
- const occupierInfo = occupier ? ` ( ${occupier} 占用)` : ''
43
-
44
- console.error(chalk.red(`\n启动失败: 端口 ${port} (${check.name}) 已被占用${occupierInfo}`))
45
- console.error(chalk.yellow(`请检查是否有其他代理软件正在运行,或修改 config.yaml 中的 ${check.key} \n`))
46
-
47
- if (!occupierInfo) {
48
- console.error(`占用进程未知,可能是权限不足或系统进程`)
49
- console.error(chalk.yellow(`提示: 可尝试使用 'sudo lsof -i :${port}' 手动查看端口占用情况`))
50
- }
51
- process.exit(1)
52
- }
53
- }
54
- }
55
- }
56
- } catch (e) {
57
- console.error(chalk.yellow('警告: 端口检查预检失败,将尝试直接启动内核:', e.message))
58
- }
59
- }
60
-
61
- // ---------------- 启动 Clash.Meta 进程 ----------------
62
- async function startClash() {
63
- // 尝试停止已存在的进程
64
- killClashProcess()
65
- // 稍微等待端口释放,避免 restart 时偶发端口占用报错
66
- await new Promise(resolve => setTimeout(resolve, 500))
67
-
68
- // 检查端口占用 (核心策略:报错/启动失败)
69
- await checkPorts()
70
-
71
- const logPath = path.join(__dirname, 'clash.log')
72
- const logFd = fs.openSync(logPath, 'a')
73
-
74
- const clashProcess = spawn(CLASH_BIN_PATH, ['-f', CLASH_CONFIG_PATH, '-d', __dirname], {
75
- cwd: __dirname,
76
- detached: true,
77
- stdio: ['ignore', logFd, logFd], // 重定向 stdout 和 stderr 到日志文件
78
- })
79
-
80
- clashProcess.on('error', err => {
81
- console.error(`启动 Clash.Meta 失败: ${err.message}`)
82
- process.exit(1)
83
- })
84
-
85
- // 监听子进程退出,方便调试
86
- clashProcess.on('exit', (code, signal) => {
87
- if (code !== 0) {
88
- console.log(`Clash 进程异常退出,代码: ${code}, 信号: ${signal}。请查看 clash.log 获取详情。`)
89
- }
90
- })
91
-
92
- // 解除与父进程的引用,让子进程在后台独立运行
93
- clashProcess.unref()
94
-
95
- return clashProcess
96
- }
97
-
98
- // 清理函数
99
- async function cleanup() {
100
- try {
101
- // 关闭系统代理
102
- await sysproxy.disableSystemProxy()
103
-
104
- // 检查并关闭 TUN 模式
105
- const tunEnabled = await tun.isTunEnabled()
106
- if (tunEnabled) {
107
- await tun.disableTun()
108
- console.log('TUN 模式已关闭')
109
- }
110
-
111
- // 停止 Clash 进程
112
- if (killClashProcess()) {
113
- console.log('Clash 服务已停止')
114
- }
115
- } catch (error) {
116
- console.error('清理过程出错:', error.message)
117
- }
118
- }
119
-
120
- // 注册进程退出处理
121
- function setupExitHandlers() {
122
- // 处理正常退出 (Ctrl+C)
123
- process.on('SIGINT', async () => {
124
- console.log('\n\n正在清理配置并退出...')
125
- await cleanup()
126
- process.exit(0)
127
- })
128
-
129
- // 处理终止信号
130
- process.on('SIGTERM', async () => {
131
- console.log('\n正在清理配置并退出...')
132
- await cleanup()
133
- process.exit(0)
134
- })
135
-
136
- // 处理未捕获的异常
137
- process.on('uncaughtException', async err => {
138
- console.error('未捕获的异常:', err)
139
- await cleanup()
140
- process.exit(1)
141
- })
142
- }
143
-
144
- // 检查服务健康状态
145
- async function checkServiceHealth(apiBase, maxRetries = 20) {
146
- for (let i = 0; i < maxRetries; i++) {
147
- try {
148
- await axios.get(apiBase, { timeout: 1000 })
149
- return true
150
- } catch (e) {
151
- if (e.response) return true // 端口已通 (即使是 401 也可以)
152
- await new Promise(r => setTimeout(r, 200)) // 200ms * 20 = 4s
153
- }
154
- }
155
- return false
156
- }
157
-
158
- export async function main() {
159
- // 检查 clash-kit 二进制文件是否存在
160
- if (!fs.existsSync(CLASH_BIN_PATH)) {
161
- return console.error(chalk.red('\n找不到 Clash.Meta 内核文件,请先运行 clash init 命令初始化内核!\n'))
162
- }
163
- // 检查配置文件是否存在
164
- if (!fs.existsSync(CLASH_CONFIG_PATH)) {
165
- return console.error(chalk.red('\n找不到配置文件 config.yaml,请先通过 clash sub 命令添加或选择订阅配置!\n'))
166
- }
167
-
168
- // 设置退出处理
169
- setupExitHandlers()
170
-
171
- const clashProcess = await startClash()
172
-
173
- const spinner = ora('正在等待服务启动...').start()
174
- const started = await checkServiceHealth(getApiBase())
175
-
176
- if (!started) {
177
- spinner.fail(chalk.red('启动失败'))
178
- const logPath = path.join(__dirname, 'clash.log')
179
- if (fs.existsSync(logPath)) {
180
- console.log(chalk.yellow('\n------- clash.log (Last 20 lines) -------'))
181
- const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n')
182
- console.log(lines.slice(-20).join('\n'))
183
- console.log(chalk.yellow('-----------------------------------------\n'))
184
- }
185
- try {
186
- process.kill(clashProcess.pid)
187
- } catch (e) {
188
- console.error('停止 Clash 进程时出错:', e)
189
- }
190
- process.exit(1)
191
- }
192
-
193
- spinner.succeed(chalk.green('启动成功'))
194
-
195
- const { http, socks } = getProxyPort()
196
-
197
- if (clashProcess.pid) {
198
- console.log(`进程名称:${chalk.yellow('clash-kit')} PID: ${chalk.yellow(clashProcess.pid)}`)
199
- }
200
-
201
- console.log(``)
202
- console.log(`HTTP Proxy: ${chalk.cyan(`127.0.0.1:${http}`)}`)
203
- console.log(`SOCKS5 Proxy: ${chalk.cyan(`127.0.0.1:${socks}`)}`)
204
- console.log(`API: ${chalk.cyan.underline(getApiBase())}`)
205
- console.log(``)
206
-
207
- console.log(chalk.gray('运行日志文件: ' + path.join(__dirname, 'clash.log')))
208
- console.log(chalk.blue('提示:如需查看运行状态可使用 clash status 命令'))
209
- console.log(chalk.blue('如需停止代理可使用 clash stop 命令'))
210
- console.log(chalk.gray('----------------------------------------'))
211
- console.log(chalk.gray('如需切换系统代理模式可使用 clash sysproxy on/off 命令'))
212
- console.log(chalk.gray('如需切换 TUN 模式可使用 clash tun on/off 命令'))
213
- }
214
-
215
- // 运行脚本
216
- if (process.argv[1] === __filename) {
217
- main()
218
- }
1
+ import { spawn, execSync } from 'child_process'
2
+ import path from 'path'
3
+ import fs from 'fs'
4
+ import chalk from 'chalk'
5
+ import axios from 'axios'
6
+ import ora from 'ora'
7
+ import boxen from 'boxen'
8
+ import YAML from 'yaml'
9
+ import { fileURLToPath } from 'url'
10
+ import { getApiBase, getProxyPort } from './lib/api.js'
11
+ import { status } from './lib/commands/status.js'
12
+ import * as sysproxy from './lib/sysproxy.js'
13
+ import * as tun from './lib/tun.js'
14
+ import { isPortOpen, extractPort, getPortOccupier } from './lib/port.js'
15
+ import { killClashProcess } from './lib/kernel.js'
16
+
17
+ const __filename = fileURLToPath(import.meta.url)
18
+ const __dirname = path.dirname(__filename)
19
+
20
+ // ---------------- 配置项 ----------------
21
+ export const CLASH_BIN_PATH = path.join(__dirname, process.platform === 'win32' ? 'clash-kit.exe' : 'clash-kit') // 解压后的二进制文件路径
22
+ export const CLASH_CONFIG_PATH = path.join(__dirname, 'config.yaml') // 配置文件路径
23
+
24
+ async function checkPorts() {
25
+ try {
26
+ if (fs.existsSync(CLASH_CONFIG_PATH)) {
27
+ const configContent = fs.readFileSync(CLASH_CONFIG_PATH, 'utf8')
28
+ const config = YAML.parse(configContent)
29
+
30
+ const checks = [
31
+ { key: 'mixed-port', name: 'Mixed Port' },
32
+ { key: 'port', name: 'HTTP Port' },
33
+ { key: 'socks-port', name: 'SOCKS Port' },
34
+ { key: 'external-controller', name: 'External Controller' },
35
+ ]
36
+
37
+ for (const check of checks) {
38
+ const val = config[check.key]
39
+ const port = extractPort(val)
40
+ if (port) {
41
+ const isOpen = await isPortOpen(port)
42
+ if (!isOpen) {
43
+ const occupier = getPortOccupier(port)
44
+ const occupierInfo = occupier ? ` (${occupier} 占用)` : ''
45
+
46
+ console.error(chalk.red(`\n启动失败: 端口 ${port} (${check.name}) 已被占用${occupierInfo}`))
47
+ console.error(chalk.yellow(`请检查是否有其他代理软件正在运行,或修改 config.yaml 中的 ${check.key} \n`))
48
+
49
+ if (!occupierInfo) {
50
+ console.error(`占用进程未知,可能是权限不足或系统进程`)
51
+ console.error(chalk.yellow(`提示: 可尝试使用 'sudo lsof -i :${port}' 手动查看端口占用情况`))
52
+ }
53
+ process.exit(1)
54
+ }
55
+ }
56
+ }
57
+ }
58
+ } catch (e) {
59
+ console.error(chalk.yellow('警告: 端口检查预检失败,将尝试直接启动内核:', e.message))
60
+ }
61
+ }
62
+
63
+ // ---------------- 启动 Clash.Meta 进程 ----------------
64
+ async function startClash() {
65
+ // 尝试停止已存在的进程
66
+ killClashProcess()
67
+ // 稍微等待端口释放,避免 restart 时偶发端口占用报错
68
+ await new Promise(resolve => setTimeout(resolve, 500))
69
+
70
+ // 检查端口占用 (核心策略:报错/启动失败)
71
+ await checkPorts()
72
+
73
+ const logPath = path.join(__dirname, 'clash.log')
74
+ const logFd = fs.openSync(logPath, 'a')
75
+
76
+ const clashProcess = spawn(CLASH_BIN_PATH, ['-f', CLASH_CONFIG_PATH, '-d', __dirname], {
77
+ cwd: __dirname,
78
+ detached: true,
79
+ stdio: ['ignore', logFd, logFd], // 重定向 stdout 和 stderr 到日志文件
80
+ })
81
+
82
+ clashProcess.on('error', err => {
83
+ console.error(`启动 Clash.Meta 失败: ${err.message}`)
84
+ process.exit(1)
85
+ })
86
+
87
+ // 监听子进程退出,方便调试
88
+ clashProcess.on('exit', (code, signal) => {
89
+ if (code !== 0) {
90
+ console.log(`Clash 进程异常退出,代码: ${code}, 信号: ${signal}。请查看 clash.log 获取详情。`)
91
+ }
92
+ })
93
+
94
+ // 解除与父进程的引用,让子进程在后台独立运行
95
+ clashProcess.unref()
96
+
97
+ return clashProcess
98
+ }
99
+
100
+ // 清理函数
101
+ async function cleanup() {
102
+ try {
103
+ // 关闭系统代理
104
+ await sysproxy.disableSystemProxy()
105
+
106
+ // 检查并关闭 TUN 模式
107
+ const tunEnabled = await tun.isTunEnabled()
108
+ if (tunEnabled) {
109
+ await tun.disableTun()
110
+ console.log('TUN 模式已关闭')
111
+ }
112
+
113
+ // 停止 Clash 进程
114
+ if (killClashProcess()) {
115
+ console.log('Clash 服务已停止')
116
+ }
117
+ } catch (error) {
118
+ console.error('清理过程出错:', error.message)
119
+ }
120
+ }
121
+
122
+ // 注册进程退出处理
123
+ function setupExitHandlers() {
124
+ // 处理正常退出 (Ctrl+C)
125
+ process.on('SIGINT', async () => {
126
+ console.log('\n\n正在清理配置并退出...')
127
+ await cleanup()
128
+ process.exit(0)
129
+ })
130
+
131
+ // 处理终止信号
132
+ process.on('SIGTERM', async () => {
133
+ console.log('\n正在清理配置并退出...')
134
+ await cleanup()
135
+ process.exit(0)
136
+ })
137
+
138
+ // 处理未捕获的异常
139
+ process.on('uncaughtException', async err => {
140
+ console.error('未捕获的异常:', err)
141
+ await cleanup()
142
+ process.exit(1)
143
+ })
144
+ }
145
+
146
+ // 检查服务健康状态
147
+ async function checkServiceHealth(apiBase, maxRetries = 20) {
148
+ for (let i = 0; i < maxRetries; i++) {
149
+ try {
150
+ await axios.get(apiBase, { timeout: 1000 })
151
+ return true
152
+ } catch (e) {
153
+ if (e.response) return true // 端口已通 (即使是 401 也可以)
154
+ await new Promise(r => setTimeout(r, 200)) // 200ms * 20 = 4s
155
+ }
156
+ }
157
+ return false
158
+ }
159
+
160
+ export async function main() {
161
+ // 检查 clash-kit 二进制文件是否存在
162
+ if (!fs.existsSync(CLASH_BIN_PATH)) {
163
+ return console.error(chalk.red('\n找不到 Clash.Meta 内核文件,请先运行 clash init 命令初始化内核!\n'))
164
+ }
165
+ // 检查配置文件是否存在
166
+ if (!fs.existsSync(CLASH_CONFIG_PATH)) {
167
+ return console.error(chalk.red('\n找不到配置文件 config.yaml,请先通过 clash sub 命令添加或选择订阅配置!\n'))
168
+ }
169
+
170
+ // 设置退出处理
171
+ setupExitHandlers()
172
+
173
+ const clashProcess = await startClash()
174
+
175
+ const spinner = ora('正在等待服务启动...').start()
176
+ const started = await checkServiceHealth(getApiBase())
177
+
178
+ if (!started) {
179
+ spinner.fail(chalk.red('启动失败'))
180
+ const logPath = path.join(__dirname, 'clash.log')
181
+ if (fs.existsSync(logPath)) {
182
+ console.log(chalk.yellow('\n------- clash.log (Last 20 lines) -------'))
183
+ const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n')
184
+ console.log(lines.slice(-20).join('\n'))
185
+ console.log(chalk.yellow('-----------------------------------------\n'))
186
+ }
187
+ try {
188
+ process.kill(clashProcess.pid)
189
+ } catch (e) {
190
+ console.error('停止 Clash 进程时出错:', e)
191
+ }
192
+ process.exit(1)
193
+ }
194
+
195
+ spinner.succeed(chalk.green('启动成功'))
196
+
197
+ // 调用 status 命令来打印完整的状态信息
198
+ await status()
199
+ }
200
+
201
+ // 运行脚本
202
+ if (process.argv[1] === __filename) {
203
+ main()
204
+ }