@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.
@@ -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
+ }