clash-kit 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -44,10 +44,13 @@ ck init
44
44
 
45
45
  ```bash
46
46
  # 启动 Clash 代理服务
47
- ck start
47
+ ck start # 或者 ck on
48
48
 
49
49
  # 启动并自动开启系统代理
50
50
  ck start -s
51
+
52
+ # 关闭服务并关闭系统代理
53
+ ck stop # 或者 ck off
51
54
  ```
52
55
 
53
56
  ### 4. 添加订阅
@@ -56,18 +59,24 @@ ck start -s
56
59
  # 交互式管理订阅(添加、切换、删除等)【推荐使用这种方式来管理订阅】
57
60
  ck sub
58
61
 
62
+ # 列出所有订阅
63
+ ck sub -l
64
+
59
65
  # 手动添加订阅
60
66
  ck sub -a "https://example.com/subscribe?token=xxx" -n "abcName"
61
67
  ```
62
68
 
63
- ### 4. 节点测速与切换
69
+ ### 4. 节点切换(自动测速)
64
70
 
65
- ```bash
66
- # 测速
67
- ck test
71
+ 进入交互式界面,自动对当前节点组进行并发测速,并展示带有即时延迟数据的节点列表供选择。
68
72
 
69
- # 切换节点
70
- ck proxy
73
+ ```bash
74
+ # 切换节点 (支持别名: node, proxy, switch)
75
+ ck use
76
+ # 或者
77
+ ck switch
78
+ # 或者
79
+ ck node
71
80
  ```
72
81
 
73
82
  ### 5. 更多功能
@@ -76,27 +85,34 @@ ck proxy
76
85
  # 查看状态
77
86
  ck status
78
87
 
88
+ # 节点并发测速 (仅测速不切换,支持别名: test, ls, t)
89
+ ck list
90
+
91
+ # 设置系统代理
92
+ ck sys on
93
+ ck sys off
94
+
79
95
  # 开启 TUN 模式 (需要 sudo 权限)
