clash-switcher 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const core_1 = require("./core");
10
+ const config_1 = require("./config");
11
+ const program = new commander_1.Command();
12
+ function createSwitcher() {
13
+ const config = (0, config_1.loadConfig)();
14
+ return new core_1.ClashSwitcher(config);
15
+ }
16
+ program
17
+ .name('clash-switcher')
18
+ .description('Clash Verge 代理切换工具')
19
+ .version('1.0.0');
20
+ // 查看/设置模式
21
+ program
22
+ .command('mode [mode]')
23
+ .alias('m')
24
+ .description('查看或设置代理模式 (rule/global/direct)')
25
+ .action(async (mode) => {
26
+ try {
27
+ const switcher = createSwitcher();
28
+ if (mode) {
29
+ if (!['rule', 'global', 'direct'].includes(mode)) {
30
+ console.error(chalk_1.default.red('无效的模式,可选: rule/global/direct'));
31
+ process.exit(1);
32
+ }
33
+ await switcher.setMode(mode);
34
+ console.log(chalk_1.default.green(`已切换到 ${mode} 模式`));
35
+ }
36
+ else {
37
+ const current = await switcher.getMode();
38
+ console.log(`当前模式: ${chalk_1.default.cyan(current)}`);
39
+ }
40
+ }
41
+ catch (e) {
42
+ console.error(chalk_1.default.red(`错误: ${e.message}`));
43
+ process.exit(1);
44
+ }
45
+ });
46
+ // 列出代理组
47
+ program
48
+ .command('groups')
49
+ .alias('g')
50
+ .description('列出所有代理组')
51
+ .action(async () => {
52
+ try {
53
+ const switcher = createSwitcher();
54
+ const groups = await switcher.getGroups();
55
+ if (groups.length === 0) {
56
+ console.log(chalk_1.default.yellow('没有可用的代理组'));
57
+ return;
58
+ }
59
+ groups.forEach((g, i) => {
60
+ const current = chalk_1.default.green(g.now);
61
+ console.log(`${chalk_1.default.gray(`#${i}`)} ${chalk_1.default.cyan(g.name)} → ${current} (${g.all.length} 节点)`);
62
+ });
63
+ }
64
+ catch (e) {
65
+ console.error(chalk_1.default.red(`错误: ${e.message}`));
66
+ process.exit(1);
67
+ }
68
+ });
69
+ // 列出节点
70
+ program
71
+ .command('nodes [group]')
72
+ .alias('n')
73
+ .description('列出代理组的节点 (支持索引或模糊匹配)')
74
+ .action(async (group) => {
75
+ try {
76
+ const switcher = createSwitcher();
77
+ const filter = group ? (isNaN(Number(group)) ? group : Number(group)) : undefined;
78
+ const g = await switcher.getGroup(undefined, filter);
79
+ if (!g) {
80
+ console.error(chalk_1.default.red('未找到代理组'));
81
+ process.exit(1);
82
+ }
83
+ console.log(chalk_1.default.cyan(`${g.name} (当前: ${chalk_1.default.green(g.now)})`));
84
+ g.all.forEach((node, i) => {
85
+ const prefix = node === g.now ? chalk_1.default.green('→') : ' ';
86
+ console.log(`${prefix} ${chalk_1.default.gray(`#${i}`)} ${node}`);
87
+ });
88
+ }
89
+ catch (e) {
90
+ console.error(chalk_1.default.red(`错误: ${e.message}`));
91
+ process.exit(1);
92
+ }
93
+ });
94
+ // 切换节点
95
+ program
96
+ .command('set <group> <node>')
97
+ .alias('s')
98
+ .description('切换节点 (支持索引或模糊匹配)')
99
+ .action(async (group, node) => {
100
+ try {
101
+ const switcher = createSwitcher();
102
+ const groupFilter = isNaN(Number(group)) ? group : Number(group);
103
+ await switcher.setNode(groupFilter, node);
104
+ console.log(chalk_1.default.green(`已切换到 ${node}`));
105
+ }
106
+ catch (e) {
107
+ console.error(chalk_1.default.red(`错误: ${e.message}`));
108
+ process.exit(1);
109
+ }
110
+ });
111
+ // 测试延迟
112
+ program
113
+ .command('test [filter]')
114
+ .alias('t')
115
+ .description('测试节点延迟 (支持索引或模糊匹配)')
116
+ .option('-u, --url <url>', '测试URL')
117
+ .option('-t, --timeout <ms>', '超时时间(ms)')
118
+ .action(async (filter, options) => {
119
+ try {
120
+ const switcher = createSwitcher();
121
+ const config = (0, config_1.loadConfig)();
122
+ const url = options.url || config.testUrl;
123
+ const timeout = options.timeout ? parseInt(options.timeout) : config.timeout;
124
+ const groupFilter = filter ? (isNaN(Number(filter)) ? filter : Number(filter)) : undefined;
125
+ const results = await switcher.testNodes(groupFilter, url, timeout);
126
+ const sorted = results.sort((a, b) => {
127
+ if (a.delay < 0)
128
+ return 1;
129
+ if (b.delay < 0)
130
+ return -1;
131
+ return a.delay - b.delay;
132
+ });
133
+ sorted.forEach(r => {
134
+ const delay = r.delay < 0
135
+ ? chalk_1.default.red('超时')
136
+ : r.delay < 200
137
+ ? chalk_1.default.green(`${r.delay}ms`)
138
+ : r.delay < 500
139
+ ? chalk_1.default.yellow(`${r.delay}ms`)
140
+ : chalk_1.default.red(`${r.delay}ms`);
141
+ console.log(`${r.name}: ${delay}`);
142
+ });
143
+ }
144
+ catch (e) {
145
+ console.error(chalk_1.default.red(`错误: ${e.message}`));
146
+ process.exit(1);
147
+ }
148
+ });
149
+ // 自动选择最快节点
150
+ program
151
+ .command('auto [filter]')
152
+ .alias('a')
153
+ .description('测试并切换到最快节点')
154
+ .option('-u, --url <url>', '测试URL')
155
+ .option('-t, --timeout <ms>', '超时时间(ms)')
156
+ .action(async (filter, options) => {
157
+ try {
158
+ const switcher = createSwitcher();
159
+ const config = (0, config_1.loadConfig)();
160
+ const url = options.url || config.testUrl;
161
+ const timeout = options.timeout ? parseInt(options.timeout) : config.timeout;
162
+ const groupFilter = filter ? (isNaN(Number(filter)) ? filter : Number(filter)) : undefined;
163
+ console.log(chalk_1.default.gray('正在测试节点延迟...'));
164
+ const result = await switcher.autoNode(groupFilter, url, timeout);
165
+ if (result) {
166
+ console.log(chalk_1.default.green(`已切换到 ${result.name} (${result.delay}ms)`));
167
+ }
168
+ else {
169
+ console.log(chalk_1.default.red('没有可用的节点'));
170
+ }
171
+ }
172
+ catch (e) {
173
+ console.error(chalk_1.default.red(`错误: ${e.message}`));
174
+ process.exit(1);
175
+ }
176
+ });
177
+ // 列出订阅
178
+ program
179
+ .command('subs')
180
+ .description('列出所有订阅')
181
+ .action(() => {
182
+ try {
183
+ const switcher = createSwitcher();
184
+ const subs = switcher.getSubs();
185
+ const current = switcher.getSub();
186
+ if (subs.length === 0) {
187
+ console.log(chalk_1.default.yellow('没有订阅'));
188
+ return;
189
+ }
190
+ subs.forEach((s, i) => {
191
+ const isCurrent = current?.uid === s.uid;
192
+ const prefix = isCurrent ? chalk_1.default.green('→') : ' ';
193
+ const name = isCurrent ? chalk_1.default.green(s.name || s.uid) : (s.name || s.uid);
194
+ console.log(`${prefix} ${chalk_1.default.gray(`#${i}`)} ${name}`);
195
+ });
196
+ }
197
+ catch (e) {
198
+ console.error(chalk_1.default.red(`错误: ${e.message}`));
199
+ process.exit(1);
200
+ }
201
+ });
202
+ // 切换订阅
203
+ program
204
+ .command('sub <filter>')
205
+ .description('切换订阅 (支持索引或模糊匹配)')
206
+ .option('--no-restart', '不重启 Clash Verge')
207
+ .action(async (filter, options) => {
208
+ try {
209
+ const switcher = createSwitcher();
210
+ const filterValue = isNaN(Number(filter)) ? filter : Number(filter);
211
+ const sub = await switcher.setSub(filterValue, options.restart);
212
+ console.log(chalk_1.default.green(`已切换到 ${sub.name || sub.uid}`));
213
+ }
214
+ catch (e) {
215
+ console.error(chalk_1.default.red(`错误: ${e.message}`));
216
+ process.exit(1);
217
+ }
218
+ });
219
+ // 配置管理
220
+ program
221
+ .command('config [key] [value]')
222
+ .alias('c')
223
+ .description('查看或设置配置')
224
+ .action((key, value) => {
225
+ const config = (0, config_1.loadConfig)();
226
+ if (key && value) {
227
+ if (key === 'port' || key === 'timeout') {
228
+ config[key] = parseInt(value);
229
+ }
230
+ else {
231
+ config[key] = value;
232
+ }
233
+ (0, config_1.saveConfig)(config);
234
+ console.log(chalk_1.default.green(`已设置 ${key} = ${value}`));
235
+ }
236
+ else {
237
+ console.log(chalk_1.default.cyan('当前配置:'));
238
+ Object.entries(config).forEach(([k, v]) => {
239
+ console.log(` ${k}: ${chalk_1.default.yellow(v)}`);
240
+ });
241
+ }
242
+ });
243
+ program.parse();
package/dist/lib.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Clash Switcher - Clash Verge 代理切换工具
3
+ * @packageDocumentation
4
+ */
5
+ export { ClashSwitcher } from './core';
6
+ export * from './types';
package/dist/lib.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * Clash Switcher - Clash Verge 代理切换工具
4
+ * @packageDocumentation
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.ClashSwitcher = void 0;
22
+ var core_1 = require("./core");
23
+ Object.defineProperty(exports, "ClashSwitcher", { enumerable: true, get: function () { return core_1.ClashSwitcher; } });
24
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Clash Switcher 配置选项
3
+ */
4
+ export interface ClashSwitcherConfig {
5
+ /**
6
+ * Clash API 主机地址
7
+ * @default '127.0.0.1'
8
+ * @example '192.168.1.100'
9
+ */
10
+ host?: string;
11
+ /**
12
+ * Clash API 端口号
13
+ * @default 9097
14
+ * @example 9090
15
+ */
16
+ port?: number;
17
+ /**
18
+ * Clash API 访问密钥
19
+ * @description 如果 Clash 配置了 secret,需要在此填写
20
+ * @default undefined
21
+ */
22
+ secret?: string;
23
+ /**
24
+ * 延迟测试 URL
25
+ * @description 用于测试节点延迟的目标地址
26
+ * @default 'http://www.gstatic.com/generate_204'
27
+ */
28
+ testUrl?: string;
29
+ /**
30
+ * 延迟测试超时时间(毫秒)
31
+ * @default 5000
32
+ */
33
+ timeout?: number;
34
+ /**
35
+ * Clash Verge 配置文件目录
36
+ * @description 自定义 Clash Verge 的配置目录路径,用于读取订阅信息
37
+ * @default Windows: %APPDATA%/io.github.clash-verge-rev.clash-verge-rev
38
+ * @default macOS: ~/Library/Application Support/io.github.clash-verge-rev.clash-verge-rev
39
+ * @default Linux: ~/.config/clash-verge
40
+ */
41
+ vergeConfigDir?: string;
42
+ }
43
+ /**
44
+ * 完整的内部配置(所有字段必填)
45
+ */
46
+ export interface ResolvedConfig {
47
+ /** Clash API 主机地址 */
48
+ host: string;
49
+ /** Clash API 端口号 */
50
+ port: number;
51
+ /** Clash API 访问密钥 */
52
+ secret?: string;
53
+ /** 延迟测试 URL */
54
+ testUrl: string;
55
+ /** 延迟测试超时时间(毫秒) */
56
+ timeout: number;
57
+ /** Clash Verge 配置文件目录 */
58
+ vergeConfigDir: string;
59
+ }
60
+ /**
61
+ * 代理组信息
62
+ */
63
+ export interface ProxyGroup {
64
+ /** 代理组名称 */
65
+ name: string;
66
+ /** 代理组类型 (Selector, URLTest, Fallback 等) */
67
+ type: string;
68
+ /** 当前选中的节点 */
69
+ now: string;
70
+ /** 组内所有节点名称 */
71
+ all: string[];
72
+ }
73
+ /**
74
+ * 代理节点信息
75
+ */
76
+ export interface ProxyInfo {
77
+ /** 节点名称 */
78
+ name: string;
79
+ /** 节点类型 (ss, vmess, trojan 等) */
80
+ type: string;
81
+ /** 延迟历史记录 */
82
+ history: {
83
+ delay: number;
84
+ }[];
85
+ }
86
+ /**
87
+ * 订阅配置信息
88
+ */
89
+ export interface Profile {
90
+ /** 订阅唯一标识 */
91
+ uid: string;
92
+ /** 订阅类型 */
93
+ type: 'remote' | 'local' | 'merge' | 'script';
94
+ /** 订阅名称 */
95
+ name?: string;
96
+ /** 订阅描述 */
97
+ desc?: string;
98
+ /** 订阅 URL(仅 remote 类型) */
99
+ url?: string;
100
+ /** 最后更新时间戳 */
101
+ updated?: number;
102
+ }
103
+ /**
104
+ * 延迟测试结果
105
+ */
106
+ export interface DelayTestResult {
107
+ /** 节点名称 */
108
+ name: string;
109
+ /** 延迟(毫秒),-1 表示超时 */
110
+ delay: number;
111
+ }
112
+ /**
113
+ * Clash 代理模式
114
+ */
115
+ export type ClashMode = 'rule' | 'global' | 'direct';
116
+ /**
117
+ * getGroups 方法的 mode 参数
118
+ */
119
+ export type GroupsMode = ClashMode | 'all' | 'active';
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ import { ClashAPI, ProxyGroup } from '../api';
2
+ /**
3
+ * 解析代理组参数,支持:
4
+ * - 不传参数:返回第一个包含所有节点的代理组
5
+ * - #1, #2:按位置索引匹配
6
+ * - 关键字:模糊匹配代理组名称
7
+ */
8
+ export declare function resolveProxyGroup(api: ClashAPI, groupArg?: string): Promise<ProxyGroup>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveProxyGroup = resolveProxyGroup;
4
+ /**
5
+ * 解析代理组参数,支持:
6
+ * - 不传参数:返回第一个包含所有节点的代理组
7
+ * - #1, #2:按位置索引匹配
8
+ * - 关键字:模糊匹配代理组名称
9
+ */
10
+ async function resolveProxyGroup(api, groupArg) {
11
+ const groups = await api.getProxyGroups();
12
+ // 过滤掉 GLOBAL、DIRECT、REJECT 等系统组,只保留有实际节点的组
13
+ const validGroups = groups.filter((g) => g.all.length > 0 &&
14
+ !['GLOBAL', 'DIRECT', 'REJECT', 'PASS', 'COMPATIBLE'].includes(g.name));
15
+ if (validGroups.length === 0) {
16
+ throw new Error('未找到可用的代理组');
17
+ }
18
+ // 不传参数:返回第一个代理组(通常是主代理组)
19
+ if (!groupArg) {
20
+ return validGroups[0];
21
+ }
22
+ // 按位置索引匹配:#1, #2, ...
23
+ if (groupArg.startsWith('#')) {
24
+ const index = parseInt(groupArg.slice(1), 10) - 1;
25
+ if (isNaN(index) || index < 0 || index >= validGroups.length) {
26
+ throw new Error(`无效的索引: ${groupArg},可用范围: #1 - #${validGroups.length}`);
27
+ }
28
+ return validGroups[index];
29
+ }
30
+ // 模糊匹配名称
31
+ const keyword = groupArg.toLowerCase();
32
+ const matched = validGroups.find((g) => g.name.toLowerCase().includes(keyword));
33
+ if (!matched) {
34
+ const available = validGroups.map((g, i) => ` #${i + 1} ${g.name}`).join('\n');
35
+ throw new Error(`未找到匹配的代理组: ${groupArg}\n\n可用的代理组:\n${available}`);
36
+ }
37
+ return matched;
38
+ }
@@ -0,0 +1,7 @@
1
+ import { ProxyGroup } from '../api';
2
+ /**
3
+ * 解析节点参数,支持:
4
+ * - #1, #2:按位置索引匹配
5
+ * - 关键字:模糊匹配节点名称
6
+ */
7
+ export declare function resolveProxy(group: ProxyGroup, proxyArg: string): string;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveProxy = resolveProxy;
4
+ /**
5
+ * 解析节点参数,支持:
6
+ * - #1, #2:按位置索引匹配
7
+ * - 关键字:模糊匹配节点名称
8
+ */
9
+ function resolveProxy(group, proxyArg) {
10
+ const proxies = group.all;
11
+ if (proxies.length === 0) {
12
+ throw new Error(`代理组 ${group.name} 没有可用节点`);
13
+ }
14
+ // 按位置索引匹配:#1, #2, ...
15
+ if (proxyArg.startsWith('#')) {
16
+ const index = parseInt(proxyArg.slice(1), 10) - 1;
17
+ if (isNaN(index) || index < 0 || index >= proxies.length) {
18
+ throw new Error(`无效的索引: ${proxyArg},可用范围: #1 - #${proxies.length}`);
19
+ }
20
+ return proxies[index];
21
+ }
22
+ // 模糊匹配名称
23
+ const keyword = proxyArg.toLowerCase();
24
+ const matched = proxies.find(p => p.toLowerCase().includes(keyword));
25
+ if (!matched) {
26
+ throw new Error(`未找到匹配的节点: ${proxyArg}`);
27
+ }
28
+ return matched;
29
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "clash-switcher",
3
+ "version": "0.0.1",
4
+ "description": "CLI tool and library for switching Clash Verge proxies and subscriptions",
5
+ "main": "dist/lib.js",
6
+ "types": "dist/lib.d.ts",
7
+ "bin": {
8
+ "clash-switcher": "./dist/index.js",
9
+ "cs": "./dist/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/lib.d.ts",
14
+ "require": "./dist/lib.js",
15
+ "import": "./dist/lib.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "dev": "ts-node src/index.ts",
24
+ "start": "node dist/index.js",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "clash",
29
+ "proxy",
30
+ "cli",
31
+ "switcher"
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "devDependencies": {
36
+ "@types/node": "^20.10.0",
37
+ "typescript": "^5.3.0"
38
+ },
39
+ "dependencies": {
40
+ "axios": "^1.6.0",
41
+ "chalk": "^4.1.2",
42
+ "commander": "^11.1.0",
43
+ "ora": "^5.4.1"
44
+ },
45
+ "engines": {
46
+ "node": ">=16.0.0"
47
+ }
48
+ }