@meanc/otter 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/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/README.md +96 -0
- package/bin/mihomo +0 -0
- package/bun.lock +146 -0
- package/index.ts +141 -0
- package/package.json +36 -0
- package/src/commands/core.tsx +181 -0
- package/src/commands/proxy.ts +203 -0
- package/src/commands/subscribe.ts +43 -0
- package/src/commands/system.ts +152 -0
- package/src/commands/ui.tsx +322 -0
- package/src/utils/api.ts +107 -0
- package/src/utils/core.ts +153 -0
- package/src/utils/parser.ts +193 -0
- package/src/utils/paths.ts +10 -0
- package/src/utils/subscription.ts +169 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
|
3
|
+
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Default to using Bun instead of Node.js.
|
|
8
|
+
|
|
9
|
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
10
|
+
- Use `bun test` instead of `jest` or `vitest`
|
|
11
|
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
12
|
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
13
|
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
14
|
+
- Bun automatically loads .env, so don't use dotenv.
|
|
15
|
+
|
|
16
|
+
## APIs
|
|
17
|
+
|
|
18
|
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
19
|
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
20
|
+
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
21
|
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
22
|
+
- `WebSocket` is built-in. Don't use `ws`.
|
|
23
|
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
24
|
+
- Bun.$`ls` instead of execa.
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
Use `bun test` to run tests.
|
|
29
|
+
|
|
30
|
+
```ts#index.test.ts
|
|
31
|
+
import { test, expect } from "bun:test";
|
|
32
|
+
|
|
33
|
+
test("hello world", () => {
|
|
34
|
+
expect(1).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Frontend
|
|
39
|
+
|
|
40
|
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
41
|
+
|
|
42
|
+
Server:
|
|
43
|
+
|
|
44
|
+
```ts#index.ts
|
|
45
|
+
import index from "./index.html"
|
|
46
|
+
|
|
47
|
+
Bun.serve({
|
|
48
|
+
routes: {
|
|
49
|
+
"/": index,
|
|
50
|
+
"/api/users/:id": {
|
|
51
|
+
GET: (req) => {
|
|
52
|
+
return new Response(JSON.stringify({ id: req.params.id }));
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
// optional websocket support
|
|
57
|
+
websocket: {
|
|
58
|
+
open: (ws) => {
|
|
59
|
+
ws.send("Hello, world!");
|
|
60
|
+
},
|
|
61
|
+
message: (ws, message) => {
|
|
62
|
+
ws.send(message);
|
|
63
|
+
},
|
|
64
|
+
close: (ws) => {
|
|
65
|
+
// handle close
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
development: {
|
|
69
|
+
hmr: true,
|
|
70
|
+
console: true,
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
76
|
+
|
|
77
|
+
```html#index.html
|
|
78
|
+
<html>
|
|
79
|
+
<body>
|
|
80
|
+
<h1>Hello, world!</h1>
|
|
81
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
With the following `frontend.tsx`:
|
|
87
|
+
|
|
88
|
+
```tsx#frontend.tsx
|
|
89
|
+
import React from "react";
|
|
90
|
+
|
|
91
|
+
// import .css files directly and it works
|
|
92
|
+
import './index.css';
|
|
93
|
+
|
|
94
|
+
import { createRoot } from "react-dom/client";
|
|
95
|
+
|
|
96
|
+
const root = createRoot(document.body);
|
|
97
|
+
|
|
98
|
+
export default function Frontend() {
|
|
99
|
+
return <h1>Hello, world!</h1>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
root.render(<Frontend />);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then, run index.ts
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
bun --hot ./index.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# 🦦 Otter
|
|
2
|
+
|
|
3
|
+
Otter (ot) 是一个基于 [Mihomo](https://github.com/MetaCubeX/mihomo)
|
|
4
|
+
核心的极简主义 Clash TUI
|
|
5
|
+
客户端。它专为速度和可组合性而设计,提供流畅的命令行体验和交互式界面。
|
|
6
|
+
|
|
7
|
+
## ✨ 特性
|
|
8
|
+
|
|
9
|
+
- **轻量级**: 基于 Bun 和 Ink 构建,启动迅速。
|
|
10
|
+
- **Mihomo 核心**: 使用高性能的 Mihomo (Clash Meta) 作为底层核心。
|
|
11
|
+
- **交互式 TUI**: 提供美观的终端用户界面,支持键盘导航。
|
|
12
|
+
- **订阅管理**: 支持多种订阅格式(Clash YAML, Base64, VMess/SS/Trojan 链接)。
|
|
13
|
+
- **系统集成**: 一键开启/关闭 macOS 系统代理,支持 Shell 代理注入。
|
|
14
|
+
- **实时监控**: 实时查看流量速度、内存占用和节点状态。
|
|
15
|
+
|
|
16
|
+
## 📦 安装
|
|
17
|
+
|
|
18
|
+
### 通过 npm (推荐)
|
|
19
|
+
|
|
20
|
+
确保你已经安装了 [Bun](https://bun.sh/)。
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bun add -g @meanc/otter
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 从源码安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 克隆仓库
|
|
30
|
+
git clone https://github.com/yourusername/otter.git
|
|
31
|
+
cd otter
|
|
32
|
+
|
|
33
|
+
# 安装依赖
|
|
34
|
+
bun install
|
|
35
|
+
|
|
36
|
+
# 链接到全局 (可选)
|
|
37
|
+
bun link
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 🚀 使用指南
|
|
41
|
+
|
|
42
|
+
### 核心控制 (Core)
|
|
43
|
+
|
|
44
|
+
- `ot up` / `ot start`: 启动 Clash 核心(后台静默运行)。
|
|
45
|
+
- `ot down` / `ot stop`: 停止 Clash 核心。
|
|
46
|
+
- `ot status`: 查看核心运行状态、版本、内存占用、实时流量及订阅信息。
|
|
47
|
+
- `ot log`: 实时查看内核日志。
|
|
48
|
+
|
|
49
|
+
### 订阅管理 (Subscription)
|
|
50
|
+
|
|
51
|
+
- `ot sub add <url> [name]`: 添加订阅源。支持自动解析 Base64 和节点链接。
|
|
52
|
+
- `ot sub rm <name>`: 删除订阅源。
|
|
53
|
+
- `ot sub update <name>`: 更新指定订阅源。
|
|
54
|
+
- `ot sub use <name>`: 切换当前使用的订阅源。
|
|
55
|
+
- `ot sub ls`: 列出所有订阅源。
|
|
56
|
+
|
|
57
|
+
### 代理管理 (Proxy)
|
|
58
|
+
|
|
59
|
+
- `ot ls`: 列出所有代理组及当前选中的节点。
|
|
60
|
+
- `ot use [node_name]`: 切换节点。支持模糊搜索。
|
|
61
|
+
- `ot use -p <index>`: 通过序号切换 `Proxy` 组节点。
|
|
62
|
+
- `ot use -g <index>`: 通过序号切换 `GLOBAL` 组节点。
|
|
63
|
+
- `ot test`: 测试当前节点的延迟。
|
|
64
|
+
- `ot best`: 自动测试并切换到延迟最低的节点。
|
|
65
|
+
|
|
66
|
+
### 系统集成 (System)
|
|
67
|
+
|
|
68
|
+
- `ot on`: 开启 macOS 系统代理。
|
|
69
|
+
- `ot off`: 关闭 macOS 系统代理。
|
|
70
|
+
- `ot shell`: 输出当前 Shell 的代理环境变量命令(可直接 `eval $(ot shell)`)。
|
|
71
|
+
- `ot mode [rule|global|direct]`: 查看或切换代理模式(规则/全局/直连)。
|
|
72
|
+
|
|
73
|
+
### 交互式界面 (TUI)
|
|
74
|
+
|
|
75
|
+
- `ot ui`: 进入全屏交互式界面。
|
|
76
|
+
|
|
77
|
+
**TUI 快捷键**:
|
|
78
|
+
|
|
79
|
+
- `↑/↓`: 上下移动光标。
|
|
80
|
+
- `←/→` 或 `Tab`: 在代理组列表和节点列表之间切换。
|
|
81
|
+
- `Enter`: 选中节点或展开组。
|
|
82
|
+
- `s`: 快速开启/关闭系统代理。
|
|
83
|
+
- `m`: 切换代理模式 (Rule/Global/Direct)。
|
|
84
|
+
- `q`: 退出 TUI。
|
|
85
|
+
|
|
86
|
+
## 🛠️ 开发
|
|
87
|
+
|
|
88
|
+
本地运行:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bun run index.ts <command>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 📝 许可证
|
|
95
|
+
|
|
96
|
+
GPL-3.0 License
|
package/bin/mihomo
ADDED
|
Binary file
|
package/bun.lock
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "otter",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@types/js-yaml": "^4.0.9",
|
|
9
|
+
"cac": "^6.7.14",
|
|
10
|
+
"chalk": "^5.6.2",
|
|
11
|
+
"fs-extra": "^11.3.3",
|
|
12
|
+
"ink": "^6.5.1",
|
|
13
|
+
"js-yaml": "^4.1.1",
|
|
14
|
+
"ps-list": "^9.0.0",
|
|
15
|
+
"react": "^19.2.3",
|
|
16
|
+
"react-reconciler": "^0.33.0",
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/bun": "latest",
|
|
20
|
+
"@types/fs-extra": "^11.0.4",
|
|
21
|
+
"@types/node": "^25.0.3",
|
|
22
|
+
"@types/react": "^19.2.7",
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"typescript": "^5",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
"packages": {
|
|
30
|
+
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA=="],
|
|
31
|
+
|
|
32
|
+
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
|
33
|
+
|
|
34
|
+
"@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="],
|
|
35
|
+
|
|
36
|
+
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
|
37
|
+
|
|
38
|
+
"@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="],
|
|
39
|
+
|
|
40
|
+
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
41
|
+
|
|
42
|
+
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
|
43
|
+
|
|
44
|
+
"ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
|
|
45
|
+
|
|
46
|
+
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
|
47
|
+
|
|
48
|
+
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
|
49
|
+
|
|
50
|
+
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
|
51
|
+
|
|
52
|
+
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
|
53
|
+
|
|
54
|
+
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
|
55
|
+
|
|
56
|
+
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
|
57
|
+
|
|
58
|
+
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
|
59
|
+
|
|
60
|
+
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
|
61
|
+
|
|
62
|
+
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
|
63
|
+
|
|
64
|
+
"cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
|
|
65
|
+
|
|
66
|
+
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
|
|
67
|
+
|
|
68
|
+
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
|
69
|
+
|
|
70
|
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
|
71
|
+
|
|
72
|
+
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
|
73
|
+
|
|
74
|
+
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
|
75
|
+
|
|
76
|
+
"es-toolkit": ["es-toolkit@1.43.0", "", {}, "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA=="],
|
|
77
|
+
|
|
78
|
+
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
|
79
|
+
|
|
80
|
+
"fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
|
|
81
|
+
|
|
82
|
+
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
|
83
|
+
|
|
84
|
+
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
|
85
|
+
|
|
86
|
+
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
|
|
87
|
+
|
|
88
|
+
"ink": ["ink@6.5.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.2.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^8.1.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-wF3j/DmkM8q5E+OtfdQhCRw8/0ahkc8CUTgEddxZzpEWPslu7YPL3t64MWRoI9m6upVGpfAg4ms2BBvxCdKRLQ=="],
|
|
89
|
+
|
|
90
|
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
|
91
|
+
|
|
92
|
+
"is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="],
|
|
93
|
+
|
|
94
|
+
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
|
95
|
+
|
|
96
|
+
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
|
97
|
+
|
|
98
|
+
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
|
99
|
+
|
|
100
|
+
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
|
101
|
+
|
|
102
|
+
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
|
103
|
+
|
|
104
|
+
"ps-list": ["ps-list@9.0.0", "", {}, "sha512-lxMEoIL/BQlk2KunFzxwUPwMvjFH7x7cmvzSLsSHpyMXl9FFfLUlfKrYwFc4wx/ZaIxxuXC4n8rjQ1CX/tkXVQ=="],
|
|
105
|
+
|
|
106
|
+
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
|
107
|
+
|
|
108
|
+
"react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
|
|
109
|
+
|
|
110
|
+
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
|
111
|
+
|
|
112
|
+
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
|
113
|
+
|
|
114
|
+
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
|
115
|
+
|
|
116
|
+
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
|
|
117
|
+
|
|
118
|
+
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
|
119
|
+
|
|
120
|
+
"string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
|
|
121
|
+
|
|
122
|
+
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
|
123
|
+
|
|
124
|
+
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
|
125
|
+
|
|
126
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
127
|
+
|
|
128
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
129
|
+
|
|
130
|
+
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
|
131
|
+
|
|
132
|
+
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
|
133
|
+
|
|
134
|
+
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
|
135
|
+
|
|
136
|
+
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
|
137
|
+
|
|
138
|
+
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
|
139
|
+
|
|
140
|
+
"bun-types/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
|
141
|
+
|
|
142
|
+
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
|
143
|
+
|
|
144
|
+
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
|
145
|
+
}
|
|
146
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { cac } from 'cac';
|
|
3
|
+
|
|
4
|
+
import * as core from './src/commands/core';
|
|
5
|
+
import * as proxy from './src/commands/proxy';
|
|
6
|
+
import * as system from './src/commands/system';
|
|
7
|
+
import * as ui from './src/commands/ui';
|
|
8
|
+
import * as subscribe from './src/commands/subscribe';
|
|
9
|
+
import { BIN_PATH } from './src/utils/paths';
|
|
10
|
+
|
|
11
|
+
const cli = cac('ot');
|
|
12
|
+
|
|
13
|
+
// Core
|
|
14
|
+
cli.command('up', 'Start Clash core').alias('start').action(core.start);
|
|
15
|
+
cli.command('down', 'Stop Clash core').alias('stop').action(core.stop);
|
|
16
|
+
cli.command('status', 'Check status').action(core.status);
|
|
17
|
+
cli.command('log', 'Show logs').action(core.log);
|
|
18
|
+
|
|
19
|
+
// Subscribe
|
|
20
|
+
cli.command('sub <cmd> [arg1] [arg2]', 'Manage subscriptions')
|
|
21
|
+
.action((cmd, arg1, arg2) => {
|
|
22
|
+
switch (cmd) {
|
|
23
|
+
case 'add':
|
|
24
|
+
// ot sub add <url> [name]
|
|
25
|
+
subscribe.add(arg1, arg2);
|
|
26
|
+
break;
|
|
27
|
+
case 'rm':
|
|
28
|
+
case 'remove':
|
|
29
|
+
// ot sub rm <name>
|
|
30
|
+
subscribe.remove(arg1);
|
|
31
|
+
break;
|
|
32
|
+
case 'update':
|
|
33
|
+
// ot sub update <name>
|
|
34
|
+
subscribe.update(arg1);
|
|
35
|
+
break;
|
|
36
|
+
case 'use':
|
|
37
|
+
// ot sub use <name>
|
|
38
|
+
subscribe.use(arg1);
|
|
39
|
+
break;
|
|
40
|
+
case 'ls':
|
|
41
|
+
case 'list':
|
|
42
|
+
// ot sub ls
|
|
43
|
+
subscribe.list();
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
// Check if cmd looks like a URL (shortcut)
|
|
47
|
+
if (cmd.startsWith('http')) {
|
|
48
|
+
subscribe.add(cmd, 'default');
|
|
49
|
+
} else {
|
|
50
|
+
console.log(`Unknown sub command: ${cmd}`);
|
|
51
|
+
console.log('Available commands: add, rm, update, use, ls');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Proxy
|
|
57
|
+
cli.command('ls', 'List proxies').action(proxy.list);
|
|
58
|
+
cli.command('use [node]', 'Switch node')
|
|
59
|
+
.option('-g, --global <index>', 'Select by global index')
|
|
60
|
+
.option('-p, --proxy <index>', 'Select by proxy index')
|
|
61
|
+
.action(proxy.use);
|
|
62
|
+
cli.command('test', 'Test latency').action(proxy.test);
|
|
63
|
+
cli.command('best', 'Select best node').action(proxy.best);
|
|
64
|
+
|
|
65
|
+
// System
|
|
66
|
+
cli.command('on', 'Enable system proxy').action(system.on);
|
|
67
|
+
cli.command('off', 'Disable system proxy').action(system.off);
|
|
68
|
+
cli.command('shell', 'Enable proxy for current shell').action(system.shell);
|
|
69
|
+
cli.command('mode [mode]', 'Get or set proxy mode (Rule/Global/Direct)').action(system.mode);
|
|
70
|
+
|
|
71
|
+
// UI
|
|
72
|
+
cli.command('ui', 'Launch TUI').action(ui.ui);
|
|
73
|
+
|
|
74
|
+
cli.command('path', 'Show binary path').action(() => {
|
|
75
|
+
console.log(BIN_PATH);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
cli.command('version', 'Show version').action(async () => {
|
|
79
|
+
const packageJson = JSON.parse(await Bun.file('./package.json').text());
|
|
80
|
+
console.log(`otter version: ${packageJson.version}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
cli.help((sections) => {
|
|
84
|
+
// Filter out the default 'Commands' section
|
|
85
|
+
const otherSections = sections.filter(s => s.title !== 'Commands');
|
|
86
|
+
|
|
87
|
+
const groups = {
|
|
88
|
+
'Core Commands': [] as any[],
|
|
89
|
+
'Subscription Commands': [] as any[],
|
|
90
|
+
'Proxy Commands': [] as any[],
|
|
91
|
+
'System Commands': [] as any[],
|
|
92
|
+
'UI Commands': [] as any[],
|
|
93
|
+
'Misc': [] as any[]
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
cli.commands.forEach(cmd => {
|
|
97
|
+
const name = cmd.name.split(' ')[0] || '';
|
|
98
|
+
if (['up', 'down', 'status', 'log', 'start', 'stop'].includes(name)) {
|
|
99
|
+
groups['Core Commands'].push(cmd);
|
|
100
|
+
} else if (name === 'sub') {
|
|
101
|
+
groups['Subscription Commands'].push(cmd);
|
|
102
|
+
} else if (['ls', 'use', 'test', 'best'].includes(name)) {
|
|
103
|
+
groups['Proxy Commands'].push(cmd);
|
|
104
|
+
} else if (['on', 'off', 'shell', 'mode'].includes(name)) {
|
|
105
|
+
groups['System Commands'].push(cmd);
|
|
106
|
+
} else if (name === 'ui') {
|
|
107
|
+
groups['UI Commands'].push(cmd);
|
|
108
|
+
} else {
|
|
109
|
+
groups['Misc'].push(cmd);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const formatCmd = (cmd: any) => {
|
|
114
|
+
const name = cmd.rawName;
|
|
115
|
+
const desc = cmd.description;
|
|
116
|
+
const padding = ' '.repeat(Math.max(0, 30 - name.length));
|
|
117
|
+
return ` ${name}${padding}${desc}`;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const newSections = [...otherSections];
|
|
121
|
+
|
|
122
|
+
// Insert grouped commands
|
|
123
|
+
Object.entries(groups).forEach(([title, cmds]) => {
|
|
124
|
+
if (cmds.length > 0) {
|
|
125
|
+
newSections.push({
|
|
126
|
+
title,
|
|
127
|
+
body: cmds.map(formatCmd).join('\n')
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Sort sections to put Usage first, then groups, then Options
|
|
133
|
+
// Actually 'Usage' is usually first in otherSections.
|
|
134
|
+
// We want to insert our groups between Usage and Options if possible,
|
|
135
|
+
// but simply appending them works too as Options is usually last.
|
|
136
|
+
// Let's just return our constructed array.
|
|
137
|
+
|
|
138
|
+
return newSections;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
cli.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meanc/otter",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"description": "以水獭为名的clash tui",
|
|
7
|
+
"module": "index.ts",
|
|
8
|
+
"version": "0.0.1",
|
|
9
|
+
"bin": {
|
|
10
|
+
"ot": "index.ts"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"postinstall": "chmod +x bin/mihomo"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/bun": "latest",
|
|
18
|
+
"@types/fs-extra": "^11.0.4",
|
|
19
|
+
"@types/node": "^25.0.3",
|
|
20
|
+
"@types/react": "^19.2.7"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@types/js-yaml": "^4.0.9",
|
|
27
|
+
"cac": "^6.7.14",
|
|
28
|
+
"chalk": "^5.6.2",
|
|
29
|
+
"fs-extra": "^11.3.3",
|
|
30
|
+
"ink": "^6.5.1",
|
|
31
|
+
"js-yaml": "^4.1.1",
|
|
32
|
+
"ps-list": "^9.0.0",
|
|
33
|
+
"react": "^19.2.3",
|
|
34
|
+
"react-reconciler": "^0.33.0"
|
|
35
|
+
}
|
|
36
|
+
}
|