80
- sudo ck tun on
96
+ ck tun on # 开启
97
+ ck tun off # 关闭
81
98
  ```
82
99
 
83
100
  ## 命令详解
84
101
 
85
- | 命令 (别名) | 说明 | 示例 |
86
- | --------------------- | -------------------------- | ------------------------------- |
87
- | `ck init` (`i`) | 初始化内核及权限 | `ck i` |
88
- | `ck start` (`on`) | 启动 Clash 服务 | `ck on -s` (启动并设置系统代理) |
89
- | `ck stop` (`off`) | 停止服务并关闭代理 | `ck off` |
90
- | `ck status` (`st`) | 查看运行状态及当前节点延迟 | `ck st` |
91
- | `ck sub` (`s`) | 管理订阅(交互式) | `ck s` |
92
- | `ck sub -a <url>` | 添加订阅 | `ck s -a "http..." -n "pro"` |
93
- | `ck sub -u <name>` | 切换订阅 | `ck s -u "pro"` |
94
- | `ck sub -l` | 列出所有订阅 | `ck s -l` |
95
- | `ck proxy` (`p`) | 切换节点(交互式) | `ck p` |
96
- | `ck test` (`t`) | 节点并发测速 | `ck t` |
97
- | `ck sysproxy` (`sys`) | 设置系统代理 (on/off) | `ck sys on` |
98
- | `ck tun` | 设置 TUN 模式 (on/off) | `sudo ck tun on` |
99
-
102
+ | 命令 (别名) | 说明 | 示例 |
103
+ | ----------------------------- | -------------------------- | --------------------------------------- |
104
+ | `ck init` | 初始化内核及权限 | `ck init` |
105
+ | `ck start` (`on`) | 启动 Clash 服务 | `ck on` `ck on -s` (启动并设置系统代理) |
106
+ | `ck stop` (`off`) | 停止服务并关闭代理 | `ck off` |
107
+ | `ck status` (`info`, `view`) | 查看运行状态及当前节点延迟 | `ck status` |
108
+ | `ck sysproxy` (`sys`) | 设置系统代理 | `ck sys on` / `ck sys off` |
109
+ | `ck tun` | 设置 TUN 模式 (需要 sudo) | `ck tun on` |
110
+ | `ck sub` | 管理订阅(交互式)【推荐】 | `ck sub` |
111
+ | `ck sub -l` | 列出所有订阅 | `ck sub -l` |
112
+ | `ck sub -a <url>` | 添加订阅 | `ck sub -a "http..." -n "pro"` |
113
+ | `ck sub -u <name>` | 切换订阅 | `ck sub -u "pro"` |
114
+ | `ck use` (`node`, `switch`) | 切换节点 (自动测速) | `ck use` / `ck node` |
115
+ | `ck list` (`ls`, `test`, `t`) | 节点测速列表 (不切换) | `ck list` / `ck test` |
100
116
 
101
117
  ## License
102
118
 
package/bin/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander'
4
+ import { createRequire } from 'module'
4
5
  import { init } from '../lib/commands/init.js'
5
6
  import { start } from '../lib/commands/start.js'
6
7
  import { stop } from '../lib/commands/stop.js'
@@ -11,9 +12,12 @@ import { manageSub } from '../lib/commands/sub.js'
11
12
  import { proxy } from '../lib/commands/proxy.js'
12
13
  import { test } from '../lib/commands/test.js'
13
14
 
15
+ const require = createRequire(import.meta.url)
16
+ const pkg = require('../package.json')
17
+
14
18
  const program = new Command()
15
19
 
16
- program.name('clash').alias('ck').description('Clash CLI 管理工具 (Alias: ck)').version('1.0.0')
20
+ program.name('clash').alias('ck').description('Clash CLI 管理工具 (Alias: ck)').version(pkg.version, '-v, --version')
17
21
 
18
22
  // 初始化 clash 内核
19
23
  program
@@ -23,10 +27,15 @@ program
23
27
  .action(init)
24
28
 
25
29
  // 启动 clash 服务
26
- program.command('start').description('启动 Clash 服务').option('-s, --sysproxy', '启动后自动开启系统代理').action(start)
30
+ program
31
+ .command('start')
32
+ .alias('on')
33
+ .description('启动 Clash 服务')
34
+ .option('-s, --sysproxy', '启动后自动开启系统代理')
35
+ .action(start)
27
36
 
28
37
  // 停止 clash 服务
29
- program.command('stop').description('停止 Clash 服务').action(stop)
38
+ program.command('stop').alias('off').description('停止 Clash 服务').action(stop)
30
39
 
31
40
  // 设置系统代理
32
41
  program
@@ -40,7 +49,7 @@ program
40
49
  program.command('tun').description('设置 TUN 模式 (可能需要提权)').argument('[action]', 'on 或 off').action(setTun)
41
50
 
42
51
  // 查看 clash 状态
43
- program.command('status').alias('st').description('查看 Clash 运行状态').action(status)
52
+ program.command('status').alias('st').alias('view').alias('info').description('查看 Clash 运行状态').action(status)
44
53
 
45
54
  // 管理订阅
46
55
  program
@@ -53,9 +62,24 @@ program
53
62
  .action(manageSub)
54
63
 
55
64
  // 切换节点
56
- program.command('proxy').alias('p').description('切换节点').action(proxy)
65
+ program
66
+ .command('use')
67
+ .aliases(['node', 'proxy', 'switch'])
68
+ .description('切换节点 (别名: node, proxy, switch)')
69
+ .action(proxy)
70
+
71
+ // 列出所有节点,并测速
72
+ program
73
+ .command('list')
74
+ .alias('ls')
75
+ .alias('test')
76
+ .alias('t')
77
+ .description('节点测速 (别名: list, ls, test, t) ')
78
+ .action(test)
57
79
 
58
- // 节点测速
59
- program.command('test').alias('t').description('节点测速').action(test)
80
+ // Support -V for version
81
+ if (process.argv.includes('-V')) {
82
+ process.argv[process.argv.indexOf('-V')] = '-v'
83
+ }
60
84
 
61
85
  program.parse(process.argv)
package/index.js CHANGED
@@ -2,27 +2,75 @@ import { spawn, execSync } from 'child_process'
2
2
  import path from 'path'
3
3
  import fs from 'fs'
4
4
  import chalk from 'chalk'
5
+ import axios from 'axios'
6
+ import ora from 'ora'
7
+ import YAML from 'yaml'
5
8
  import { fileURLToPath } from 'url'
6
9
  import { getApiBase, getProxyPort } from './lib/api.js'
7
10
  import * as sysproxy from './lib/sysproxy.js'
8
11
  import * as tun from './lib/tun.js'
12
+ import { isPortOpen, extractPort, getPortOccupier } from './lib/port.js'
9
13
 
10
14
  const __filename = fileURLToPath(import.meta.url)
11
15
  const __dirname = path.dirname(__filename)
12
16
 
13
- // ---------------- 1. 配置项 ----------------
17
+ // ---------------- 配置项 ----------------
14
18
  const CLASH_BIN_PATH = path.join(__dirname, 'clash-kit') // 解压后的二进制文件路径
15
19
  const CLASH_CONFIG_PATH = path.join(__dirname, 'config.yaml') // 配置文件路径
16
20
 
17
- // ---------------- 2. 启动 Clash.Meta 进程 ----------------
18
- function startClash() {
21
+ async function checkPorts() {
22
+ try {
23
+ if (fs.existsSync(CLASH_CONFIG_PATH)) {
24
+ const configContent = fs.readFileSync(CLASH_CONFIG_PATH, 'utf8')
25
+ const config = YAML.parse(configContent)
26
+
27
+ const checks = [
28
+ { key: 'mixed-port', name: 'Mixed Port' },
29
+ { key: 'port', name: 'HTTP Port' },
30
+ { key: 'socks-port', name: 'SOCKS Port' },
31
+ { key: 'external-controller', name: 'External Controller' },
32
+ ]
33
+
34
+ for (const check of checks) {
35
+ const val = config[check.key]
36
+ const port = extractPort(val)
37
+ if (port) {
38
+ const isOpen = await isPortOpen(port)
39
+ if (!isOpen) {
40
+ const occupier = getPortOccupier(port)
41
+ const occupierInfo = occupier ? ` (被 ${occupier} 占用)` : ''
42
+
43
+ console.error(chalk.red(`\n启动失败: 端口 ${port} (${check.name}) 已被占用${occupierInfo}`))
44
+ console.error(chalk.yellow(`请检查是否有其他代理软件正在运行,或修改 config.yaml 中的 ${check.key} \n`))
45
+
46
+ if (!occupierInfo) {
47
+ console.error(`占用进程未知,可能是权限不足或系统进程`)
48
+ console.error(chalk.yellow(`提示: 可尝试使用 'sudo lsof -i :${port}' 手动查看端口占用情况`))
49
+ }
50
+ process.exit(1)
51
+ }
52
+ }
53
+ }
54
+ }
55
+ } catch (e) {
56
+ console.error(chalk.yellow('警告: 端口检查预检失败,将尝试直接启动内核:', e.message))
57
+ }
58
+ }
59
+
60
+ // ---------------- 启动 Clash.Meta 进程 ----------------
61
+ async function startClash() {
19
62
  // 尝试停止已存在的进程
20
63
  try {
21
64
  execSync('pkill -f clash-kit')
65
+ // 稍微等待端口释放,避免 restart 时偶发端口占用报错
66
+ await new Promise(resolve => setTimeout(resolve, 500))
22
67
  } catch (e) {
23
68
  // 忽略错误,说明没有运行中的进程
24
69
  }
25
70
 
71
+ // 检查端口占用 (核心策略:报错/启动失败)
72
+ await checkPorts()
73
+
26
74
  const logPath = path.join(__dirname, 'clash.log')
27
75
  const logFd = fs.openSync(logPath, 'a')
28
76
 
@@ -50,7 +98,7 @@ function startClash() {
50
98
  return clashProcess
51
99
  }
52
100
 
53
- // ---------------- 3. 清理函数 ----------------
101
+ // 清理函数
54
102
  async function cleanup() {
55
103
  try {
56
104
  // 关闭系统代理
@@ -75,7 +123,7 @@ async function cleanup() {
75
123
  }
76
124
  }
77
125
 
78
- // ---------------- 4. 注册进程退出处理 ----------------
126
+ // 注册进程退出处理
79
127
  function setupExitHandlers() {
80
128
  // 处理正常退出 (Ctrl+C)
81
129
  process.on('SIGINT', async () => {
@@ -99,8 +147,21 @@ function setupExitHandlers() {
99
147
  })
100
148
  }
101
149
 
102
- // ---------------- 5. 执行流程 ----------------
103
- export function main() {
150
+ // 检查服务健康状态
151
+ async function checkServiceHealth(apiBase, maxRetries = 20) {
152
+ for (let i = 0; i < maxRetries; i++) {
153
+ try {
154
+ await axios.get(apiBase, { timeout: 1000 })
155
+ return true
156
+ } catch (e) {
157
+ if (e.response) return true // 端口已通 (即使是 401 也可以)
158
+ await new Promise(r => setTimeout(r, 200)) // 200ms * 20 = 4s
159
+ }
160
+ }
161
+ return false
162
+ }
163
+
164
+ export async function main() {
104
165
  // 检查 clash-kit 二进制文件是否存在
105
166
  if (!fs.existsSync(CLASH_BIN_PATH)) {
106
167
  return console.error(chalk.red('\n找不到 Clash.Meta 内核文件,请先运行 clash init 命令初始化内核!\n'))
@@ -113,10 +174,30 @@ export function main() {
113
174
  // 设置退出处理
114
175
  setupExitHandlers()
115
176
 
116
- const clashProcess = startClash()
177
+ const clashProcess = await startClash()
178
+
179
+ const spinner = ora('正在等待服务启动...').start()
180
+ const started = await checkServiceHealth(getApiBase())
181
+
182
+ if (!started) {
183
+ spinner.fail(chalk.red('启动失败'))
184
+ const logPath = path.join(__dirname, 'clash.log')
185
+ if (fs.existsSync(logPath)) {
186
+ console.log(chalk.yellow('\n------- clash.log (Last 20 lines) -------'))
187
+ const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n')
188
+ console.log(lines.slice(-20).join('\n'))
189
+ console.log(chalk.yellow('-----------------------------------------\n'))
190
+ }
191
+ try {
192
+ process.kill(clashProcess.pid)
193
+ } catch (e) {}
194
+ process.exit(1)
195
+ }
196
+
197
+ spinner.succeed(chalk.green('启动成功'))
198
+
117
199
  const { http, socks } = getProxyPort()
118
200
 
119
- console.log(chalk.green('\n代理服务已在后台启动✅'))
120
201
  if (clashProcess.pid) {
121
202
  console.log(`进程名称:${chalk.yellow('clash-kit')} PID: ${chalk.yellow(clashProcess.pid)}`)
122
203
  }
@@ -1,11 +1,12 @@
1
1
  import { select } from '@inquirer/prompts'
2
2
  import ora from 'ora'
3
+ import chalk from 'chalk'
3
4
  import * as api from '../api.js'
4
5
 
5
6
  export async function proxy() {
6
7
  let spinner = ora('正在获取最新代理列表...').start()
7
8
  try {
8
- const proxies = await api.getProxies()
9
+ let proxies = await api.getProxies()
9
10
  spinner.stop()
10
11
 
11
12
  // 通常我们只关心 Proxy 组或者 Selector 类型的组
@@ -24,10 +25,42 @@ export async function proxy() {
24
25
 
25
26
  const group = proxies[groupName]
26
27
 
28
+ // 自动对组内所有节点进行测速
29
+ spinner = ora(`测速后选择合适的节点,正在对 [${groupName}] 进行测速...`).start()
30
+ await Promise.all(group.all.map(n => api.getProxyDelay(n).catch(() => {})))
31
+
32
+ // 测速完成后,刷新数据以获取最新状态
33
+ proxies = await api.getProxies()
34
+ spinner.stop()
35
+
36
+ const updatedGroup = proxies[groupName]
37
+
27
38
  // 选择节点
28
39
  const proxyName = await select({
29
- message: `[${groupName}] 当前: ${group.now}, 请选择节点:`,
30
- choices: group.all.map(n => ({ name: n, value: n })),
40
+ message: `[${groupName}] 当前: ${updatedGroup.now}, 请选择节点:`,
41
+ pageSize: 15,
42
+ choices: updatedGroup.all.map(n => {
43
+ const node = proxies[n]
44
+ const lastHistory = node?.history && node.history.length ? node.history[node.history.length - 1] : null
45
+ let delayInfo = ''
46
+
47
+ if (lastHistory && lastHistory.delay > 0) {
48
+ const delay = lastHistory.delay
49
+ if (delay < 800) {
50
+ delayInfo = chalk.green(` ${delay}ms`)
51
+ } else if (delay < 1500) {
52
+ delayInfo = chalk.yellow(` ${delay}ms`)
53
+ } else {
54
+ delayInfo = chalk.red(` ${delay}ms`)
55
+ }
56
+ } else if (lastHistory && lastHistory.delay === 0) {
57
+ delayInfo = chalk.red(' [超时]')
58
+ } else {
59
+ delayInfo = chalk.gray(' [未测速]')
60
+ }
61
+
62
+ return { name: `${n}${delayInfo}`, value: n }
63
+ }),
31
64
  })
32
65
 
33
66
  spinner = ora(`正在切换到 ${proxyName}...`).start()
@@ -2,7 +2,7 @@ import * as sysproxy from '../sysproxy.js'
2
2
  import { main as startClashService } from '../../index.js'
3
3
 
4
4
  export async function start(options) {
5
- startClashService()
5
+ await startClashService()
6
6
 
7
7
  if (options.sysproxy) {
8
8
  console.log('正在等待 Clash API 就绪以设置系统代理...')
package/lib/port.js ADDED
@@ -0,0 +1,108 @@
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
+ return `PID: ${pid}`
41
+ }
42
+ } catch (e) { return null }
43
+ } else {
44
+ // macOS / Linux
45
+ // lsof output format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
46
+ const output = execSync(`lsof -i :${port} -sTCP:LISTEN -P -n`).toString().trim()
47
+ const lines = output.split('\n')
48
+ if (lines.length > 1) {
49
+ const parts = lines[1].trim().split(/\s+/)
50
+ return `${parts[0]} (PID: ${parts[1]})`
51
+ }
52
+ }
53
+ } catch (e) {
54
+ // lsof 找不到时会返回非 0 退出码,抛出错误
55
+ return null
56
+ }
57
+ return null
58
+ }
59
+
60
+ /**
61
+ * 寻找可用端口
62
+ * @param {number} startPort
63
+ * @returns {Promise<number>}
64
+ */
65
+ export function findAvailablePort(startPort) {
66
+ return new Promise((resolve, reject) => {
67
+ const server = net.createServer()
68
+ server.on('error', (err) => {
69
+ if (startPort <= 65535) {
70
+ // 核心逻辑:如果报错,递归尝试 +1 的端口
71
+ resolve(findAvailablePort(startPort + 1))
72
+ } else {
73
+ reject(err)
74
+ }
75
+ })
76
+ server.on('listening', () => {
77
+ server.close(() => {
78
+ resolve(startPort)
79
+ })
80
+ })
81
+ server.listen(startPort, '127.0.0.1')
82
+ })
83
+ }
84
+
85
+ /**
86
+ * 从配置值中提取端口号
87
+ * @param {string|number} val - 例如 9090, ":9090", "127.0.0.1:9090"
88
+ * @returns {number|null}
89
+ */
90
+ export function extractPort(val) {
91
+ if (!val) return null
92
+ if (typeof val === 'number') return val
93
+ if (typeof val === 'string') {
94
+ // 移除空白
95
+ val = val.trim()
96
+ // 如果是纯数字字符串
97
+ if (/^\d+$/.test(val)) return parseInt(val, 10)
98
+
99
+ // 如果包含冒号,取最后一部分
100
+ if (val.includes(':')) {
101
+ const parts = val.split(':')
102
+ const lastPart = parts[parts.length - 1]
103
+ const port = parseInt(lastPart, 10)
104
+ return isNaN(port) ? null : port
105
+ }
106
+ }
107
+ return null
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clash-kit",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "A command-line interface for managing Clash configurations, subscriptions, and proxies.",
6
6
  "bin": {