@qpjoy/tunnel-cli 0.1.0

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 ADDED
@@ -0,0 +1,45 @@
1
+ # @qpjoy/tunnel-cli
2
+
3
+ Global CLI wrapper for the QPJoy Linux `mihomo-client` server script.
4
+
5
+ ```bash
6
+ npm i -g @qpjoy/tunnel-cli
7
+ qp-tunnel-cli help
8
+ ```
9
+
10
+ The package ships the existing `scripts/mihomo-client.sh` file in the npm
11
+ tarball. It does not reimplement the Linux systemd, proxy, SSH, daemon, or TUN
12
+ orchestration in Node.
13
+
14
+ ## Server Usage
15
+
16
+ Run commands directly through the bundled script:
17
+
18
+ ```bash
19
+ qp-tunnel-cli status
20
+ qp-tunnel-cli tun-on
21
+ qp-tunnel-cli tun-off
22
+ qp-tunnel-cli update-subscription
23
+ ```
24
+
25
+ For server commands, `qp-tunnel-cli` re-runs itself with `sudo` when root is needed.
26
+
27
+ Install the bundled script as a normal Linux command:
28
+
29
+ ```bash
30
+ sudo qp-tunnel-cli install-script
31
+ sudo mihomo-client status
32
+ sudo mihomo-client tun-on
33
+ ```
34
+
35
+ Use a custom target when needed:
36
+
37
+ ```bash
38
+ sudo qp-tunnel-cli install-script --target /opt/qpjoy/bin/mihomo-client
39
+ ```
40
+
41
+ Show the underlying script help:
42
+
43
+ ```bash
44
+ qp-tunnel-cli client-help
45
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_path_1 = require("node:path");
8
+ const args = process.argv.slice(2);
9
+ const packageRoot = (0, node_path_1.resolve)(__dirname, '..');
10
+ const bundledClientScript = (0, node_path_1.resolve)(packageRoot, 'resources/mihomo-client.sh');
11
+ const repoClientScript = (0, node_path_1.resolve)(packageRoot, '../../../scripts/mihomo-client.sh');
12
+ const defaultInstallTarget = '/usr/local/bin/mihomo-client';
13
+ const clientCommands = new Set([
14
+ 'setup',
15
+ 'install',
16
+ 'update-subscription',
17
+ 'start',
18
+ 'stop',
19
+ 'restart',
20
+ 'status',
21
+ 'logs',
22
+ 'enable',
23
+ 'disable',
24
+ 'proxy-on',
25
+ 'proxy-off',
26
+ 'tun-on',
27
+ 'tun-off',
28
+ 'ssh-proxy-on',
29
+ 'ssh-proxy-off',
30
+ 'daemon-proxy-on',
31
+ 'daemon-proxy-off',
32
+ 'docker-proxy-on',
33
+ 'docker-proxy-off',
34
+ 'run',
35
+ 'test',
36
+ 'print-env',
37
+ 'uninstall',
38
+ ]);
39
+ function help() {
40
+ process.stdout.write(`QPJoy Tunnel CLI
41
+
42
+ Usage:
43
+ qp-tunnel-cli help
44
+ qp-tunnel-cli install-script [--target /usr/local/bin/mihomo-client]
45
+ qp-tunnel-cli script-path
46
+ qp-tunnel-cli client-help
47
+ qp-tunnel-cli <mihomo-client command> [options]
48
+
49
+ Common commands:
50
+ qp-tunnel-cli install --url http://IP:3434/peer_user01.mihomo.yaml --user download --password pass
51
+ qp-tunnel-cli status
52
+ qp-tunnel-cli start
53
+ qp-tunnel-cli tun-on
54
+ qp-tunnel-cli tun-off
55
+ qp-tunnel-cli update-subscription
56
+ qp-tunnel-cli uninstall --purge
57
+
58
+ The npm package is a thin distributor for the Linux mihomo-client script. Client
59
+ commands re-run through sudo when needed, then execute the bundled shell script.
60
+
61
+ Install the script as a normal server command:
62
+ sudo qp-tunnel-cli install-script
63
+ sudo mihomo-client status
64
+ `);
65
+ }
66
+ function clientHelp() {
67
+ runScriptWithoutSudo(['help']);
68
+ }
69
+ function version() {
70
+ try {
71
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(packageRoot, 'package.json'), 'utf8'));
72
+ return pkg.version ?? 'unknown';
73
+ }
74
+ catch {
75
+ return 'unknown';
76
+ }
77
+ }
78
+ function isRoot() {
79
+ return typeof process.getuid === 'function' && process.getuid() === 0;
80
+ }
81
+ function resolveClientScript() {
82
+ if ((0, node_fs_1.existsSync)(bundledClientScript)) {
83
+ return bundledClientScript;
84
+ }
85
+ if ((0, node_fs_1.existsSync)(repoClientScript)) {
86
+ return repoClientScript;
87
+ }
88
+ process.stderr.write(`Could not find mihomo-client.sh. Expected ${bundledClientScript} in the npm package.\n`);
89
+ process.exit(1);
90
+ }
91
+ function exitFromSpawn(result) {
92
+ if (result.error) {
93
+ process.stderr.write(`${result.error.message}\n`);
94
+ process.exit(1);
95
+ }
96
+ if (result.signal) {
97
+ process.stderr.write(`Command terminated by signal ${result.signal}\n`);
98
+ process.exit(1);
99
+ }
100
+ process.exit(result.status ?? 0);
101
+ }
102
+ function sudoSelf(cliArgs) {
103
+ const result = (0, node_child_process_1.spawnSync)('sudo', ['-E', process.execPath, __filename, ...cliArgs], {
104
+ stdio: 'inherit',
105
+ env: process.env,
106
+ });
107
+ exitFromSpawn(result);
108
+ }
109
+ function runScriptWithoutSudo(scriptArgs) {
110
+ const result = (0, node_child_process_1.spawnSync)('bash', [resolveClientScript(), ...scriptArgs], {
111
+ stdio: 'inherit',
112
+ env: process.env,
113
+ });
114
+ exitFromSpawn(result);
115
+ }
116
+ function runClientCommand(scriptArgs) {
117
+ if (process.platform !== 'linux') {
118
+ process.stderr.write('mihomo-client commands target Linux systemd servers. Use this CLI on the server host.\n');
119
+ process.exit(1);
120
+ }
121
+ if (!isRoot()) {
122
+ sudoSelf(scriptArgs);
123
+ }
124
+ runScriptWithoutSudo(scriptArgs);
125
+ }
126
+ function parseInstallScriptArgs(scriptArgs) {
127
+ let target = defaultInstallTarget;
128
+ for (let index = 0; index < scriptArgs.length; index += 1) {
129
+ const arg = scriptArgs[index];
130
+ if (arg === '--target') {
131
+ const value = scriptArgs[index + 1];
132
+ if (!value) {
133
+ process.stderr.write('Missing value for --target.\n');
134
+ process.exit(1);
135
+ }
136
+ target = value;
137
+ index += 1;
138
+ }
139
+ else if (arg === '--help' || arg === '-h') {
140
+ process.stdout.write(`Usage:
141
+ qp-tunnel-cli install-script [--target /usr/local/bin/mihomo-client]
142
+
143
+ Copies the bundled mihomo-client.sh to the target path and chmods it 755.
144
+ `);
145
+ process.exit(0);
146
+ }
147
+ else {
148
+ process.stderr.write(`Unknown install-script option: ${arg}\n`);
149
+ process.exit(1);
150
+ }
151
+ }
152
+ return (0, node_path_1.resolve)(target);
153
+ }
154
+ function isPermissionError(error) {
155
+ return (typeof error === 'object' &&
156
+ error !== null &&
157
+ 'code' in error &&
158
+ error.code === 'EACCES');
159
+ }
160
+ function installClientScript(scriptArgs) {
161
+ const target = parseInstallScriptArgs(scriptArgs);
162
+ const source = resolveClientScript();
163
+ try {
164
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
165
+ (0, node_fs_1.copyFileSync)(source, target);
166
+ (0, node_fs_1.chmodSync)(target, 0o755);
167
+ }
168
+ catch (error) {
169
+ if (!isRoot() && isPermissionError(error)) {
170
+ sudoSelf(['install-script', ...scriptArgs]);
171
+ }
172
+ throw error;
173
+ }
174
+ process.stdout.write(`Installed mihomo-client launcher to ${target}\n`);
175
+ }
176
+ async function printScriptPath() {
177
+ const script = resolveClientScript();
178
+ try {
179
+ await (0, promises_1.access)(script, node_fs_1.constants.R_OK);
180
+ }
181
+ catch {
182
+ process.stderr.write(`Script is not readable: ${script}\n`);
183
+ process.exit(1);
184
+ }
185
+ process.stdout.write(`${script}\n`);
186
+ }
187
+ async function main() {
188
+ const command = args[0] ?? 'help';
189
+ if (command === 'help' || command === '--help' || command === '-h') {
190
+ help();
191
+ return;
192
+ }
193
+ if (command === 'version' || command === '--version' || command === '-v') {
194
+ process.stdout.write(`${version()}\n`);
195
+ return;
196
+ }
197
+ if (command === 'client-help') {
198
+ clientHelp();
199
+ }
200
+ if (command === 'script-path') {
201
+ await printScriptPath();
202
+ return;
203
+ }
204
+ if (command === 'install-script') {
205
+ installClientScript(args.slice(1));
206
+ return;
207
+ }
208
+ const passthroughCommand = command === '--verbose' ? args[1] : command;
209
+ if (passthroughCommand && clientCommands.has(passthroughCommand)) {
210
+ runClientCommand(args);
211
+ }
212
+ process.stderr.write(`Unknown command: ${command}\n`);
213
+ help();
214
+ process.exitCode = 1;
215
+ }
216
+ main().catch((error) => {
217
+ const message = error instanceof Error ? error.message : String(error);
218
+ process.stderr.write(`${message}\n`);
219
+ process.exitCode = 1;
220
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@qpjoy/tunnel-cli",
3
+ "version": "0.1.0",
4
+ "description": "Global QPJoy Tunnel CLI for installing and running the Linux mihomo-client script.",
5
+ "private": false,
6
+ "type": "commonjs",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "qp-tunnel-cli": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "resources",
15
+ "scripts",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.10.7"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "typecheck": "tsc -p tsconfig.json --noEmit",
30
+ "lint": "tsc -p tsconfig.json --noEmit"
31
+ }
32
+ }
@@ -0,0 +1,1037 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+
5
+ MIHOMO_VERBOSE="${MIHOMO_VERBOSE:-false}"
6
+
7
+ MIHOMO_HOME="${MIHOMO_HOME:-/etc/mihomo-client}"
8
+ MIHOMO_ENV_FILE="${MIHOMO_ENV_FILE:-$MIHOMO_HOME/client.env}"
9
+ MIHOMO_SUBSCRIPTION_FILE="${MIHOMO_SUBSCRIPTION_FILE:-$MIHOMO_HOME/subscription.yaml}"
10
+ MIHOMO_CONFIG_FILE="${MIHOMO_CONFIG_FILE:-$MIHOMO_HOME/config.yaml}"
11
+ MIHOMO_TUN_OVERLAY_FILE="${MIHOMO_TUN_OVERLAY_FILE:-$MIHOMO_HOME/tun-overlay.yaml}"
12
+ MIHOMO_BIN="${MIHOMO_BIN:-/usr/local/bin/mihomo}"
13
+ MIHOMO_CLIENT_LAUNCHER="${MIHOMO_CLIENT_LAUNCHER:-/usr/local/bin/mihomo-client}"
14
+ MIHOMO_SERVICE_NAME="${MIHOMO_SERVICE_NAME:-mihomo-client.service}"
15
+ MIHOMO_SERVICE_FILE="${MIHOMO_SERVICE_FILE:-/etc/systemd/system/$MIHOMO_SERVICE_NAME}"
16
+ MIHOMO_PROFILE_PROXY_FILE="${MIHOMO_PROFILE_PROXY_FILE:-/etc/profile.d/mihomo-client-proxy.sh}"
17
+ MIHOMO_DAEMON_PROXY_SERVICES="${MIHOMO_DAEMON_PROXY_SERVICES:-docker.service containerd.service buildkit.service}"
18
+ MIHOMO_DAEMON_PROXY_DROPIN_NAME="${MIHOMO_DAEMON_PROXY_DROPIN_NAME:-mihomo-proxy.conf}"
19
+ MIHOMO_SSH_PROXY_HELPER="${MIHOMO_SSH_PROXY_HELPER:-/usr/local/bin/mihomo-ssh-proxy}"
20
+ MIHOMO_SSH_CONFIG_DIR="${MIHOMO_SSH_CONFIG_DIR:-/etc/ssh/ssh_config.d}"
21
+ MIHOMO_SSH_CONFIG_FILE="${MIHOMO_SSH_CONFIG_FILE:-$MIHOMO_SSH_CONFIG_DIR/99-mihomo-proxy.conf}"
22
+ MIHOMO_SSH_PROXY_HOSTS="${MIHOMO_SSH_PROXY_HOSTS:-github.com gitlab.com bitbucket.org ssh.dev.azure.com}"
23
+ GITHUB_API_ROOT="${GITHUB_API_ROOT:-https://api.github.com/repos/MetaCubeX/mihomo/releases}"
24
+ MIHOMO_GEOX_GEOIP_URL="${MIHOMO_GEOX_GEOIP_URL:-https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat}"
25
+ MIHOMO_GEOX_GEOSITE_URL="${MIHOMO_GEOX_GEOSITE_URL:-https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat}"
26
+ MIHOMO_GEOX_MMDB_URL="${MIHOMO_GEOX_MMDB_URL:-https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb}"
27
+
28
+ usage() {
29
+ cat <<'EOF'
30
+ Usage:
31
+ sudo bash ./scripts/mihomo-client.sh [--verbose] <command> [options]
32
+
33
+ Commands:
34
+ setup Interactive install wizard (same as install without flags)
35
+ install Install Mihomo core, fetch remote subscription, create+enable service
36
+ update-subscription Re-download remote YAML and restart service if it is running
37
+ start Start Mihomo client service
38
+ stop Stop Mihomo client service
39
+ restart Restart Mihomo client service
40
+ status Show service status and current local proxy info
41
+ logs Show recent service logs
42
+ enable Enable service on boot
43
+ disable Disable service on boot
44
+ proxy-on Write /etc/profile.d proxy exports for login shells
45
+ proxy-off Remove /etc/profile.d proxy exports
46
+ tun-on Enable Mihomo TUN mode and also turn proxy-on on
47
+ tun-off Disable Mihomo TUN mode and also turn proxy-on off
48
+ ssh-proxy-on Configure OpenSSH client to use local Mihomo SOCKS for common Git hosts
49
+ ssh-proxy-off Remove OpenSSH proxy override
50
+ daemon-proxy-on Configure common daemon services to use local Mihomo proxy
51
+ daemon-proxy-off Remove daemon proxy overrides
52
+ docker-proxy-on Backward-compatible alias for daemon-proxy-on
53
+ docker-proxy-off Backward-compatible alias for daemon-proxy-off
54
+ run Run one command with Mihomo proxy env injected
55
+ test Test outbound access through the local mixed-port
56
+ print-env Print proxy env exports for the current local mixed-port
57
+ uninstall Stop+disable service and optionally remove config/binary
58
+ help Show this help
59
+
60
+ Install options:
61
+ --url URL Subscription URL, e.g. http://IP:3434/peer_user01.mihomo.yaml
62
+ --user USER Basic Auth username for subscription
63
+ --password PASS Basic Auth password for subscription
64
+ --version TAG Mihomo version tag. Default: latest stable release
65
+ --binary-path PATH Use an existing Mihomo binary instead of downloading from GitHub
66
+ --no-start Install/update files but do not start the service
67
+
68
+ Update options:
69
+ --url URL Override saved subscription URL
70
+ --user USER Override saved subscription username
71
+ --password PASS Override saved subscription password
72
+
73
+ Uninstall options:
74
+ --purge Also remove config, env, downloaded YAML, and Mihomo binary
75
+
76
+ Examples:
77
+ sudo bash ./scripts/mihomo-client.sh install \
78
+ --url http://IP:3434/peer_user01.mihomo.yaml \
79
+ --user download \
80
+ --password pass
81
+
82
+ sudo bash ./scripts/mihomo-client.sh install \
83
+ --url http://IP:3434/peer_user01.mihomo.yaml \
84
+ --binary-path /tmp/mihomo
85
+
86
+ sudo bash ./scripts/mihomo-client.sh update-subscription
87
+ sudo bash ./scripts/mihomo-client.sh start
88
+ sudo bash ./scripts/mihomo-client.sh proxy-on
89
+ sudo bash ./scripts/mihomo-client.sh tun-on
90
+ sudo bash ./scripts/mihomo-client.sh ssh-proxy-on
91
+ sudo bash ./scripts/mihomo-client.sh daemon-proxy-on
92
+ sudo bash ./scripts/mihomo-client.sh run curl -I https://www.google.com/generate_204
93
+ sudo bash ./scripts/mihomo-client.sh print-env
94
+ EOF
95
+ }
96
+
97
+ die() {
98
+ echo "Error: $*" >&2
99
+ exit 1
100
+ }
101
+
102
+ log() {
103
+ echo "[mihomo-client] $*" >&2
104
+ }
105
+
106
+ enable_verbose() {
107
+ MIHOMO_VERBOSE="true"
108
+ set -x
109
+ }
110
+
111
+ require_root() {
112
+ [[ "${EUID:-0}" -eq 0 ]] || die "Please run this script as root."
113
+ }
114
+
115
+ require_cmd() {
116
+ command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
117
+ }
118
+
119
+ ensure_dirs() {
120
+ mkdir -p "$MIHOMO_HOME"
121
+ chmod 700 "$MIHOMO_HOME"
122
+ }
123
+
124
+ set_env_value() {
125
+ local key="$1"
126
+ local value="$2"
127
+ local tmp escaped
128
+
129
+ escaped="$(printf "%s" "$value" | sed "s/'/'\\\\''/g")"
130
+ value="'$escaped'"
131
+ tmp="$(mktemp)"
132
+
133
+ if [[ -f "$MIHOMO_ENV_FILE" ]]; then
134
+ awk -v key="$key" -v value="$value" '
135
+ BEGIN { updated = 0 }
136
+ $0 ~ "^" key "=" {
137
+ print key "=" value
138
+ updated = 1
139
+ next
140
+ }
141
+ { print }
142
+ END {
143
+ if (!updated) {
144
+ print key "=" value
145
+ }
146
+ }
147
+ ' "$MIHOMO_ENV_FILE" > "$tmp"
148
+ else
149
+ printf "%s=%s\n" "$key" "$value" > "$tmp"
150
+ fi
151
+
152
+ mv "$tmp" "$MIHOMO_ENV_FILE"
153
+ chmod 600 "$MIHOMO_ENV_FILE"
154
+ }
155
+
156
+ load_env() {
157
+ [[ -f "$MIHOMO_ENV_FILE" ]] || return 0
158
+ set -a
159
+ # shellcheck disable=SC1090
160
+ source "$MIHOMO_ENV_FILE"
161
+ set +a
162
+ }
163
+
164
+ prompt_default() {
165
+ local prompt="$1"
166
+ local default_value="${2:-}"
167
+ local result
168
+
169
+ if [[ -n "$default_value" ]]; then
170
+ read -r -p "$prompt [$default_value]: " result
171
+ echo "${result:-$default_value}"
172
+ else
173
+ read -r -p "$prompt: " result
174
+ echo "$result"
175
+ fi
176
+ }
177
+
178
+ prompt_password() {
179
+ local prompt="$1"
180
+ local value
181
+ read -r -s -p "$prompt: " value
182
+ echo
183
+ echo "$value"
184
+ }
185
+
186
+ extract_auth_from_url() {
187
+ local raw_url="$1"
188
+ python3 - "$raw_url" <<'PY'
189
+ import sys
190
+ from urllib.parse import urlsplit, urlunsplit, unquote
191
+
192
+ raw = sys.argv[1]
193
+ parts = urlsplit(raw)
194
+
195
+ username = parts.username or ""
196
+ password = parts.password or ""
197
+
198
+ hostname = parts.hostname or ""
199
+ netloc = hostname
200
+ if parts.port:
201
+ netloc = f"{netloc}:{parts.port}"
202
+
203
+ sanitized = urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment))
204
+ print(sanitized)
205
+ print(unquote(username))
206
+ print(unquote(password))
207
+ PY
208
+ }
209
+
210
+ normalize_subscription_inputs() {
211
+ local url="$1"
212
+ local username="$2"
213
+ local password="$3"
214
+ local sanitized_url parsed_user parsed_pass
215
+ local -a parsed=()
216
+
217
+ if [[ "$url" == *"@"* ]] && [[ "$url" == http://* || "$url" == https://* ]]; then
218
+ mapfile -t parsed < <(extract_auth_from_url "$url")
219
+ if [[ "${#parsed[@]}" -ge 3 ]]; then
220
+ sanitized_url="${parsed[0]}"
221
+ parsed_user="${parsed[1]}"
222
+ parsed_pass="${parsed[2]}"
223
+ url="$sanitized_url"
224
+ log "Detected Basic Auth inside subscription URL. Credentials will be stored separately."
225
+ if [[ -z "$username" && -n "$parsed_user" ]]; then
226
+ username="$parsed_user"
227
+ fi
228
+ if [[ -z "$password" && -n "$parsed_pass" ]]; then
229
+ password="$parsed_pass"
230
+ fi
231
+ fi
232
+ fi
233
+
234
+ printf "%s\n%s\n%s\n" "$url" "$username" "$password"
235
+ }
236
+
237
+ detect_asset_selector() {
238
+ case "$(uname -m)" in
239
+ x86_64|amd64) echo "linux-amd64-v1" ;;
240
+ aarch64|arm64) echo "linux-arm64" ;;
241
+ armv7l|armv7) echo "linux-armv7" ;;
242
+ armv6l|armv6) echo "linux-armv6" ;;
243
+ i386|i686) echo "linux-386" ;;
244
+ riscv64) echo "linux-riscv64" ;;
245
+ s390x) echo "linux-s390x" ;;
246
+ *) die "Unsupported CPU architecture: $(uname -m)" ;;
247
+ esac
248
+ }
249
+
250
+ resolve_release_asset() {
251
+ local version="${1:-latest}"
252
+ local selector="$2"
253
+ local api_url json_file
254
+
255
+ json_file="$(mktemp)"
256
+ if [[ "$version" == "latest" ]]; then
257
+ api_url="$GITHUB_API_ROOT"
258
+ log "Querying latest stable Mihomo release metadata for $selector"
259
+ else
260
+ api_url="$GITHUB_API_ROOT/tags/$version"
261
+ log "Querying Mihomo release metadata for tag $version ($selector)"
262
+ fi
263
+
264
+ if [[ "$version" == "latest" ]]; then
265
+ curl -fsSL "$api_url" -o "$json_file"
266
+ python3 - "$json_file" "$selector" <<'PY'
267
+ import json, sys
268
+ from pathlib import Path
269
+
270
+ path = Path(sys.argv[1])
271
+ selector = sys.argv[2]
272
+ releases = json.loads(path.read_text())
273
+
274
+ for rel in releases:
275
+ if rel.get("prerelease"):
276
+ continue
277
+ assets = rel.get("assets", [])
278
+ for asset in assets:
279
+ name = asset.get("name", "")
280
+ if name.startswith(f"mihomo-{selector}-") and name.endswith(".gz"):
281
+ print(rel.get("tag_name", ""))
282
+ print(asset.get("browser_download_url", ""))
283
+ sys.exit(0)
284
+ raise SystemExit(1)
285
+ PY
286
+ else
287
+ curl -fsSL "$api_url" -o "$json_file"
288
+ python3 - "$json_file" "$selector" <<'PY'
289
+ import json, sys
290
+ from pathlib import Path
291
+
292
+ path = Path(sys.argv[1])
293
+ selector = sys.argv[2]
294
+ rel = json.loads(path.read_text())
295
+ assets = rel.get("assets", [])
296
+
297
+ for asset in assets:
298
+ name = asset.get("name", "")
299
+ if name.startswith(f"mihomo-{selector}-") and name.endswith(".gz"):
300
+ print(rel.get("tag_name", ""))
301
+ print(asset.get("browser_download_url", ""))
302
+ sys.exit(0)
303
+ raise SystemExit(1)
304
+ PY
305
+ fi
306
+ rm -f "$json_file"
307
+ }
308
+
309
+ install_binary() {
310
+ local version="${1:-latest}"
311
+ local selector release_info tag download_url tmpdir archive tmpbin
312
+
313
+ require_cmd curl
314
+ require_cmd python3
315
+ require_cmd gzip
316
+
317
+ selector="$(detect_asset_selector)"
318
+ mapfile -t release_info < <(resolve_release_asset "$version" "$selector") || die "Failed to resolve Mihomo release asset for $selector"
319
+ [[ "${#release_info[@]}" -ge 2 ]] || die "Unexpected release metadata returned from GitHub."
320
+ tag="${release_info[0]}"
321
+ download_url="${release_info[1]}"
322
+
323
+ tmpdir="$(mktemp -d)"
324
+ archive="$tmpdir/mihomo.gz"
325
+ tmpbin="$tmpdir/mihomo"
326
+
327
+ log "Downloading Mihomo $tag from GitHub"
328
+ curl -fsSL "$download_url" -o "$archive"
329
+ gzip -dc "$archive" > "$tmpbin"
330
+ chmod 755 "$tmpbin"
331
+ mv "$tmpbin" "$MIHOMO_BIN"
332
+
333
+ set_env_value MIHOMO_VERSION "$tag"
334
+ echo "Installed Mihomo $tag to $MIHOMO_BIN"
335
+ rm -rf "$tmpdir"
336
+ }
337
+
338
+ install_binary_from_path() {
339
+ local source_path="$1"
340
+ [[ -n "$source_path" ]] || die "Binary path is required."
341
+ [[ -f "$source_path" ]] || die "Mihomo binary not found: $source_path"
342
+ [[ -x "$source_path" ]] || die "Mihomo binary is not executable: $source_path"
343
+ cp "$source_path" "$MIHOMO_BIN"
344
+ chmod 755 "$MIHOMO_BIN"
345
+ set_env_value MIHOMO_VERSION "local"
346
+ echo "Installed Mihomo local binary to $MIHOMO_BIN"
347
+ }
348
+
349
+ install_client_launcher() {
350
+ local source_script
351
+ source_script="$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")"
352
+ [[ -f "$source_script" ]] || die "Could not locate current mihomo-client script: $source_script"
353
+ cp "$source_script" "$MIHOMO_CLIENT_LAUNCHER"
354
+ chmod 755 "$MIHOMO_CLIENT_LAUNCHER"
355
+ log "Installed global launcher to $MIHOMO_CLIENT_LAUNCHER"
356
+ }
357
+
358
+ write_service_file() {
359
+ cat > "$MIHOMO_SERVICE_FILE" <<EOF
360
+ [Unit]
361
+ Description=Mihomo Client
362
+ After=network-online.target
363
+ Wants=network-online.target
364
+
365
+ [Service]
366
+ Type=simple
367
+ WorkingDirectory=$MIHOMO_HOME
368
+ ExecStart=$MIHOMO_BIN -d $MIHOMO_HOME -f $MIHOMO_CONFIG_FILE
369
+ Restart=on-failure
370
+ RestartSec=5
371
+ LimitNOFILE=1048576
372
+
373
+ [Install]
374
+ WantedBy=multi-user.target
375
+ EOF
376
+ }
377
+
378
+ systemd_reload() {
379
+ systemctl daemon-reload
380
+ }
381
+
382
+ service_is_active() {
383
+ systemctl is-active --quiet "$MIHOMO_SERVICE_NAME"
384
+ }
385
+
386
+ ensure_subscription_source() {
387
+ if [[ ! -f "$MIHOMO_SUBSCRIPTION_FILE" && -f "$MIHOMO_CONFIG_FILE" ]]; then
388
+ cp "$MIHOMO_CONFIG_FILE" "$MIHOMO_SUBSCRIPTION_FILE"
389
+ chmod 600 "$MIHOMO_SUBSCRIPTION_FILE"
390
+ fi
391
+ [[ -f "$MIHOMO_SUBSCRIPTION_FILE" ]] || die "Subscription source not found. Please run install or update-subscription first."
392
+ }
393
+
394
+ write_tun_overlay() {
395
+ cat > "$MIHOMO_TUN_OVERLAY_FILE" <<'EOF'
396
+ tun:
397
+ enable: true
398
+ stack: system
399
+ auto-route: true
400
+ auto-redirect: true
401
+ auto-detect-interface: true
402
+ strict-route: true
403
+ dns-hijack:
404
+ - any:53
405
+ - tcp://any:53
406
+
407
+ dns:
408
+ enable: true
409
+ listen: 0.0.0.0:1053
410
+ ipv6: false
411
+ use-hosts: true
412
+ use-system-hosts: true
413
+ cache-algorithm: arc
414
+ enhanced-mode: fake-ip
415
+ fake-ip-range: 198.18.0.1/16
416
+ default-nameserver:
417
+ - 223.5.5.5
418
+ - 119.29.29.29
419
+ - 1.1.1.1
420
+ nameserver:
421
+ - https://dns.alidns.com/dns-query
422
+ - https://doh.pub/dns-query
423
+ fallback:
424
+ - tls://1.1.1.1
425
+ - tls://8.8.8.8
426
+ fallback-filter:
427
+ geoip: true
428
+ geoip-code: CN
429
+ geosite:
430
+ - gfw
431
+ EOF
432
+ chmod 600 "$MIHOMO_TUN_OVERLAY_FILE"
433
+ }
434
+
435
+ remove_tun_overlay() {
436
+ rm -f "$MIHOMO_TUN_OVERLAY_FILE"
437
+ }
438
+
439
+ tun_enabled() {
440
+ [[ -f "$MIHOMO_TUN_OVERLAY_FILE" ]]
441
+ }
442
+
443
+ config_uses_geodata() {
444
+ grep -Eq '^[[:space:]]*geodata-mode:[[:space:]]*true([[:space:]]|$)' "$MIHOMO_SUBSCRIPTION_FILE"
445
+ }
446
+
447
+ config_has_geox_url() {
448
+ grep -Eq '^[[:space:]]*geox-url:[[:space:]]*$' "$MIHOMO_SUBSCRIPTION_FILE"
449
+ }
450
+
451
+ append_geox_overlay_if_needed() {
452
+ if config_uses_geodata && ! config_has_geox_url; then
453
+ log "Injecting geox-url mirror overrides for geo data downloads"
454
+ cat >> "$MIHOMO_CONFIG_FILE" <<EOF
455
+
456
+ geox-url:
457
+ geoip: "$MIHOMO_GEOX_GEOIP_URL"
458
+ geosite: "$MIHOMO_GEOX_GEOSITE_URL"
459
+ mmdb: "$MIHOMO_GEOX_MMDB_URL"
460
+ EOF
461
+ fi
462
+ }
463
+
464
+ render_runtime_config() {
465
+ ensure_subscription_source
466
+ cat "$MIHOMO_SUBSCRIPTION_FILE" > "$MIHOMO_CONFIG_FILE"
467
+ append_geox_overlay_if_needed
468
+ if tun_enabled; then
469
+ printf "\n" >> "$MIHOMO_CONFIG_FILE"
470
+ cat "$MIHOMO_TUN_OVERLAY_FILE" >> "$MIHOMO_CONFIG_FILE"
471
+ fi
472
+ chmod 600 "$MIHOMO_CONFIG_FILE"
473
+ }
474
+
475
+ fetch_subscription() {
476
+ local url="$1"
477
+ local username="${2:-}"
478
+ local password="${3:-}"
479
+ local tmp_file
480
+ local -a curl_args=()
481
+
482
+ [[ -n "$url" ]] || die "Subscription URL is required."
483
+
484
+ tmp_file="$(mktemp)"
485
+ curl_args=(-fsSL)
486
+ if [[ -n "$username" || -n "$password" ]]; then
487
+ curl_args+=(-u "${username}:${password}")
488
+ fi
489
+
490
+ log "Fetching remote subscription: $url"
491
+ curl "${curl_args[@]}" "$url" -o "$tmp_file"
492
+ [[ -s "$tmp_file" ]] || die "Downloaded subscription is empty."
493
+ mv "$tmp_file" "$MIHOMO_SUBSCRIPTION_FILE"
494
+ chmod 600 "$MIHOMO_SUBSCRIPTION_FILE"
495
+ render_runtime_config
496
+ }
497
+
498
+ mixed_port_from_config() {
499
+ if [[ -f "$MIHOMO_CONFIG_FILE" ]]; then
500
+ awk -F: '/^[[:space:]]*mixed-port[[:space:]]*:/ {gsub(/[[:space:]]/, "", $2); print $2; exit}' "$MIHOMO_CONFIG_FILE"
501
+ fi
502
+ }
503
+
504
+ proxy_env_lines() {
505
+ local port="$1"
506
+ cat <<EOF
507
+ export http_proxy=http://127.0.0.1:$port
508
+ export https_proxy=http://127.0.0.1:$port
509
+ export HTTP_PROXY=http://127.0.0.1:$port
510
+ export HTTPS_PROXY=http://127.0.0.1:$port
511
+ export all_proxy=socks5://127.0.0.1:$port
512
+ export ALL_PROXY=socks5://127.0.0.1:$port
513
+ EOF
514
+ }
515
+
516
+ print_env_command() {
517
+ local port
518
+ port="$(mixed_port_from_config)"
519
+ [[ -n "$port" ]] || die "Could not detect mixed-port from $MIHOMO_CONFIG_FILE"
520
+ proxy_env_lines "$port"
521
+ }
522
+
523
+ docker_proxy_env_lines() {
524
+ local port="$1"
525
+ cat <<EOF
526
+ HTTP_PROXY=http://127.0.0.1:$port
527
+ HTTPS_PROXY=http://127.0.0.1:$port
528
+ NO_PROXY=localhost,127.0.0.1,::1
529
+ http_proxy=http://127.0.0.1:$port
530
+ https_proxy=http://127.0.0.1:$port
531
+ no_proxy=localhost,127.0.0.1,::1
532
+ EOF
533
+ }
534
+
535
+ service_dropin_dir() {
536
+ local service="$1"
537
+ echo "/etc/systemd/system/${service}.d"
538
+ }
539
+
540
+ service_dropin_file() {
541
+ local service="$1"
542
+ echo "$(service_dropin_dir "$service")/$MIHOMO_DAEMON_PROXY_DROPIN_NAME"
543
+ }
544
+
545
+ service_exists() {
546
+ local service="$1"
547
+ local state
548
+ state="$(systemctl show -p LoadState --value "$service" 2>/dev/null || true)"
549
+ [[ -n "$state" && "$state" != "not-found" ]]
550
+ }
551
+
552
+ managed_daemon_services() {
553
+ local service
554
+ for service in $MIHOMO_DAEMON_PROXY_SERVICES; do
555
+ if service_exists "$service"; then
556
+ echo "$service"
557
+ fi
558
+ done
559
+ }
560
+
561
+ managed_daemon_services_with_proxy() {
562
+ local service file
563
+ for service in $MIHOMO_DAEMON_PROXY_SERVICES; do
564
+ file="$(service_dropin_file "$service")"
565
+ if [[ -f "$file" ]]; then
566
+ echo "$service"
567
+ fi
568
+ done
569
+ }
570
+
571
+ ssh_proxy_enabled() {
572
+ [[ -f "$MIHOMO_SSH_CONFIG_FILE" ]]
573
+ }
574
+
575
+ status_command() {
576
+ local port version daemon_services
577
+ load_env
578
+ version="${MIHOMO_VERSION:-unknown}"
579
+ port="$(mixed_port_from_config || true)"
580
+ daemon_services="$(managed_daemon_services_with_proxy | paste -sd ',' - 2>/dev/null || true)"
581
+
582
+ echo "Mihomo binary: $MIHOMO_BIN"
583
+ echo "Mihomo version: $version"
584
+ echo "Config file: $MIHOMO_CONFIG_FILE"
585
+ echo "Subscription file: $MIHOMO_SUBSCRIPTION_FILE"
586
+ echo "Subscription URL: ${MIHOMO_SUBSCRIPTION_URL:-unset}"
587
+ echo "Mixed port: ${port:-unknown}"
588
+ echo "Shell proxy profile: $([[ -f "$MIHOMO_PROFILE_PROXY_FILE" ]] && echo enabled || echo disabled)"
589
+ echo "TUN mode: $([[ -f "$MIHOMO_TUN_OVERLAY_FILE" ]] && echo enabled || echo disabled)"
590
+ echo "SSH proxy config: $([[ -f "$MIHOMO_SSH_CONFIG_FILE" ]] && echo enabled || echo disabled)"
591
+ echo "Managed daemon proxy services: ${daemon_services:-none}"
592
+ echo
593
+ systemctl status "$MIHOMO_SERVICE_NAME" --no-pager || true
594
+ }
595
+
596
+ logs_command() {
597
+ journalctl -u "$MIHOMO_SERVICE_NAME" -n 100 --no-pager
598
+ }
599
+
600
+ update_subscription_command() {
601
+ local url="${1:-}"
602
+ local username="${2:-}"
603
+ local password="${3:-}"
604
+ local -a normalized=()
605
+
606
+ load_env
607
+
608
+ url="${url:-${MIHOMO_SUBSCRIPTION_URL:-}}"
609
+ username="${username:-${MIHOMO_SUBSCRIPTION_USER:-}}"
610
+ password="${password:-${MIHOMO_SUBSCRIPTION_PASSWORD:-}}"
611
+ mapfile -t normalized < <(normalize_subscription_inputs "$url" "$username" "$password")
612
+ url="${normalized[0]}"
613
+ username="${normalized[1]}"
614
+ password="${normalized[2]}"
615
+
616
+ [[ -n "$url" ]] || die "No subscription URL configured. Use install or pass --url."
617
+
618
+ fetch_subscription "$url" "$username" "$password"
619
+ set_env_value MIHOMO_SUBSCRIPTION_URL "$url"
620
+ set_env_value MIHOMO_SUBSCRIPTION_USER "$username"
621
+ set_env_value MIHOMO_SUBSCRIPTION_PASSWORD "$password"
622
+
623
+ if service_is_active; then
624
+ log "Restarting Mihomo service after subscription update"
625
+ systemctl restart "$MIHOMO_SERVICE_NAME"
626
+ echo "Subscription updated and service restarted."
627
+ else
628
+ echo "Subscription updated."
629
+ fi
630
+ }
631
+
632
+ proxy_on_command() {
633
+ local port
634
+ port="$(mixed_port_from_config)"
635
+ [[ -n "$port" ]] || die "Could not detect mixed-port from $MIHOMO_CONFIG_FILE"
636
+ cat > "$MIHOMO_PROFILE_PROXY_FILE" <<EOF
637
+ # Generated by scripts/mihomo-client.sh
638
+ $(proxy_env_lines "$port")
639
+ EOF
640
+ chmod 644 "$MIHOMO_PROFILE_PROXY_FILE"
641
+ echo "Shell proxy exports enabled at $MIHOMO_PROFILE_PROXY_FILE"
642
+ echo "Open a new shell or run: source $MIHOMO_PROFILE_PROXY_FILE"
643
+ }
644
+
645
+ proxy_off_command() {
646
+ rm -f "$MIHOMO_PROFILE_PROXY_FILE"
647
+ echo "Shell proxy exports removed."
648
+ }
649
+
650
+ write_ssh_proxy_helper() {
651
+ cat > "$MIHOMO_SSH_PROXY_HELPER" <<'EOF'
652
+ #!/bin/sh
653
+ set -eu
654
+
655
+ HOST="${1:?host required}"
656
+ PORT="${2:?port required}"
657
+ PROXY_ADDR="${MIHOMO_SSH_PROXY_ADDR:-127.0.0.1:7890}"
658
+
659
+ if command -v nc >/dev/null 2>&1; then
660
+ exec nc -x "$PROXY_ADDR" -X 5 "$HOST" "$PORT"
661
+ fi
662
+
663
+ if command -v ncat >/dev/null 2>&1; then
664
+ exec ncat --proxy "$PROXY_ADDR" --proxy-type socks5 "$HOST" "$PORT"
665
+ fi
666
+
667
+ if command -v connect-proxy >/dev/null 2>&1; then
668
+ exec connect-proxy -S "$PROXY_ADDR" "$HOST" "$PORT"
669
+ fi
670
+
671
+ echo "No SOCKS-capable helper found (need nc, ncat, or connect-proxy)." >&2
672
+ exit 1
673
+ EOF
674
+ chmod 755 "$MIHOMO_SSH_PROXY_HELPER"
675
+ }
676
+
677
+ ssh_proxy_on_command() {
678
+ local port
679
+ port="$(mixed_port_from_config)"
680
+ [[ -n "$port" ]] || die "Could not detect mixed-port from $MIHOMO_CONFIG_FILE"
681
+ mkdir -p "$MIHOMO_SSH_CONFIG_DIR"
682
+ write_ssh_proxy_helper
683
+ cat > "$MIHOMO_SSH_CONFIG_FILE" <<EOF
684
+ Host $MIHOMO_SSH_PROXY_HOSTS
685
+ ProxyCommand env MIHOMO_SSH_PROXY_ADDR=127.0.0.1:$port $MIHOMO_SSH_PROXY_HELPER %h %p
686
+ EOF
687
+ chmod 644 "$MIHOMO_SSH_CONFIG_FILE"
688
+ echo "OpenSSH proxy config enabled at $MIHOMO_SSH_CONFIG_FILE"
689
+ }
690
+
691
+ ssh_proxy_off_command() {
692
+ rm -f "$MIHOMO_SSH_CONFIG_FILE"
693
+ rm -f "$MIHOMO_SSH_PROXY_HELPER"
694
+ echo "OpenSSH proxy config removed."
695
+ }
696
+
697
+ daemon_proxy_on_command() {
698
+ local port
699
+ local service file dir
700
+ require_cmd systemctl
701
+ port="$(mixed_port_from_config)"
702
+ [[ -n "$port" ]] || die "Could not detect mixed-port from $MIHOMO_CONFIG_FILE"
703
+ if [[ -z "$(managed_daemon_services)" ]]; then
704
+ log "No known daemon services found for proxy integration."
705
+ fi
706
+ for service in $(managed_daemon_services); do
707
+ dir="$(service_dropin_dir "$service")"
708
+ file="$(service_dropin_file "$service")"
709
+ mkdir -p "$dir"
710
+ cat > "$file" <<EOF
711
+ [Service]
712
+ Environment="HTTP_PROXY=http://127.0.0.1:$port"
713
+ Environment="HTTPS_PROXY=http://127.0.0.1:$port"
714
+ Environment="NO_PROXY=localhost,127.0.0.1,::1"
715
+ Environment="http_proxy=http://127.0.0.1:$port"
716
+ Environment="https_proxy=http://127.0.0.1:$port"
717
+ Environment="no_proxy=localhost,127.0.0.1,::1"
718
+ EOF
719
+ done
720
+ systemd_reload
721
+ for service in $(managed_daemon_services); do
722
+ systemctl restart "$service"
723
+ done
724
+ echo "Daemon proxy enabled for: $(managed_daemon_services | paste -sd ',' - 2>/dev/null || true)"
725
+ }
726
+
727
+ daemon_proxy_off_command() {
728
+ local service dir file
729
+ require_cmd systemctl
730
+ for service in $MIHOMO_DAEMON_PROXY_SERVICES; do
731
+ file="$(service_dropin_file "$service")"
732
+ dir="$(service_dropin_dir "$service")"
733
+ rm -f "$file"
734
+ if [[ -d "$dir" ]] && [[ -z "$(ls -A "$dir" 2>/dev/null)" ]]; then
735
+ rmdir "$dir" 2>/dev/null || true
736
+ fi
737
+ done
738
+ systemd_reload
739
+ for service in $(managed_daemon_services); do
740
+ systemctl restart "$service"
741
+ done
742
+ echo "Daemon proxy disabled."
743
+ }
744
+
745
+ tun_on_command() {
746
+ write_tun_overlay
747
+ render_runtime_config
748
+ proxy_on_command
749
+ ssh_proxy_on_command
750
+ daemon_proxy_on_command
751
+ if service_is_active; then
752
+ systemctl restart "$MIHOMO_SERVICE_NAME"
753
+ echo "Mihomo TUN mode enabled and service restarted."
754
+ else
755
+ systemctl start "$MIHOMO_SERVICE_NAME"
756
+ echo "Mihomo TUN mode enabled and service started."
757
+ fi
758
+ }
759
+
760
+ tun_off_command() {
761
+ remove_tun_overlay
762
+ render_runtime_config
763
+ proxy_off_command
764
+ ssh_proxy_off_command
765
+ daemon_proxy_off_command
766
+ if service_is_active; then
767
+ systemctl restart "$MIHOMO_SERVICE_NAME"
768
+ echo "Mihomo TUN mode disabled and service restarted."
769
+ else
770
+ echo "Mihomo TUN mode disabled."
771
+ fi
772
+ }
773
+
774
+ run_command() {
775
+ local port
776
+ [[ $# -gt 0 ]] || die "Usage: sudo bash ./scripts/mihomo-client.sh run <command> [args...]"
777
+ port="$(mixed_port_from_config)"
778
+ [[ -n "$port" ]] || die "Could not detect mixed-port from $MIHOMO_CONFIG_FILE"
779
+ http_proxy="http://127.0.0.1:$port" \
780
+ https_proxy="http://127.0.0.1:$port" \
781
+ HTTP_PROXY="http://127.0.0.1:$port" \
782
+ HTTPS_PROXY="http://127.0.0.1:$port" \
783
+ all_proxy="socks5://127.0.0.1:$port" \
784
+ ALL_PROXY="socks5://127.0.0.1:$port" \
785
+ "$@"
786
+ }
787
+
788
+ test_command() {
789
+ local url="${1:-https://www.google.com/generate_204}"
790
+ local port
791
+ port="$(mixed_port_from_config)"
792
+ [[ -n "$port" ]] || die "Could not detect mixed-port from $MIHOMO_CONFIG_FILE"
793
+ require_cmd curl
794
+ echo "Testing through local mixed-port $port -> $url"
795
+ curl -I --proxy "http://127.0.0.1:$port" --max-time 15 "$url"
796
+ }
797
+
798
+ install_command() {
799
+ local url="${1:-}"
800
+ local username="${2:-}"
801
+ local password="${3:-}"
802
+ local version="${4:-latest}"
803
+ local autostart="${5:-true}"
804
+ local binary_path="${6:-}"
805
+ local -a normalized=()
806
+
807
+ ensure_dirs
808
+ load_env
809
+ log "Starting Mihomo client install/setup"
810
+
811
+ if [[ -z "$url" ]]; then
812
+ url="$(prompt_default "Subscription URL" "${MIHOMO_SUBSCRIPTION_URL:-}")"
813
+ fi
814
+ mapfile -t normalized < <(normalize_subscription_inputs "$url" "$username" "$password")
815
+ url="${normalized[0]}"
816
+ username="${normalized[1]}"
817
+ password="${normalized[2]}"
818
+ if [[ -z "$username" ]]; then
819
+ username="$(prompt_default "Subscription username (empty if none)" "${MIHOMO_SUBSCRIPTION_USER:-}")"
820
+ fi
821
+ if [[ -z "$password" ]]; then
822
+ password="$(prompt_password "Subscription password (empty if none)")"
823
+ fi
824
+
825
+ if [[ -n "$binary_path" ]]; then
826
+ log "Installing Mihomo core from local binary path"
827
+ install_binary_from_path "$binary_path"
828
+ else
829
+ log "Installing Mihomo core binary"
830
+ install_binary "$version"
831
+ fi
832
+ log "Installing global mihomo-client launcher"
833
+ install_client_launcher
834
+ log "Writing systemd service file"
835
+ write_service_file
836
+ systemd_reload
837
+
838
+ set_env_value MIHOMO_SUBSCRIPTION_URL "$url"
839
+ set_env_value MIHOMO_SUBSCRIPTION_USER "$username"
840
+ set_env_value MIHOMO_SUBSCRIPTION_PASSWORD "$password"
841
+
842
+ log "Downloading initial subscription and rendering runtime config"
843
+ update_subscription_command "$url" "$username" "$password"
844
+
845
+ systemctl enable "$MIHOMO_SERVICE_NAME" >/dev/null 2>&1 || true
846
+ if [[ "$autostart" == "true" ]]; then
847
+ log "Starting Mihomo client service"
848
+ systemctl restart "$MIHOMO_SERVICE_NAME"
849
+ echo "Mihomo client installed and started."
850
+ else
851
+ echo "Mihomo client installed. Service not started because --no-start was used."
852
+ fi
853
+ }
854
+
855
+ start_command() {
856
+ systemctl start "$MIHOMO_SERVICE_NAME"
857
+ }
858
+
859
+ stop_command() {
860
+ systemctl stop "$MIHOMO_SERVICE_NAME"
861
+ }
862
+
863
+ restart_command() {
864
+ systemctl restart "$MIHOMO_SERVICE_NAME"
865
+ }
866
+
867
+ enable_command() {
868
+ systemctl enable "$MIHOMO_SERVICE_NAME"
869
+ }
870
+
871
+ disable_command() {
872
+ systemctl disable "$MIHOMO_SERVICE_NAME"
873
+ }
874
+
875
+ uninstall_command() {
876
+ local purge="${1:-false}"
877
+ systemctl disable --now "$MIHOMO_SERVICE_NAME" >/dev/null 2>&1 || true
878
+ rm -f "$MIHOMO_SERVICE_FILE"
879
+ rm -f "$MIHOMO_PROFILE_PROXY_FILE"
880
+ rm -f "$MIHOMO_TUN_OVERLAY_FILE"
881
+ rm -f "$MIHOMO_SSH_CONFIG_FILE"
882
+ rm -f "$MIHOMO_SSH_PROXY_HELPER"
883
+ rm -f "$MIHOMO_CLIENT_LAUNCHER"
884
+ systemd_reload
885
+
886
+ if [[ "$purge" == "true" ]]; then
887
+ rm -f "$MIHOMO_BIN"
888
+ rm -rf "$MIHOMO_HOME"
889
+ fi
890
+
891
+ echo "Mihomo client service removed."
892
+ if [[ "$purge" == "true" ]]; then
893
+ echo "Binary and client state purged."
894
+ fi
895
+ }
896
+
897
+ main() {
898
+ local command="${1:-help}"
899
+
900
+ if [[ "${1:-}" == "--verbose" ]]; then
901
+ enable_verbose
902
+ shift
903
+ command="${1:-help}"
904
+ fi
905
+
906
+ shift || true
907
+
908
+ if [[ "$command" == "help" || "$command" == "-h" || "$command" == "--help" ]]; then
909
+ usage
910
+ exit 0
911
+ fi
912
+
913
+ require_root
914
+ require_cmd systemctl
915
+
916
+ case "$command" in
917
+ setup)
918
+ install_command "" "" "" "latest" "true"
919
+ ;;
920
+ install)
921
+ local url=""
922
+ local username=""
923
+ local password=""
924
+ local version="latest"
925
+ local autostart="true"
926
+ local binary_path=""
927
+ while [[ $# -gt 0 ]]; do
928
+ case "$1" in
929
+ --url) url="$2"; shift 2 ;;
930
+ --user) username="$2"; shift 2 ;;
931
+ --password) password="$2"; shift 2 ;;
932
+ --version) version="$2"; shift 2 ;;
933
+ --binary-path) binary_path="$2"; shift 2 ;;
934
+ --no-start) autostart="false"; shift ;;
935
+ *) die "Unknown install option: $1" ;;
936
+ esac
937
+ done
938
+ install_command "$url" "$username" "$password" "$version" "$autostart" "$binary_path"
939
+ ;;
940
+ update-subscription)
941
+ local url=""
942
+ local username=""
943
+ local password=""
944
+ while [[ $# -gt 0 ]]; do
945
+ case "$1" in
946
+ --url) url="$2"; shift 2 ;;
947
+ --user) username="$2"; shift 2 ;;
948
+ --password) password="$2"; shift 2 ;;
949
+ *) die "Unknown update-subscription option: $1" ;;
950
+ esac
951
+ done
952
+ update_subscription_command "$url" "$username" "$password"
953
+ ;;
954
+ start)
955
+ start_command
956
+ ;;
957
+ stop)
958
+ stop_command
959
+ ;;
960
+ restart)
961
+ restart_command
962
+ ;;
963
+ status)
964
+ status_command
965
+ ;;
966
+ logs)
967
+ logs_command
968
+ ;;
969
+ enable)
970
+ enable_command
971
+ ;;
972
+ disable)
973
+ disable_command
974
+ ;;
975
+ proxy-on)
976
+ proxy_on_command
977
+ ;;
978
+ proxy-off)
979
+ proxy_off_command
980
+ ;;
981
+ tun-on)
982
+ tun_on_command
983
+ ;;
984
+ tun-off)
985
+ tun_off_command
986
+ ;;
987
+ ssh-proxy-on)
988
+ ssh_proxy_on_command
989
+ ;;
990
+ ssh-proxy-off)
991
+ ssh_proxy_off_command
992
+ ;;
993
+ daemon-proxy-on)
994
+ daemon_proxy_on_command
995
+ ;;
996
+ daemon-proxy-off)
997
+ daemon_proxy_off_command
998
+ ;;
999
+ docker-proxy-on)
1000
+ daemon_proxy_on_command
1001
+ ;;
1002
+ docker-proxy-off)
1003
+ daemon_proxy_off_command
1004
+ ;;
1005
+ run)
1006
+ run_command "$@"
1007
+ ;;
1008
+ test)
1009
+ local url="https://www.google.com/generate_204"
1010
+ while [[ $# -gt 0 ]]; do
1011
+ case "$1" in
1012
+ --url) url="$2"; shift 2 ;;
1013
+ *) die "Unknown test option: $1" ;;
1014
+ esac
1015
+ done
1016
+ test_command "$url"
1017
+ ;;
1018
+ print-env)
1019
+ print_env_command
1020
+ ;;
1021
+ uninstall)
1022
+ local purge="false"
1023
+ while [[ $# -gt 0 ]]; do
1024
+ case "$1" in
1025
+ --purge) purge="true"; shift ;;
1026
+ *) die "Unknown uninstall option: $1" ;;
1027
+ esac
1028
+ done
1029
+ uninstall_command "$purge"
1030
+ ;;
1031
+ *)
1032
+ die "Unknown command: $command"
1033
+ ;;
1034
+ esac
1035
+ }
1036
+
1037
+ main "$@"
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const repoRoot = resolve(packageRoot, '../../..');
9
+ const source = resolve(repoRoot, 'scripts/mihomo-client.sh');
10
+ const target = resolve(packageRoot, 'resources/mihomo-client.sh');
11
+
12
+ if (!existsSync(source)) {
13
+ if (existsSync(target)) {
14
+ console.log(`Using existing packaged script at ${target}`);
15
+ process.exit(0);
16
+ }
17
+ throw new Error(`Cannot package @qpjoy/tunnel-cli; missing ${source}`);
18
+ }
19
+
20
+ mkdirSync(dirname(target), { recursive: true });
21
+ copyFileSync(source, target);
22
+ chmodSync(target, 0o755);
23
+
24
+ console.log(`Copied ${source} -> ${target}`);