mihomo-cli 2.10.0 → 3.2.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/dist/index.js CHANGED
@@ -1,73 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/paths.ts
4
- import fs from "fs";
5
- import os from "os";
6
- import path from "path";
7
- function getUserDataDir() {
8
- if (process.env.MIHOMO_CLI_DIR) {
9
- return process.env.MIHOMO_CLI_DIR;
10
- }
11
- return path.join(os.homedir(), ".mihomo-cli");
12
- }
13
- var USER_DATA_DIR = getUserDataDir();
14
- var DIRS = {
15
- kernel: path.join(USER_DATA_DIR, "kernel"),
16
- subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
17
- logs: path.join(USER_DATA_DIR, "logs"),
18
- data: path.join(USER_DATA_DIR, "data"),
19
- runtime: path.join(USER_DATA_DIR, "runtime")
20
- };
21
- var PATHS = {
22
- mihomoBinary: path.join(DIRS.kernel, "mihomo"),
23
- settingsFile: path.join(USER_DATA_DIR, "settings.json"),
24
- subscriptionsCacheFile: path.join(DIRS.subscriptions, "cache.json"),
25
- configFile: path.join(DIRS.runtime, "config.yaml"),
26
- logFile: path.join(DIRS.logs, "mihomo.log"),
27
- pidFile: path.join(DIRS.runtime, "pid"),
28
- configStage1Subscription: path.join(DIRS.runtime, "1.subscription.yaml"),
29
- configStage2Overwrite: path.join(DIRS.runtime, "2.overwrite.yaml"),
30
- configStage3System: path.join(DIRS.runtime, "3.system.yaml")
31
- };
32
- var DIRECTORY_TARGETS = {
33
- root: { path: null, label: "\u6839\u76EE\u5F55" },
34
- subs: { path: DIRS.subscriptions, label: "\u8BA2\u9605\u76EE\u5F55" },
35
- logs: { path: DIRS.logs, label: "\u65E5\u5FD7\u76EE\u5F55" },
36
- data: { path: DIRS.data, label: "mihomo \u6570\u636E\u76EE\u5F55" },
37
- runtime: { path: DIRS.runtime, label: "\u8FD0\u884C\u65F6\u76EE\u5F55" },
38
- kernel: { path: DIRS.kernel, label: "\u5185\u6838\u76EE\u5F55" }
39
- };
40
- function ensureDirs() {
41
- for (const dir of Object.values(DIRS)) {
42
- if (!fs.existsSync(dir)) {
43
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
44
- }
45
- }
46
- }
47
- function atomicWriteFileSync(filePath, content, options) {
48
- const tmp = `${filePath}.${process.pid}.tmp`;
49
- try {
50
- fs.writeFileSync(tmp, content, options);
51
- fs.renameSync(tmp, filePath);
52
- } catch (e) {
53
- try {
54
- fs.unlinkSync(tmp);
55
- } catch {
56
- }
57
- throw e;
58
- }
59
- }
60
- function rmrf(dir) {
61
- fs.rmSync(dir, { recursive: true, force: true });
62
- }
63
-
64
- // src/process.ts
65
- import { spawn, spawnSync as spawnSync3 } from "child_process";
66
- import fs5 from "fs";
67
- import path3 from "path";
68
-
69
3
  // src/config.ts
70
- import { spawnSync } from "child_process";
4
+ import { spawnSync as spawnSync2 } from "child_process";
71
5
  import fs4 from "fs";
72
6
 
73
7
  // node_modules/js-yaml/dist/js-yaml.mjs
@@ -2989,6 +2923,11 @@ var UI_URLS = {
2989
2923
  dash: "https://metacubex.github.io/metacubexd",
2990
2924
  yacd: "https://yacd.metacubex.one"
2991
2925
  };
2926
+ var LAUNCH_DAEMON_LABEL = process.env.MIHOMO_CLI_DAEMON_LABEL || "com.mihomo-cli.daemon";
2927
+ var CONTROLLER_PORT = 9090;
2928
+ var CONTROLLER_ADDR = `127.0.0.1:${CONTROLLER_PORT}`;
2929
+ var CONTROLLER_BASE_URL = `http://${CONTROLLER_ADDR}`;
2930
+ var TEST_CONTROLLER_ADDR = "127.0.0.1:29090";
2992
2931
  var TUN_CONFIG = {
2993
2932
  tun: {
2994
2933
  enable: true,
@@ -3002,14 +2941,14 @@ var TUN_CONFIG = {
3002
2941
  var TEST_CONFIG = {
3003
2942
  "mixed-port": 27890,
3004
2943
  "allow-lan": false,
3005
- "external-controller": "127.0.0.1:29090",
2944
+ "external-controller": TEST_CONTROLLER_ADDR,
3006
2945
  "log-level": "error",
3007
2946
  "geodata-mode": true
3008
2947
  };
3009
2948
  var BASE_CONFIG = {
3010
2949
  "mixed-port": 7890,
3011
2950
  "allow-lan": false,
3012
- "external-controller": "127.0.0.1:9090",
2951
+ "external-controller": CONTROLLER_ADDR,
3013
2952
  "unified-delay": true,
3014
2953
  "tcp-concurrent": true,
3015
2954
  "geo-auto-update": true,
@@ -3038,10 +2977,74 @@ var AUTO_CLEAN_THRESHOLD_GITHUB = 50;
3038
2977
 
3039
2978
  // src/overwrite.ts
3040
2979
  import fs3 from "fs";
3041
- import path2 from "path";
2980
+ import path3 from "path";
2981
+
2982
+ // src/paths.ts
2983
+ import fs from "fs";
2984
+ import os from "os";
2985
+ import path from "path";
2986
+ function getUserDataDir() {
2987
+ if (process.env.MIHOMO_CLI_DIR) {
2988
+ return process.env.MIHOMO_CLI_DIR;
2989
+ }
2990
+ return path.join(os.homedir(), ".mihomo-cli");
2991
+ }
2992
+ var USER_DATA_DIR = getUserDataDir();
2993
+ var DIRS = {
2994
+ kernel: path.join(USER_DATA_DIR, "kernel"),
2995
+ subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
2996
+ logs: path.join(USER_DATA_DIR, "logs"),
2997
+ data: path.join(USER_DATA_DIR, "data"),
2998
+ runtime: path.join(USER_DATA_DIR, "runtime")
2999
+ };
3000
+ var PATHS = {
3001
+ mihomoBinary: path.join(DIRS.kernel, "mihomo"),
3002
+ settingsFile: path.join(USER_DATA_DIR, "settings.json"),
3003
+ subscriptionsCacheFile: path.join(DIRS.subscriptions, "cache.json"),
3004
+ configFile: path.join(DIRS.runtime, "config.yaml"),
3005
+ logFile: path.join(DIRS.logs, "mihomo.log"),
3006
+ pidFile: path.join(DIRS.runtime, "pid"),
3007
+ configStage1Subscription: path.join(DIRS.runtime, "1.subscription.yaml"),
3008
+ configStage2Overwrite: path.join(DIRS.runtime, "2.overwrite.yaml"),
3009
+ configStage3System: path.join(DIRS.runtime, "3.system.yaml"),
3010
+ // launchd LaunchDaemon plist 位于系统级 /Library/LaunchDaemons/,root:wheel 拥有,与 homedir / MIHOMO_CLI_DIR 无关
3011
+ launchDaemonPlist: path.join("/Library/LaunchDaemons", `${LAUNCH_DAEMON_LABEL}.plist`)
3012
+ };
3013
+ var DIRECTORY_TARGETS = {
3014
+ root: { path: null, label: "\u6839\u76EE\u5F55" },
3015
+ subs: { path: DIRS.subscriptions, label: "\u8BA2\u9605\u76EE\u5F55" },
3016
+ logs: { path: DIRS.logs, label: "\u65E5\u5FD7\u76EE\u5F55" },
3017
+ data: { path: DIRS.data, label: "mihomo \u6570\u636E\u76EE\u5F55" },
3018
+ runtime: { path: DIRS.runtime, label: "\u8FD0\u884C\u65F6\u76EE\u5F55" },
3019
+ kernel: { path: DIRS.kernel, label: "\u5185\u6838\u76EE\u5F55" }
3020
+ };
3021
+ function ensureDirs() {
3022
+ for (const dir of Object.values(DIRS)) {
3023
+ if (!fs.existsSync(dir)) {
3024
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
3025
+ }
3026
+ }
3027
+ }
3028
+ function atomicWriteFileSync(filePath, content, options) {
3029
+ const tmp = `${filePath}.${process.pid}.tmp`;
3030
+ try {
3031
+ fs.writeFileSync(tmp, content, options);
3032
+ fs.renameSync(tmp, filePath);
3033
+ } catch (e) {
3034
+ try {
3035
+ fs.unlinkSync(tmp);
3036
+ } catch {
3037
+ }
3038
+ throw e;
3039
+ }
3040
+ }
3041
+ function rmrf(dir) {
3042
+ fs.rmSync(dir, { recursive: true, force: true });
3043
+ }
3042
3044
 
3043
3045
  // src/settings.ts
3044
3046
  import fs2 from "fs";
3047
+ import path2 from "path";
3045
3048
  var settingsCache = null;
3046
3049
  function readSettings() {
3047
3050
  if (settingsCache !== null) return settingsCache;
@@ -3145,12 +3148,10 @@ function addSubscription(url, name = "default") {
3145
3148
  validateSubscriptionName(name);
3146
3149
  const settings = readSettings();
3147
3150
  const subs = [...settings.subscriptions || []];
3148
- const existingIndex = subs.findIndex((s) => s.name === name);
3149
- if (existingIndex >= 0) {
3150
- subs[existingIndex] = { name, url };
3151
- } else {
3152
- subs.push({ name, url });
3151
+ if (subs.some((s) => s.name === name)) {
3152
+ throw new Error(`\u8BA2\u9605 "${name}" \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E2A\u540D\u79F0\uFF08mihomo sub add <url> <\u540D\u79F0>\uFF09\uFF0C\u6216\u5148\u5220\u9664\uFF08mihomo sub remove ${name}\uFF09`);
3153
3153
  }
3154
+ subs.push({ name, url });
3154
3155
  const updates = { subscriptions: subs };
3155
3156
  if (!settings.active_subscription && subs.length === 1) {
3156
3157
  updates.active_subscription = name;
@@ -3175,7 +3176,10 @@ function removeSubscription(name) {
3175
3176
  delete cache[name];
3176
3177
  writeSubscriptionCache(cache);
3177
3178
  }
3178
- fs2.rmSync(getSubscriptionRawConfigPath(name), { force: true });
3179
+ try {
3180
+ fs2.rmSync(getSubscriptionRawConfigPath(name), { force: true });
3181
+ } catch {
3182
+ }
3179
3183
  return switchedTo;
3180
3184
  }
3181
3185
  function setDefaultSubscription(name) {
@@ -3188,7 +3192,10 @@ function setDefaultSubscription(name) {
3188
3192
  return true;
3189
3193
  }
3190
3194
  function getSubscriptionRawConfigPath(subName) {
3191
- return `${DIRS.subscriptions}/${subName}.yaml`;
3195
+ if (!SAFE_NAME_RE.test(subName)) {
3196
+ throw new Error(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${subName}"`);
3197
+ }
3198
+ return path2.join(DIRS.subscriptions, `${subName}.yaml`);
3192
3199
  }
3193
3200
  function saveSubscriptionRawConfig(subName, content) {
3194
3201
  ensureDirs();
@@ -3286,17 +3293,20 @@ function isOverwriteEnabled() {
3286
3293
  function setOverwriteEnabled(enabled) {
3287
3294
  writeSettings({ overwrite_enabled: enabled });
3288
3295
  }
3296
+ function isOverwriteFilename(filename) {
3297
+ return filename === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(filename);
3298
+ }
3289
3299
  function loadOverwriteFile() {
3290
3300
  const dir = USER_DATA_DIR;
3291
3301
  if (!fs3.existsSync(dir)) return [];
3292
- const files = fs3.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).sort((a, b) => {
3302
+ const files = fs3.readdirSync(dir).filter(isOverwriteFilename).sort((a, b) => {
3293
3303
  if (a === "overwrite.yaml") return -1;
3294
3304
  if (b === "overwrite.yaml") return 1;
3295
3305
  return a.localeCompare(b);
3296
3306
  });
3297
3307
  const results = [];
3298
3308
  for (const file of files) {
3299
- const filePath = path2.join(dir, file);
3309
+ const filePath = path3.join(dir, file);
3300
3310
  try {
3301
3311
  const content = fs3.readFileSync(filePath, "utf8");
3302
3312
  const parsed = load(content);
@@ -3333,57 +3343,267 @@ function listOverwriteFile() {
3333
3343
  };
3334
3344
  }
3335
3345
 
3336
- // src/config.ts
3337
- function parseYamlOrJson(content, errorMsg) {
3338
- if (!content?.trim()) {
3339
- throw new Error(`${errorMsg || "\u5185\u5BB9"}\u4E3A\u7A7A`);
3346
+ // src/utils.ts
3347
+ import { spawnSync } from "child_process";
3348
+ import { createRequire } from "module";
3349
+ var require2 = createRequire(import.meta.url);
3350
+ var pkg = require2("../package.json");
3351
+ var VERSION = pkg.version;
3352
+ var sleepBuf = new Int32Array(new SharedArrayBuffer(4));
3353
+ var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
3354
+ function colorize(code, str) {
3355
+ if (NO_COLOR) return String(str);
3356
+ return `${code + String(str)}\x1B[0m`;
3357
+ }
3358
+ var colors = {
3359
+ bold: (s) => colorize("\x1B[1m", s),
3360
+ red: (s) => colorize("\x1B[31m", s),
3361
+ green: (s) => colorize("\x1B[32m", s),
3362
+ yellow: (s) => colorize("\x1B[33m", s),
3363
+ cyan: (s) => colorize("\x1B[36m", s),
3364
+ gray: (s) => colorize("\x1B[90m", s)
3365
+ };
3366
+ function sleepSync(ms) {
3367
+ Atomics.wait(sleepBuf, 0, 0, ms);
3368
+ }
3369
+ function sleep(ms) {
3370
+ return new Promise((resolve) => setTimeout(resolve, ms));
3371
+ }
3372
+ function escapeRegExp(s) {
3373
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3374
+ }
3375
+ function shellQuote(s) {
3376
+ return `'${s.replace(/'/g, "'\\''")}'`;
3377
+ }
3378
+ var TimeoutError = class extends Error {
3379
+ constructor() {
3380
+ super("timeout");
3381
+ this.name = "TimeoutError";
3340
3382
  }
3383
+ };
3384
+ function withTimeout(promise, ms) {
3385
+ return new Promise((resolve, reject) => {
3386
+ const timer = setTimeout(() => reject(new TimeoutError()), ms);
3387
+ promise.then(
3388
+ (v) => {
3389
+ clearTimeout(timer);
3390
+ resolve(v);
3391
+ },
3392
+ (e) => {
3393
+ clearTimeout(timer);
3394
+ reject(e);
3395
+ }
3396
+ );
3397
+ });
3398
+ }
3399
+ function formatBytes(bytes) {
3400
+ if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
3401
+ const num = Number(bytes);
3402
+ if (!Number.isFinite(num) || num < 0) return "\u672A\u77E5";
3403
+ if (num === 0) return "0 B";
3404
+ const k = 1024;
3405
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
3406
+ const i = Math.min(Math.floor(Math.log(num) / Math.log(k)), sizes.length - 1);
3407
+ return `${parseFloat((num / k ** i).toFixed(2))} ${sizes[i]}`;
3408
+ }
3409
+ function formatTimestamp(ts) {
3410
+ if (ts === void 0 || ts === null) return "\u672A\u77E5";
3341
3411
  try {
3342
- const result = load(content);
3343
- if (result != null && typeof result === "object" && !Array.isArray(result)) return result;
3412
+ return new Date(ts * 1e3).toLocaleString("zh-CN");
3344
3413
  } catch {
3414
+ return "\u672A\u77E5";
3345
3415
  }
3416
+ }
3417
+ function formatLocalTimestamp(d = /* @__PURE__ */ new Date()) {
3418
+ const p = (n) => String(n).padStart(2, "0");
3419
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}`;
3420
+ }
3421
+ function formatDate(dateOrIso) {
3422
+ if (dateOrIso === void 0 || dateOrIso === null) return "\u672A\u77E5";
3346
3423
  try {
3347
- return JSON.parse(content);
3424
+ const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
3425
+ if (Number.isNaN(d.getTime())) return "\u672A\u77E5";
3426
+ return d.toLocaleString("zh-CN");
3348
3427
  } catch {
3349
- throw new Error(`${errorMsg || "\u5185\u5BB9"}\u683C\u5F0F\u9519\u8BEF\uFF0C\u65E0\u6CD5\u89E3\u6790\u4E3A YAML \u6216 JSON`);
3428
+ return "\u672A\u77E5";
3350
3429
  }
3351
3430
  }
3352
- function collectOverwriteProxyNames(overwriteFiles) {
3353
- const names = [];
3354
- for (const file of overwriteFiles) {
3355
- for (const [key, value] of Object.entries(file.config)) {
3356
- if ((key === "+proxies" || key === "proxies+") && Array.isArray(value)) {
3357
- for (const proxy of value) {
3358
- if (proxy && typeof proxy === "object" && "name" in proxy) {
3359
- names.push(proxy.name);
3360
- }
3361
- }
3362
- }
3363
- }
3364
- }
3365
- return names;
3431
+ function hasFlag(args, short, long) {
3432
+ return !!args && (args.includes(short) || args.includes(long));
3366
3433
  }
3367
- function excludeOverwriteProxiesFromIncludeAll(config, overwriteFiles) {
3368
- const injectedNames = collectOverwriteProxyNames(overwriteFiles);
3369
- if (injectedNames.length === 0) return;
3370
- const groups = config["proxy-groups"];
3371
- if (!groups) return;
3372
- const excludePattern = injectedNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
3373
- for (const group of groups) {
3374
- if (!group["include-all"] && !group["include-all-proxies"]) continue;
3375
- const existing = group["exclude-filter"];
3376
- if (existing) {
3377
- group["exclude-filter"] = `${existing}|${excludePattern}`;
3378
- } else {
3379
- group["exclude-filter"] = excludePattern;
3434
+ function parseIntArg(args, short, long, defaultValue) {
3435
+ if (!args) return defaultValue;
3436
+ for (let i = 0; i < args.length; i++) {
3437
+ if (args[i] === short || args[i] === long) {
3438
+ if (i + 1 < args.length) {
3439
+ const val = parseInt(args[i + 1], 10);
3440
+ return Number.isNaN(val) ? defaultValue : val;
3441
+ }
3380
3442
  }
3381
3443
  }
3444
+ return defaultValue;
3382
3445
  }
3383
- var BUILTIN_PROXY_NAMES = /* @__PURE__ */ new Set(["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"]);
3384
- function deduplicateByName(items) {
3385
- const names = /* @__PURE__ */ new Set();
3386
- const duplicates = [];
3446
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["-t", "--timeout", "-j", "--concurrency", "-r", "--rounds", "-n", "--lines", "-u", "--update-timeout"]);
3447
+ function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
3448
+ if (!args) return null;
3449
+ for (let i = startIdx; i < args.length; i++) {
3450
+ const a = args[i];
3451
+ if (a.startsWith("-")) {
3452
+ if (valueFlags.has(a)) i++;
3453
+ continue;
3454
+ }
3455
+ return a;
3456
+ }
3457
+ return null;
3458
+ }
3459
+ function isProcessRunning(pid) {
3460
+ if (!pid) return false;
3461
+ try {
3462
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: 5e3 });
3463
+ return (result.stdout || "").trim().length > 0;
3464
+ } catch {
3465
+ return false;
3466
+ }
3467
+ }
3468
+ function isProcessRoot(pid) {
3469
+ if (!pid) return false;
3470
+ try {
3471
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: 5e3 });
3472
+ return (result.stdout || "").trim() === "0";
3473
+ } catch {
3474
+ return false;
3475
+ }
3476
+ }
3477
+ function createHttpClient(options = {}) {
3478
+ const { timeout = 6e4 } = options;
3479
+ return {
3480
+ async get(url, config) {
3481
+ const controller = new AbortController();
3482
+ const timer = setTimeout(() => controller.abort(), timeout);
3483
+ const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
3484
+ try {
3485
+ const response = await fetch(url, {
3486
+ signal,
3487
+ headers: { "User-Agent": `mihomo-cli/${VERSION}` }
3488
+ });
3489
+ if (!response.ok) {
3490
+ const error = new Error(`HTTP ${response.status}`);
3491
+ error.response = { status: response.status };
3492
+ try {
3493
+ error.response.data = await response.json();
3494
+ } catch {
3495
+ }
3496
+ throw error;
3497
+ }
3498
+ const data = config?.responseType === "json" ? await response.json() : await response.text();
3499
+ return { data, headers: response.headers, status: response.status };
3500
+ } finally {
3501
+ clearTimeout(timer);
3502
+ }
3503
+ }
3504
+ };
3505
+ }
3506
+ function normalizeMirrorUrl(val) {
3507
+ if (!val) return null;
3508
+ if (val === "direct" || val === "no" || val === "none") return null;
3509
+ let url = val;
3510
+ if (!url.startsWith("http")) {
3511
+ url = `https://${url}`;
3512
+ }
3513
+ if (!url.endsWith("/")) {
3514
+ url += "/";
3515
+ }
3516
+ return url;
3517
+ }
3518
+ function parseMirrorArg(args) {
3519
+ if (!args || args.length < 2) {
3520
+ return { mirror: null, isOverride: false, type: "download" };
3521
+ }
3522
+ if (args.includes("--no-mirror") || args.includes("--direct")) {
3523
+ return { mirror: null, isOverride: true, type: "download" };
3524
+ }
3525
+ const mirrorAllIdx = args.indexOf("--mirror-all");
3526
+ if (mirrorAllIdx >= 0) {
3527
+ const nextArg = args[mirrorAllIdx + 1];
3528
+ if (!nextArg || nextArg.startsWith("-")) {
3529
+ return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "all" };
3530
+ }
3531
+ return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "all" };
3532
+ }
3533
+ const mirrorIdx = args.indexOf("--mirror");
3534
+ if (mirrorIdx >= 0) {
3535
+ const nextArg = args[mirrorIdx + 1];
3536
+ if (!nextArg || nextArg.startsWith("-")) {
3537
+ return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "download" };
3538
+ }
3539
+ return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "download" };
3540
+ }
3541
+ return { mirror: null, isOverride: false, type: "download" };
3542
+ }
3543
+ function isProxyValid(proxy) {
3544
+ if (!proxy.name || !proxy.server || !proxy.port) return false;
3545
+ if (!proxy.type) return false;
3546
+ if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3547
+ const pw = String(proxy.password || "");
3548
+ if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
3549
+ }
3550
+ return true;
3551
+ }
3552
+
3553
+ // src/config.ts
3554
+ function parseYamlOrJson(content, errorMsg) {
3555
+ if (!content?.trim()) {
3556
+ throw new Error(`${errorMsg || "\u5185\u5BB9"}\u4E3A\u7A7A`);
3557
+ }
3558
+ try {
3559
+ const result = load(content);
3560
+ if (result != null && typeof result === "object" && !Array.isArray(result)) return result;
3561
+ } catch {
3562
+ }
3563
+ try {
3564
+ return JSON.parse(content);
3565
+ } catch {
3566
+ throw new Error(`${errorMsg || "\u5185\u5BB9"}\u683C\u5F0F\u9519\u8BEF\uFF0C\u65E0\u6CD5\u89E3\u6790\u4E3A YAML \u6216 JSON`);
3567
+ }
3568
+ }
3569
+ function dumpYaml(obj) {
3570
+ return dump(obj, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
3571
+ }
3572
+ function collectOverwriteProxyNames(overwriteFiles) {
3573
+ const names = [];
3574
+ for (const file of overwriteFiles) {
3575
+ for (const [key, value] of Object.entries(file.config)) {
3576
+ if ((key === "+proxies" || key === "proxies+") && Array.isArray(value)) {
3577
+ for (const proxy of value) {
3578
+ if (proxy && typeof proxy === "object" && "name" in proxy) {
3579
+ names.push(proxy.name);
3580
+ }
3581
+ }
3582
+ }
3583
+ }
3584
+ }
3585
+ return names;
3586
+ }
3587
+ function excludeOverwriteProxiesFromIncludeAll(config, overwriteFiles) {
3588
+ const injectedNames = collectOverwriteProxyNames(overwriteFiles);
3589
+ if (injectedNames.length === 0) return;
3590
+ const groups = config["proxy-groups"];
3591
+ if (!groups) return;
3592
+ const excludePattern = injectedNames.map((n) => escapeRegExp(n)).join("|");
3593
+ for (const group of groups) {
3594
+ if (!group["include-all"] && !group["include-all-proxies"]) continue;
3595
+ const existing = group["exclude-filter"];
3596
+ if (existing) {
3597
+ group["exclude-filter"] = `${existing}|${excludePattern}`;
3598
+ } else {
3599
+ group["exclude-filter"] = excludePattern;
3600
+ }
3601
+ }
3602
+ }
3603
+ var BUILTIN_PROXY_NAMES = /* @__PURE__ */ new Set(["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"]);
3604
+ function deduplicateByName(items) {
3605
+ const names = /* @__PURE__ */ new Set();
3606
+ const duplicates = [];
3387
3607
  const result = items.filter((item) => {
3388
3608
  if (names.has(item.name)) {
3389
3609
  duplicates.push(item.name);
@@ -3394,6 +3614,15 @@ function deduplicateByName(items) {
3394
3614
  });
3395
3615
  return { result, names, duplicates };
3396
3616
  }
3617
+ function getRuleTarget(rule) {
3618
+ const parts = rule.split(",");
3619
+ if (parts.length < 2) return "";
3620
+ const last = parts[parts.length - 1].trim();
3621
+ if (last.toLowerCase() === "no-resolve" && parts.length >= 3) {
3622
+ return parts[parts.length - 2].trim();
3623
+ }
3624
+ return last;
3625
+ }
3397
3626
  function validateConfig(config) {
3398
3627
  const warnings = [];
3399
3628
  const proxies = config.proxies || [];
@@ -3438,9 +3667,7 @@ function validateConfig(config) {
3438
3667
  if (rules.length > 0) {
3439
3668
  const removedRules = [];
3440
3669
  config.rules = rules.filter((rule) => {
3441
- const parts = rule.split(",");
3442
- if (parts.length < 2) return true;
3443
- const target = parts[parts.length - 1].trim();
3670
+ const target = getRuleTarget(rule);
3444
3671
  if (!target || validNames.has(target)) return true;
3445
3672
  removedRules.push(rule);
3446
3673
  return false;
@@ -3487,6 +3714,8 @@ function buildConfig(subRawContent, mode) {
3487
3714
  if (Object.keys(dns).length > 0) {
3488
3715
  systemConfig.dns = dns;
3489
3716
  }
3717
+ } else {
3718
+ delete withOverwrites.tun;
3490
3719
  }
3491
3720
  const merged = { ...withOverwrites, ...systemConfig };
3492
3721
  if (systemConfig.dns) {
@@ -3509,20 +3738,19 @@ function buildConfig(subRawContent, mode) {
3509
3738
  }
3510
3739
  function writeMihomoConfig(configObj) {
3511
3740
  ensureDirs();
3512
- const content = dump(configObj, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
3741
+ const content = dumpYaml(configObj);
3513
3742
  atomicWriteFileSync(PATHS.configFile, content, { mode: 384 });
3514
3743
  }
3515
3744
  function writeDebugConfig(buildResult) {
3516
3745
  ensureDirs();
3517
- const dumpOpts = { indent: 2, lineWidth: -1, schema: CORE_SCHEMA };
3518
- fs4.writeFileSync(PATHS.configStage1Subscription, dump(buildResult.subscriptionConfig, dumpOpts), { mode: 384 });
3746
+ fs4.writeFileSync(PATHS.configStage1Subscription, dumpYaml(buildResult.subscriptionConfig), { mode: 384 });
3519
3747
  const overwriteMerged = {};
3520
3748
  for (const f of buildResult.overwriteFiles) {
3521
3749
  Object.assign(overwriteMerged, f.config);
3522
3750
  }
3523
- const overwriteContent = buildResult.overwriteFiles.length > 0 ? dump(overwriteMerged, dumpOpts) : "# overwrite \u5DF2\u7981\u7528\u6216\u65E0\u8986\u5199\u6587\u4EF6\n";
3751
+ const overwriteContent = buildResult.overwriteFiles.length > 0 ? dumpYaml(overwriteMerged) : "# overwrite \u5DF2\u7981\u7528\u6216\u65E0\u8986\u5199\u6587\u4EF6\n";
3524
3752
  fs4.writeFileSync(PATHS.configStage2Overwrite, overwriteContent, { mode: 384 });
3525
- fs4.writeFileSync(PATHS.configStage3System, dump(buildResult.systemConfig, dumpOpts), { mode: 384 });
3753
+ fs4.writeFileSync(PATHS.configStage3System, dumpYaml(buildResult.systemConfig), { mode: 384 });
3526
3754
  }
3527
3755
  function hasConfig() {
3528
3756
  return fs4.existsSync(PATHS.configFile);
@@ -3562,7 +3790,7 @@ function getKernelVersion() {
3562
3790
  }
3563
3791
  if (kernelVersionCached) return kernelVersionCache;
3564
3792
  try {
3565
- const result = spawnSync(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
3793
+ const result = spawnSync2(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
3566
3794
  const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
3567
3795
  if (output) {
3568
3796
  const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
@@ -3581,219 +3809,102 @@ function clearKernelVersionCache() {
3581
3809
  kernelVersionCached = false;
3582
3810
  }
3583
3811
 
3584
- // src/utils.ts
3585
- import { spawnSync as spawnSync2 } from "child_process";
3586
- import { createRequire } from "module";
3587
- var require2 = createRequire(import.meta.url);
3588
- var pkg = require2("../package.json");
3589
- var VERSION = pkg.version;
3590
- var sleepBuf = new Int32Array(new SharedArrayBuffer(4));
3591
- var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
3592
- function colorize(code, str) {
3593
- if (NO_COLOR) return String(str);
3594
- return `${code + String(str)}\x1B[0m`;
3812
+ // src/commands/help.ts
3813
+ function printShortHelp() {
3814
+ console.log(`
3815
+ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
3816
+ `);
3817
+ console.log(
3818
+ `\u5E38\u7528\u547D\u4EE4:
3819
+ ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
3820
+ ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
3821
+ ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
3822
+ ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
3823
+ `
3824
+ );
3595
3825
  }
3596
- var colors = {
3597
- bold: (s) => colorize("\x1B[1m", s),
3598
- red: (s) => colorize("\x1B[31m", s),
3599
- green: (s) => colorize("\x1B[32m", s),
3600
- yellow: (s) => colorize("\x1B[33m", s),
3601
- cyan: (s) => colorize("\x1B[36m", s),
3602
- gray: (s) => colorize("\x1B[90m", s)
3603
- };
3604
- function sleepSync(ms) {
3605
- Atomics.wait(sleepBuf, 0, 0, ms);
3826
+ var GROUP_TITLES = [
3827
+ ["control", "\u63A7\u5236:"],
3828
+ ["interface", "\u754C\u9762:"],
3829
+ ["subscription", "\u8BA2\u9605:"],
3830
+ ["config", "\u914D\u7F6E:"],
3831
+ ["system", "\u7CFB\u7EDF:"]
3832
+ ];
3833
+ function printHelp(commands) {
3834
+ const lines = [`
3835
+ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}`, "", "\u547D\u4EE4\u522B\u540D: mihomo, mhm, mh", "", "\u7528\u6CD5:", " mihomo <\u547D\u4EE4> [\u9009\u9879]"];
3836
+ for (const [group, title] of GROUP_TITLES) {
3837
+ const usageLines = commands.filter((c) => c.group === group).flatMap((c) => c.usage);
3838
+ if (usageLines.length === 0) continue;
3839
+ lines.push("", colors.cyan(title));
3840
+ for (const u of usageLines) {
3841
+ lines.push(u.startsWith(" ") ? ` ${u}` : ` ${boldFirstToken(u)}`);
3842
+ }
3843
+ }
3844
+ const meta = commands.filter((c) => c.group === "meta").flatMap((c) => c.usage);
3845
+ if (meta.length > 0) {
3846
+ lines.push("", colors.cyan("\u5143:"));
3847
+ for (const u of meta) lines.push(` ${boldFirstToken(u)}`);
3848
+ }
3849
+ lines.push(
3850
+ "",
3851
+ `${colors.cyan("\u793A\u4F8B:")}`,
3852
+ " mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F",
3853
+ " mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F",
3854
+ " mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605",
3855
+ " mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)",
3856
+ " mihomo daemon on # \u5F00\u542F\u4FDD\u6D3B\uFF08\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF09",
3857
+ " mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)",
3858
+ " mihomo ui # \u6253\u5F00 Web UI",
3859
+ "",
3860
+ `${colors.cyan("\u5FEB\u6377\u547D\u4EE4:")}`,
3861
+ " tun = start tun use = sub use on/off = ow on/off open = dir open",
3862
+ " up = start down = stop upd/upgrade = update",
3863
+ "",
3864
+ `${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}`,
3865
+ " mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)",
3866
+ " tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo",
3867
+ "",
3868
+ `${colors.cyan("\u6570\u636E\u76EE\u5F55:")}`,
3869
+ " \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E",
3870
+ ` \u9ED8\u8BA4: ${USER_DATA_DIR}`
3871
+ );
3872
+ console.log(lines.join("\n"));
3606
3873
  }
3607
- function sleep(ms) {
3608
- return new Promise((resolve) => setTimeout(resolve, ms));
3874
+ function boldFirstToken(usage) {
3875
+ const spaceIdx = usage.indexOf(" ");
3876
+ if (spaceIdx < 0) return colors.bold(usage);
3877
+ return `${colors.bold(usage.slice(0, spaceIdx))}${usage.slice(spaceIdx)}`;
3609
3878
  }
3610
- var TimeoutError = class extends Error {
3611
- constructor() {
3612
- super("timeout");
3613
- this.name = "TimeoutError";
3614
- }
3615
- };
3616
- function withTimeout(promise, ms) {
3617
- return new Promise((resolve, reject) => {
3618
- const timer = setTimeout(() => reject(new TimeoutError()), ms);
3619
- promise.then(
3620
- (v) => {
3621
- clearTimeout(timer);
3622
- resolve(v);
3623
- },
3624
- (e) => {
3625
- clearTimeout(timer);
3626
- reject(e);
3627
- }
3628
- );
3629
- });
3879
+ function printVersion() {
3880
+ const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
3881
+ console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
3882
+ console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
3883
+ console.log(`${colors.gray("\u6570\u636E\u76EE\u5F55: ")}${USER_DATA_DIR}`);
3630
3884
  }
3631
- function formatBytes(bytes) {
3632
- if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
3633
- const num = Number(bytes);
3634
- if (!Number.isFinite(num) || num < 0) return "\u672A\u77E5";
3635
- if (num === 0) return "0 B";
3636
- const k = 1024;
3637
- const sizes = ["B", "KB", "MB", "GB", "TB"];
3638
- const i = Math.min(Math.floor(Math.log(num) / Math.log(k)), sizes.length - 1);
3639
- return `${parseFloat((num / k ** i).toFixed(2))} ${sizes[i]}`;
3640
- }
3641
- function formatTimestamp(ts) {
3642
- if (ts === void 0 || ts === null) return "\u672A\u77E5";
3643
- try {
3644
- return new Date(ts * 1e3).toLocaleString("zh-CN");
3645
- } catch {
3646
- return "\u672A\u77E5";
3647
- }
3648
- }
3649
- function formatDate(dateOrIso) {
3650
- if (dateOrIso === void 0 || dateOrIso === null) return "\u672A\u77E5";
3651
- try {
3652
- const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
3653
- if (Number.isNaN(d.getTime())) return "\u672A\u77E5";
3654
- return d.toLocaleString("zh-CN");
3655
- } catch {
3656
- return "\u672A\u77E5";
3657
- }
3658
- }
3659
- function hasFlag(args, short, long) {
3660
- return !!args && (args.includes(short) || args.includes(long));
3661
- }
3662
- function parseIntArg(args, short, long, defaultValue) {
3663
- if (!args) return defaultValue;
3664
- for (let i = 0; i < args.length; i++) {
3665
- if (args[i] === short || args[i] === long) {
3666
- if (i + 1 < args.length) {
3667
- const val = parseInt(args[i + 1], 10);
3668
- return Number.isNaN(val) ? defaultValue : val;
3669
- }
3670
- }
3671
- }
3672
- return defaultValue;
3673
- }
3674
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["-t", "--timeout", "-j", "--concurrency", "-r", "--rounds", "-n", "--lines", "-u", "--update-timeout"]);
3675
- function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
3676
- if (!args) return null;
3677
- for (let i = startIdx; i < args.length; i++) {
3678
- const a = args[i];
3679
- if (a.startsWith("-")) {
3680
- if (valueFlags.has(a)) i++;
3681
- continue;
3682
- }
3683
- return a;
3684
- }
3685
- return null;
3686
- }
3687
- function isProcessRunning(pid) {
3688
- if (!pid) return false;
3689
- try {
3690
- const result = spawnSync2("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: 5e3 });
3691
- return (result.stdout || "").trim().length > 0;
3692
- } catch {
3693
- return false;
3694
- }
3695
- }
3696
- function isProcessRoot(pid) {
3697
- if (!pid) return false;
3698
- try {
3699
- const result = spawnSync2("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: 5e3 });
3700
- return (result.stdout || "").trim() === "0";
3701
- } catch {
3702
- return false;
3703
- }
3704
- }
3705
- function createHttpClient(options = {}) {
3706
- const { timeout = 6e4 } = options;
3707
- return {
3708
- async get(url, config) {
3709
- const controller = new AbortController();
3710
- const timer = setTimeout(() => controller.abort(), timeout);
3711
- const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
3712
- try {
3713
- const response = await fetch(url, {
3714
- signal,
3715
- headers: { "User-Agent": `mihomo-cli/${VERSION}` }
3716
- });
3717
- if (!response.ok) {
3718
- const error = new Error(`HTTP ${response.status}`);
3719
- error.response = { status: response.status };
3720
- try {
3721
- error.response.data = await response.json();
3722
- } catch {
3723
- }
3724
- throw error;
3725
- }
3726
- const data = config?.responseType === "json" ? await response.json() : await response.text();
3727
- return { data, headers: response.headers, status: response.status };
3728
- } finally {
3729
- clearTimeout(timer);
3730
- }
3731
- }
3732
- };
3733
- }
3734
- function normalizeMirrorUrl(val) {
3735
- if (!val) return null;
3736
- if (val === "direct" || val === "no" || val === "none") return null;
3737
- let url = val;
3738
- if (!url.startsWith("http")) {
3739
- url = `https://${url}`;
3740
- }
3741
- if (!url.endsWith("/")) {
3742
- url += "/";
3743
- }
3744
- return url;
3745
- }
3746
- function parseMirrorArg(args) {
3747
- if (!args || args.length < 2) {
3748
- return { mirror: null, isOverride: false, type: "download" };
3749
- }
3750
- if (args.includes("--no-mirror") || args.includes("--direct")) {
3751
- return { mirror: null, isOverride: true, type: "download" };
3752
- }
3753
- const mirrorAllIdx = args.indexOf("--mirror-all");
3754
- if (mirrorAllIdx >= 0) {
3755
- const nextArg = args[mirrorAllIdx + 1];
3756
- if (!nextArg || nextArg.startsWith("-")) {
3757
- return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "all" };
3758
- }
3759
- return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "all" };
3760
- }
3761
- const mirrorIdx = args.indexOf("--mirror");
3762
- if (mirrorIdx >= 0) {
3763
- const nextArg = args[mirrorIdx + 1];
3764
- if (!nextArg || nextArg.startsWith("-")) {
3765
- return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "download" };
3766
- }
3767
- return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "download" };
3768
- }
3769
- return { mirror: null, isOverride: false, type: "download" };
3770
- }
3771
- function isProxyValid(proxy) {
3772
- if (!proxy.name || !proxy.server || !proxy.port) return false;
3773
- if (!proxy.type) return false;
3774
- if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3775
- const pw = String(proxy.password || "");
3776
- if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
3777
- }
3778
- return true;
3779
- }
3780
-
3781
- // src/process.ts
3782
- var PROCESS_WAIT_ATTEMPTS = 50;
3783
- var PROCESS_WAIT_INTERVAL = 100;
3784
- var STARTUP_WAIT_MS = 800;
3785
- var SUDO_TIMEOUT_MS = 6e4;
3786
- var TUN_MODE_POST_WAIT_MS = 500;
3787
- var BATCH_KILL_THRESHOLD = 3;
3788
- var DEFAULT_LOG_RETENTION_DAYS = 7;
3789
- function escapeForPgrep(s) {
3790
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3791
- }
3792
- function clearRuntime() {
3793
- if (fs5.existsSync(DIRS.runtime)) {
3794
- rmrf(DIRS.runtime);
3795
- }
3796
- ensureDirs();
3885
+
3886
+ // src/daemon.ts
3887
+ import { spawnSync as spawnSync4 } from "child_process";
3888
+ import fs6 from "fs";
3889
+ import path5 from "path";
3890
+
3891
+ // src/process.ts
3892
+ import { spawn, spawnSync as spawnSync3 } from "child_process";
3893
+ import fs5 from "fs";
3894
+ import path4 from "path";
3895
+ var PROCESS_WAIT_ATTEMPTS = 50;
3896
+ var PROCESS_WAIT_INTERVAL = 100;
3897
+ var STARTUP_WAIT_MS = 800;
3898
+ var SUDO_TIMEOUT_MS = 6e4;
3899
+ var TUN_MODE_POST_WAIT_MS = 500;
3900
+ var BATCH_KILL_THRESHOLD = 3;
3901
+ var DEFAULT_LOG_RETENTION_DAYS = 7;
3902
+ var MAIN_INSTANCE_PATTERN = `${escapeRegExp(PATHS.mihomoBinary)}.*${escapeRegExp(PATHS.configFile)}`;
3903
+ function clearRuntime() {
3904
+ if (fs5.existsSync(DIRS.runtime)) {
3905
+ rmrf(DIRS.runtime);
3906
+ }
3907
+ ensureDirs();
3797
3908
  }
3798
3909
  function getPid() {
3799
3910
  if (!fs5.existsSync(PATHS.pidFile)) return null;
@@ -3808,10 +3919,9 @@ function isRunning() {
3808
3919
  const pid = getPid();
3809
3920
  return pid ? isProcessRunning(pid) : false;
3810
3921
  }
3811
- function getAllMihomoPids() {
3812
- const binaryPath = PATHS.mihomoBinary;
3922
+ function getMihomoPids() {
3813
3923
  try {
3814
- const result = spawnSync3("pgrep", ["-f", escapeForPgrep(binaryPath)], { encoding: "utf8", timeout: 1e4 });
3924
+ const result = spawnSync3("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
3815
3925
  const output = (result.stdout || "").trim();
3816
3926
  if (!output) return [];
3817
3927
  return output.split("\n").filter(Boolean).map((p) => parseInt(p, 10)).filter((p) => Number.isInteger(p) && p > 0);
@@ -3829,7 +3939,7 @@ function isPidFileOwnedByRoot() {
3829
3939
  }
3830
3940
  }
3831
3941
  function checkStaleState() {
3832
- const allPids = getAllMihomoPids();
3942
+ const allPids = getMihomoPids();
3833
3943
  const hasRootProcess = allPids.some((p) => isProcessRoot(p));
3834
3944
  const hasRootPidFile = isPidFileOwnedByRoot();
3835
3945
  return {
@@ -3880,7 +3990,7 @@ function killProcess(pid, needsSudo = false) {
3880
3990
  }
3881
3991
  }
3882
3992
  function killAllMihomo(forceSudo = false) {
3883
- const pattern = escapeForPgrep(PATHS.mihomoBinary);
3993
+ const pattern = MAIN_INSTANCE_PATTERN;
3884
3994
  if (forceSudo) {
3885
3995
  try {
3886
3996
  spawnSync3("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
@@ -3898,7 +4008,7 @@ function killAllMihomo(forceSudo = false) {
3898
4008
  }
3899
4009
  }
3900
4010
  function cleanupAll(forceSudo = false) {
3901
- const pids = getAllMihomoPids();
4011
+ const pids = getMihomoPids();
3902
4012
  if (pids.length === 0) {
3903
4013
  clearPid();
3904
4014
  return { killed: 0, failed: 0, remaining: [] };
@@ -3929,26 +4039,26 @@ function cleanupAll(forceSudo = false) {
3929
4039
  }
3930
4040
  }
3931
4041
  for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
3932
- if (getAllMihomoPids().length === 0) break;
4042
+ if (getMihomoPids().length === 0) break;
3933
4043
  sleepSync(PROCESS_WAIT_INTERVAL);
3934
4044
  }
3935
4045
  clearPid();
3936
- return { killed: killedCount, failed: failedPids.length, remaining: getAllMihomoPids() };
4046
+ return { killed: killedCount, failed: failedPids.length, remaining: getMihomoPids() };
3937
4047
  }
3938
4048
  function createTunLaunchScript() {
3939
- const binary = PATHS.mihomoBinary;
3940
- const configFile = PATHS.configFile;
3941
- const logFile = PATHS.logFile;
3942
- const pidFile = PATHS.pidFile;
3943
- const dataDir = DIRS.data;
3944
- const killPattern = escapeForPgrep(binary);
4049
+ const binary = shellQuote(PATHS.mihomoBinary);
4050
+ const configFile = shellQuote(PATHS.configFile);
4051
+ const logFile = shellQuote(PATHS.logFile);
4052
+ const pidFile = shellQuote(PATHS.pidFile);
4053
+ const dataDir = shellQuote(DIRS.data);
4054
+ const killPattern = shellQuote(MAIN_INSTANCE_PATTERN);
3945
4055
  const scriptContent = `#!/bin/bash
3946
- BINARY="${binary}"
3947
- CONFIG_FILE="${configFile}"
3948
- LOG_FILE="${logFile}"
3949
- PID_FILE="${pidFile}"
3950
- DATA_DIR="${dataDir}"
3951
- KILL_PATTERN='${killPattern}'
4056
+ BINARY=${binary}
4057
+ CONFIG_FILE=${configFile}
4058
+ LOG_FILE=${logFile}
4059
+ PID_FILE=${pidFile}
4060
+ DATA_DIR=${dataDir}
4061
+ KILL_PATTERN=${killPattern}
3952
4062
 
3953
4063
  # \u7EC8\u6B62\u65E7\u8FDB\u7A0B
3954
4064
  pkill -9 -f "\${KILL_PATTERN}" 2>/dev/null || true
@@ -3979,7 +4089,7 @@ echo "--- \u65E5\u5FD7 ---"
3979
4089
  tail -25 "\${LOG_FILE}" 2>/dev/null
3980
4090
  exit 1
3981
4091
  `;
3982
- const scriptPath = path3.join(DIRS.runtime, "launch-tun.sh");
4092
+ const scriptPath = path4.join(DIRS.runtime, "launch-tun.sh");
3983
4093
  fs5.writeFileSync(scriptPath, scriptContent, { mode: 448 });
3984
4094
  return scriptPath;
3985
4095
  }
@@ -4001,7 +4111,7 @@ function getProcessInfo(pid) {
4001
4111
  function getStatus() {
4002
4112
  const running = isRunning();
4003
4113
  const pid = getPid();
4004
- const allPids = getAllMihomoPids();
4114
+ const allPids = getMihomoPids();
4005
4115
  return {
4006
4116
  running,
4007
4117
  pid: running ? pid : null,
@@ -4051,6 +4161,16 @@ async function startMixedMode(staleState) {
4051
4161
  const configFile = PATHS.configFile;
4052
4162
  const logFile = PATHS.logFile;
4053
4163
  const args = ["-d", DIRS.data, "-f", configFile];
4164
+ if (fs5.existsSync(logFile)) {
4165
+ try {
4166
+ fs5.accessSync(logFile, fs5.constants.W_OK);
4167
+ } catch {
4168
+ try {
4169
+ fs5.unlinkSync(logFile);
4170
+ } catch {
4171
+ }
4172
+ }
4173
+ }
4054
4174
  const logFd = fs5.openSync(logFile, "a");
4055
4175
  const child = spawn(PATHS.mihomoBinary, args, {
4056
4176
  detached: true,
@@ -4113,14 +4233,14 @@ async function startTunMode(staleState) {
4113
4233
  return { success: true, pid: finalPid, mode: "tun" };
4114
4234
  }
4115
4235
  function stop(forceSudo = false) {
4116
- const allPids = getAllMihomoPids();
4236
+ const allPids = getMihomoPids();
4117
4237
  if (allPids.length === 0) {
4118
4238
  clearPid();
4119
4239
  clearRuntime();
4120
4240
  return { success: true, notRunning: true };
4121
4241
  }
4122
4242
  const result = cleanupAll(forceSudo);
4123
- const remaining = getAllMihomoPids();
4243
+ const remaining = getMihomoPids();
4124
4244
  if (remaining.length > 0) {
4125
4245
  console.log("");
4126
4246
  console.log("\u4ECD\u6709\u8FDB\u7A0B\u6B8B\u7559\uFF0C\u9700\u8981\u624B\u52A8\u6E05\u7406:");
@@ -4144,9 +4264,8 @@ function rotateLog() {
4144
4264
  if (!fs5.existsSync(logFile)) return null;
4145
4265
  const stat = fs5.statSync(logFile);
4146
4266
  if (stat.size === 0) return null;
4147
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/T/, "_").replace(/:/g, "-").replace(/\..+/, "");
4148
- const rotatedName = `mihomo.${timestamp}.log`;
4149
- const rotatedPath = path3.join(DIRS.logs, rotatedName);
4267
+ const rotatedName = `mihomo.${formatLocalTimestamp()}.log`;
4268
+ const rotatedPath = path4.join(DIRS.logs, rotatedName);
4150
4269
  fs5.renameSync(logFile, rotatedPath);
4151
4270
  return rotatedPath;
4152
4271
  }
@@ -4161,7 +4280,7 @@ function cleanupOldLogs(maxAgeDays = DEFAULT_LOG_RETENTION_DAYS) {
4161
4280
  for (const file of files) {
4162
4281
  if (!file.match(/^mihomo\.\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$/)) continue;
4163
4282
  try {
4164
- const filePath = path3.join(logsDir, file);
4283
+ const filePath = path4.join(logsDir, file);
4165
4284
  const stat = fs5.statSync(filePath);
4166
4285
  if (now - stat.mtimeMs > maxAgeMs) {
4167
4286
  fs5.unlinkSync(filePath);
@@ -4192,7 +4311,7 @@ function listLogs() {
4192
4311
  const match = file.match(/^mihomo\.(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.log$/);
4193
4312
  if (!match) continue;
4194
4313
  try {
4195
- const filePath = path3.join(logsDir, file);
4314
+ const filePath = path4.join(logsDir, file);
4196
4315
  const stat = fs5.statSync(filePath);
4197
4316
  result.archives.push({
4198
4317
  name: file,
@@ -4208,22 +4327,22 @@ function listLogs() {
4208
4327
  return result;
4209
4328
  }
4210
4329
  function isPathUnderDir(filePath, baseDir) {
4211
- const resolvedPath = path3.resolve(filePath);
4212
- const resolvedBase = path3.resolve(baseDir);
4213
- return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path3.sep);
4330
+ const resolvedPath = path4.resolve(filePath);
4331
+ const resolvedBase = path4.resolve(baseDir);
4332
+ return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path4.sep);
4214
4333
  }
4215
4334
  function getLogPathByName(name) {
4216
4335
  const logsDir = DIRS.logs;
4217
4336
  let targetName = name;
4218
4337
  if (!name.endsWith(".log")) targetName = `mihomo.${name}.log`;
4219
4338
  if (!targetName.startsWith("mihomo.")) targetName = `mihomo.${targetName}`;
4220
- const filePath = path3.join(logsDir, targetName);
4339
+ const filePath = path4.join(logsDir, targetName);
4221
4340
  if (fs5.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) return filePath;
4222
4341
  if (fs5.existsSync(logsDir)) {
4223
4342
  const files = fs5.readdirSync(logsDir);
4224
4343
  for (const file of files) {
4225
4344
  if (file.includes(name)) {
4226
- const candidatePath = path3.join(logsDir, file);
4345
+ const candidatePath = path4.join(logsDir, file);
4227
4346
  if (isPathUnderDir(candidatePath, logsDir)) return candidatePath;
4228
4347
  }
4229
4348
  }
@@ -4271,1021 +4390,1232 @@ function viewLogWithTail(logPath, options) {
4271
4390
  });
4272
4391
  }
4273
4392
 
4274
- // src/commands/directory.ts
4275
- function cmdDirectory(args) {
4276
- const action = args?.[1];
4277
- if (action === "open") {
4278
- const target = args[2];
4279
- if (!target || target === "root") {
4280
- console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
4281
- const success = openUrl(USER_DATA_DIR);
4282
- if (!success) {
4283
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
4393
+ // src/daemon.ts
4394
+ var SERVICE_TARGET = `system/${LAUNCH_DAEMON_LABEL}`;
4395
+ var HOT_RELOAD_TIMEOUT_MS = 5e3;
4396
+ var DAEMON_BOOT_WAIT_MS = 500;
4397
+ var LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
4398
+ function logOversized() {
4399
+ try {
4400
+ return fs6.statSync(PATHS.logFile).size > LOG_ROTATE_MAX_BYTES;
4401
+ } catch {
4402
+ return false;
4403
+ }
4404
+ }
4405
+ function escapeXml(s) {
4406
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4407
+ }
4408
+ function buildPlist() {
4409
+ const programArguments = [PATHS.mihomoBinary, "-d", DIRS.data, "-f", PATHS.configFile];
4410
+ const argsXml = programArguments.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
4411
+ return `<?xml version="1.0" encoding="UTF-8"?>
4412
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4413
+ <plist version="1.0">
4414
+ <dict>
4415
+ <key>Label</key>
4416
+ <string>${escapeXml(LAUNCH_DAEMON_LABEL)}</string>
4417
+ <key>ProgramArguments</key>
4418
+ <array>
4419
+ ${argsXml}
4420
+ </array>
4421
+ <key>RunAtLoad</key>
4422
+ <true/>
4423
+ <key>KeepAlive</key>
4424
+ <true/>
4425
+ <key>StandardOutPath</key>
4426
+ <string>${escapeXml(PATHS.logFile)}</string>
4427
+ <key>StandardErrorPath</key>
4428
+ <string>${escapeXml(PATHS.logFile)}</string>
4429
+ <key>WorkingDirectory</key>
4430
+ <string>/tmp</string>
4431
+ </dict>
4432
+ </plist>
4433
+ `;
4434
+ }
4435
+ function runSudoScript(scriptBody, opts) {
4436
+ if (!process.stdin.isTTY) {
4437
+ throw new Error("\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u8F93\u5165\u7BA1\u7406\u5458\u5BC6\u7801\uFF08\u9700\u8981\u5728\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\u8FD0\u884C sudo\uFF09");
4438
+ }
4439
+ ensureDirs();
4440
+ const scriptPath = path5.join(DIRS.runtime, opts.file);
4441
+ fs6.writeFileSync(scriptPath, scriptBody, { mode: 448 });
4442
+ try {
4443
+ const result = spawnSync4("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4444
+ if (result.error) throw result.error;
4445
+ if (result.status !== 0) {
4446
+ if (result.status === 1) {
4447
+ throw new Error("\u5DF2\u53D6\u6D88\u6216\u5BC6\u7801\u9519\u8BEF");
4284
4448
  }
4285
- return;
4286
- }
4287
- const key = target.toLowerCase();
4288
- const targetInfo = Object.hasOwn(DIRECTORY_TARGETS, key) ? DIRECTORY_TARGETS[key] : void 0;
4289
- if (targetInfo) {
4290
- const targetPath = targetInfo.path || USER_DATA_DIR;
4291
- console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
4292
- const success = openUrl(targetPath);
4293
- if (!success) {
4294
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${targetPath}`);
4449
+ if (result.status == null) {
4450
+ throw new Error(`${opts.action}\u88AB\u4E2D\u65AD\uFF08sudo \u8FDB\u7A0B\u88AB\u4FE1\u53F7\u7EC8\u6B62\uFF09`);
4295
4451
  }
4296
- return;
4452
+ const custom = opts.codeMessages?.[result.status];
4453
+ throw new Error(custom || `${opts.action}\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.status}\uFF09`);
4297
4454
  }
4298
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`);
4299
- console.log("");
4300
- console.log("\u53EF\u7528\u76EE\u6807:");
4301
- console.log(" root (\u9ED8\u8BA4) \u6839\u76EE\u5F55");
4302
- for (const [key2, val] of Object.entries(DIRECTORY_TARGETS)) {
4303
- if (key2 !== "root") {
4304
- console.log(` ${key2.padEnd(14)}${val.label}`);
4455
+ } finally {
4456
+ try {
4457
+ fs6.unlinkSync(scriptPath);
4458
+ } catch {
4459
+ }
4460
+ }
4461
+ }
4462
+ function isDaemonEnabled() {
4463
+ return fs6.existsSync(PATHS.launchDaemonPlist);
4464
+ }
4465
+ function getDaemonStatus() {
4466
+ if (!isDaemonEnabled()) {
4467
+ return { enabled: false, loaded: false, pid: null };
4468
+ }
4469
+ const rootPids = getMihomoPids().filter(isProcessRoot);
4470
+ return { enabled: true, loaded: rootPids.length > 0, pid: rootPids[0] ?? null };
4471
+ }
4472
+ function isDaemonRunning(status) {
4473
+ return status.loaded && status.pid !== null;
4474
+ }
4475
+ function enableDaemon() {
4476
+ if (!fs6.existsSync(PATHS.mihomoBinary)) {
4477
+ throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4478
+ }
4479
+ if (!fs6.existsSync(PATHS.configFile)) {
4480
+ throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4481
+ }
4482
+ ensureDirs();
4483
+ const stagePath = path5.join(DIRS.runtime, "daemon.plist.stage");
4484
+ atomicWriteFileSync(stagePath, buildPlist(), { mode: 384 });
4485
+ const target = shellQuote(SERVICE_TARGET);
4486
+ const plistDest = shellQuote(PATHS.launchDaemonPlist);
4487
+ const stage = shellQuote(stagePath);
4488
+ const pattern = shellQuote(MAIN_INSTANCE_PATTERN);
4489
+ const script = [
4490
+ "#!/bin/bash",
4491
+ `launchctl bootout ${target} 2>/dev/null || true`,
4492
+ `pkill -9 -f ${pattern} 2>/dev/null || true`,
4493
+ "sleep 0.2",
4494
+ `install -m 644 -o root -g wheel ${stage} ${plistDest} || exit 2`,
4495
+ `launchctl bootstrap system ${plistDest} || { launchctl bootout ${target} 2>/dev/null; rm -f ${plistDest}; exit 3; }`,
4496
+ "exit 0",
4497
+ ""
4498
+ ].join("\n");
4499
+ try {
4500
+ runSudoScript(script, {
4501
+ action: "\u542F\u7528\u4FDD\u6D3B",
4502
+ file: "daemon-enable.sh",
4503
+ codeMessages: {
4504
+ 2: "\u5B89\u88C5 plist \u5230 /Library/LaunchDaemons \u5931\u8D25",
4505
+ 3: "\u88C5\u8F7D\u4FDD\u6D3B\u670D\u52A1\u5931\u8D25\uFF08launchctl bootstrap\uFF09"
4305
4506
  }
4507
+ });
4508
+ } finally {
4509
+ try {
4510
+ fs6.unlinkSync(stagePath);
4511
+ } catch {
4306
4512
  }
4513
+ }
4514
+ }
4515
+ function disableDaemon() {
4516
+ if (!isDaemonEnabled()) return;
4517
+ const target = shellQuote(SERVICE_TARGET);
4518
+ const plistDest = shellQuote(PATHS.launchDaemonPlist);
4519
+ const logFile = shellQuote(PATHS.logFile);
4520
+ const dataDir = shellQuote(DIRS.data);
4521
+ const script = [
4522
+ "#!/bin/bash",
4523
+ `launchctl bootout ${target} 2>/dev/null || true`,
4524
+ `rm -f ${plistDest}`,
4525
+ `chown "$SUDO_UID:$SUDO_GID" ${logFile} 2>/dev/null || true`,
4526
+ `chown -R "$SUDO_UID:$SUDO_GID" ${dataDir} 2>/dev/null || true`,
4527
+ "exit 0",
4528
+ ""
4529
+ ].join("\n");
4530
+ runSudoScript(script, { action: "\u5173\u95ED\u4FDD\u6D3B", file: "daemon-disable.sh" });
4531
+ const rootPids = getMihomoPids().filter(isProcessRoot);
4532
+ if (rootPids.length > 0) {
4307
4533
  console.log("");
4308
- process.exit(1);
4534
+ console.log(`\u4ECD\u6709 root \u5185\u6838\u8FDB\u7A0B\u6B8B\u7559 (PID ${rootPids.join(", ")})`);
4535
+ console.log("\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo");
4309
4536
  }
4310
- console.log("");
4311
- console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
4312
- console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
4313
- console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
4314
- console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
4315
- console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
4316
- console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
4317
- console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
4318
- console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
4319
- console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
4320
- console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
4321
- console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
4322
- console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
4323
- console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
4324
- console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
4325
- console.log("");
4326
- console.log("\u6253\u5F00\u76EE\u5F55:");
4327
- console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
4328
- console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
4329
- console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
4330
- console.log(" mihomo dir open data \u6253\u5F00 mihomo \u6570\u636E\u76EE\u5F55");
4331
- console.log(" mihomo dir open runtime \u6253\u5F00\u8FD0\u884C\u65F6\u76EE\u5F55");
4332
- console.log(" mihomo dir open kernel \u6253\u5F00\u5185\u6838\u76EE\u5F55");
4333
- console.log("");
4334
- console.log("\u73AF\u5883\u53D8\u91CF:");
4335
- console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
4336
- console.log("");
4337
4537
  }
4338
-
4339
- // src/commands/help.ts
4340
- function printShortHelp() {
4341
- console.log(`
4342
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
4343
- `);
4344
- console.log(
4345
- `\u5E38\u7528\u547D\u4EE4:
4346
- ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
4347
- ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
4348
- ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
4349
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
4350
- `
4351
- );
4538
+ async function tryHotReload() {
4539
+ const controller = new AbortController();
4540
+ const timer = setTimeout(() => controller.abort(), HOT_RELOAD_TIMEOUT_MS);
4541
+ try {
4542
+ const res = await fetch(`${CONTROLLER_BASE_URL}/configs?force=true`, {
4543
+ method: "PUT",
4544
+ headers: { "Content-Type": "application/json" },
4545
+ body: "{}",
4546
+ signal: controller.signal
4547
+ });
4548
+ return res.status === 204 || res.ok;
4549
+ } catch {
4550
+ return false;
4551
+ } finally {
4552
+ clearTimeout(timer);
4553
+ }
4554
+ }
4555
+ async function restartDaemon() {
4556
+ if (!fs6.existsSync(PATHS.launchDaemonPlist)) {
4557
+ throw new Error("\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u6CD5\u91CD\u542F");
4558
+ }
4559
+ if (!logOversized() && await tryHotReload()) return;
4560
+ const target = shellQuote(SERVICE_TARGET);
4561
+ const plistDest = shellQuote(PATHS.launchDaemonPlist);
4562
+ const logFile = shellQuote(PATHS.logFile);
4563
+ const archiveFile = shellQuote(path5.join(DIRS.logs, `mihomo.${formatLocalTimestamp()}.log`));
4564
+ const script = [
4565
+ "#!/bin/bash",
4566
+ `if [ -f ${logFile} ] && [ "$(stat -f%z ${logFile} 2>/dev/null || echo 0)" -gt ${LOG_ROTATE_MAX_BYTES} ]; then`,
4567
+ ` cp ${logFile} ${archiveFile} 2>/dev/null && : > ${logFile}`,
4568
+ "fi",
4569
+ `if launchctl kickstart -k ${target} 2>/dev/null; then exit 0; fi`,
4570
+ `launchctl bootstrap system ${plistDest} || exit 3`,
4571
+ "exit 0",
4572
+ ""
4573
+ ].join("\n");
4574
+ runSudoScript(script, {
4575
+ action: "\u91CD\u542F\u4FDD\u6D3B",
4576
+ file: "daemon-restart.sh",
4577
+ codeMessages: { 3: "\u91CD\u542F\u4FDD\u6D3B\u5931\u8D25\uFF08launchctl bootstrap\uFF09" }
4578
+ });
4579
+ cleanupOldLogs();
4352
4580
  }
4353
- function printHelp() {
4354
- console.log(
4355
- `
4356
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}
4357
-
4358
- \u547D\u4EE4\u522B\u540D: mihomo, mhm, mh
4359
-
4360
- \u7528\u6CD5:
4361
- mihomo <\u547D\u4EE4> [\u9009\u9879]
4362
-
4363
- ${colors.cyan("\u63A7\u5236:")}
4364
- ${colors.bold("start")} [tun|mixed] [-s] [-u ms] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)
4365
- [-r N] [-t ms] [-j N]
4366
- ${colors.bold("stop")} \u505C\u6B62\u4EE3\u7406
4367
- ${colors.bold("status")} \u67E5\u770B\u72B6\u6001
4368
-
4369
- ${colors.cyan("\u754C\u9762:")}
4370
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)
4371
- ${colors.bold("log")} [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09
4372
- ${colors.bold("logs")} [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09
4373
-
4374
- ${colors.cyan("\u8BA2\u9605:")}
4375
- ${colors.bold("subscription")} \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09
4376
- ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
4377
- ${colors.bold("subscription")} add <url> [name] \u6DFB\u52A0\u8BA2\u9605
4378
- ${colors.bold("subscription")} update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09
4379
- ${colors.bold("subscription")} remove <name> \u5220\u9664\u8BA2\u9605
4380
- ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
4381
- ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
4382
- ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
4383
- ${colors.bold("test")} [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027
4384
- ${colors.bold("clean")} [-t ms] [-j N] [-r N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F
4385
4581
 
4386
- ${colors.cyan("\u914D\u7F6E:")}
4387
- ${colors.bold("overwrite")} \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09
4388
- ${colors.bold("overwrite")} on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E
4389
- ${colors.bold("directory")} \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09
4390
- ${colors.bold("directory")} open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|...
4391
-
4392
- ${colors.cyan("\u7CFB\u7EDF:")}
4393
- ${colors.bold("kernel")} [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09
4394
- ${colors.bold("update")} \u66F4\u65B0 mihomo-cli (npm install -g)
4395
- ${colors.bold("reset")} [\u76EE\u6807...] [--full] \u91CD\u7F6E: \u7559\u7A7A\u4FDD\u7559\u8BBE\u7F6E/\u5185\u6838/\u8986\u5199, \u6307\u5B9A\u76EE\u6807\u5220\u5BF9\u5E94\u9879, --full \u5220\u5168\u90E8
4396
- ${colors.bold("help")}, -h \u663E\u793A\u5E2E\u52A9
4397
- ${colors.bold("version")}, -v \u663E\u793A\u7248\u672C
4398
-
4399
- ${colors.cyan("\u793A\u4F8B:")}
4400
- mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F
4401
- mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F
4402
- mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605
4403
- mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)
4404
- mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)
4405
- mihomo ui # \u6253\u5F00 Web UI
4406
-
4407
- ${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}
4408
- mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)
4409
- tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo
4410
-
4411
- ${colors.cyan("\u6570\u636E\u76EE\u5F55:")}
4412
- \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E
4413
- \u9ED8\u8BA4: ${USER_DATA_DIR}
4414
- `
4415
- );
4582
+ // src/subscription.ts
4583
+ function isGithubUrl(url) {
4584
+ return /github\.com|raw\.githubusercontent\.com/i.test(url);
4416
4585
  }
4417
- function printVersion() {
4418
- const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
4419
- console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
4420
- console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
4421
- console.log(`${colors.gray("\u6570\u636E\u76EE\u5F55: ")}${USER_DATA_DIR}`);
4586
+ function getDefaultUpdateInterval(url) {
4587
+ return isGithubUrl(url) ? DEFAULT_UPDATE_INTERVAL_HOURS_GITHUB : DEFAULT_UPDATE_INTERVAL_HOURS;
4422
4588
  }
4423
-
4424
- // src/kernel.ts
4425
- import { spawnSync as spawnSync4 } from "child_process";
4426
- import fs6 from "fs";
4427
- import path4 from "path";
4428
-
4429
- // node_modules/compare-versions/lib/esm/utils.js
4430
- var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
4431
- var validateAndParse = (version) => {
4432
- if (typeof version !== "string") {
4433
- throw new TypeError("Invalid argument expected string");
4434
- }
4435
- const match = version.match(semver);
4436
- if (!match) {
4437
- throw new Error(`Invalid argument not valid semver ('${version}' received)`);
4438
- }
4439
- match.shift();
4440
- return match;
4441
- };
4442
- var isWildcard = (s) => s === "*" || s === "x" || s === "X";
4443
- var tryParse = (v) => {
4444
- const n = parseInt(v, 10);
4445
- return isNaN(n) ? v : n;
4446
- };
4447
- var forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b];
4448
- var compareStrings = (a, b) => {
4449
- if (isWildcard(a) || isWildcard(b))
4450
- return 0;
4451
- const [ap, bp] = forceType(tryParse(a), tryParse(b));
4452
- if (ap > bp)
4453
- return 1;
4454
- if (ap < bp)
4455
- return -1;
4456
- return 0;
4457
- };
4458
- var compareSegments = (a, b) => {
4459
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
4460
- const r = compareStrings(a[i] || "0", b[i] || "0");
4461
- if (r !== 0)
4462
- return r;
4463
- }
4464
- return 0;
4465
- };
4466
-
4467
- // node_modules/compare-versions/lib/esm/compareVersions.js
4468
- var compareVersions = (v1, v2) => {
4469
- const n1 = validateAndParse(v1);
4470
- const n2 = validateAndParse(v2);
4471
- const p1 = n1.pop();
4472
- const p2 = n2.pop();
4473
- const r = compareSegments(n1, n2);
4474
- if (r !== 0)
4475
- return r;
4476
- if (p1 && p2) {
4477
- return compareSegments(p1.split("."), p2.split("."));
4478
- } else if (p1 || p2) {
4479
- return p1 ? -1 : 1;
4480
- }
4481
- return 0;
4482
- };
4483
-
4484
- // src/kernel.ts
4485
- var GITHUB_REPO = "MetaCubeX/mihomo";
4486
- var KERNEL_HTTP_TIMEOUT = 12e4;
4487
- var KERNEL_DOWNLOAD_TIMEOUT = 18e4;
4488
- var HTTP_CLIENT = createHttpClient({ timeout: KERNEL_HTTP_TIMEOUT });
4489
- function withMirror(url, mirror) {
4490
- if (mirror && (url.startsWith("https://github.com/") || url.startsWith("https://api.github.com/"))) {
4491
- return mirror + url;
4492
- }
4493
- return url;
4589
+ function resolveUpdateInterval(url, cachedInterval) {
4590
+ return cachedInterval && cachedInterval > 0 ? cachedInterval : getDefaultUpdateInterval(url);
4494
4591
  }
4495
- function getArch() {
4496
- const arch = process.arch;
4497
- if (arch === "arm64") return "arm64";
4498
- if (arch === "x64") return "amd64";
4499
- return arch;
4592
+ var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4593
+ function isMultiUrl(url) {
4594
+ return url.includes(",");
4500
4595
  }
4501
- function findMatchingAsset(assets, platform, arch) {
4502
- const prefix = `mihomo-${platform}-${arch}`;
4503
- const matchingAssets = assets.filter(
4504
- (a) => a.name.startsWith(prefix) && a.name.endsWith(".gz") || a.name.startsWith(`${prefix}-`) && a.name.endsWith(".gz")
4505
- );
4506
- if (matchingAssets.length === 0) return null;
4507
- if (matchingAssets.length === 1) return matchingAssets[0];
4508
- const standardAsset = matchingAssets.find((a) => {
4509
- const nameWithoutGz = a.name.slice(0, -3);
4510
- const parts = nameWithoutGz.split("-");
4511
- const lastPart = parts[parts.length - 1];
4512
- return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go");
4513
- });
4514
- return standardAsset || matchingAssets[0];
4596
+ function splitUrls(url) {
4597
+ return url.split(",").map((u) => u.trim()).filter(Boolean);
4515
4598
  }
4516
- async function getLatestRelease(repo, mirror) {
4517
- const url = withMirror(`https://api.github.com/repos/${repo}/releases`, mirror);
4518
- const response = await HTTP_CLIENT.get(url, { responseType: "json" });
4519
- const releases = response.data;
4520
- if (!Array.isArray(releases) || releases.length === 0) {
4521
- throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7248\u672C\u4FE1\u606F");
4599
+ function loadSubscriptionConfig(subName) {
4600
+ const rawContent = readSubscriptionRawConfig(subName);
4601
+ if (!rawContent) {
4602
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4522
4603
  }
4523
- const stableReleases = releases.filter(
4524
- (r) => !r.prerelease && !r.tag_name.toLowerCase().includes("alpha") && !r.tag_name.toLowerCase().includes("beta") && !r.tag_name.toLowerCase().includes("prerelease")
4525
- );
4526
- return stableReleases.length > 0 ? stableReleases[0] : releases[0];
4604
+ const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4605
+ return {
4606
+ raw,
4607
+ proxies: raw.proxies || [],
4608
+ proxyGroups: raw["proxy-groups"] || []
4609
+ };
4527
4610
  }
4528
- async function checkUpdate(mirror) {
4529
- const currentVersion = getKernelVersion();
4530
- const latest = await getLatestRelease(GITHUB_REPO, mirror);
4531
- const latestVersion = latest.tag_name;
4532
- let needsUpdate = false;
4533
- const currentDisplay = currentVersion || "\u672A\u5B89\u88C5";
4534
- if (!currentVersion) {
4535
- needsUpdate = true;
4536
- } else {
4537
- try {
4538
- needsUpdate = compareVersions(latestVersion.replace(/^v/, ""), currentVersion.replace(/^v/, "")) > 0;
4539
- } catch {
4540
- needsUpdate = latestVersion !== currentVersion;
4611
+ function saveSubscriptionConfig(subName, parsed) {
4612
+ normalizeProxyNamesBeforeSave(parsed);
4613
+ parsed.raw.proxies = parsed.proxies;
4614
+ parsed.raw["proxy-groups"] = parsed.proxyGroups;
4615
+ saveSubscriptionRawConfig(subName, dumpYaml(parsed.raw));
4616
+ }
4617
+ function parseUserInfo(header) {
4618
+ if (!header) return null;
4619
+ const info = {};
4620
+ const parts = header.split(";").map((p) => p.trim());
4621
+ for (const part of parts) {
4622
+ const [key, val] = part.split("=").map((s) => s.trim());
4623
+ if (key && val !== void 0) {
4624
+ const numVal = parseFloat(val);
4625
+ info[key] = Number.isNaN(numVal) ? 0 : numVal;
4541
4626
  }
4542
4627
  }
4628
+ return info;
4629
+ }
4630
+ function parsePositiveInterval(header) {
4631
+ if (!header) return null;
4632
+ const n = parseInt(header, 10);
4633
+ return Number.isFinite(n) && n > 0 ? n : null;
4634
+ }
4635
+ function parseUsernameFromContentDisposition(header) {
4636
+ if (!header) return null;
4637
+ const match = header.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
4638
+ if (!match) return null;
4639
+ const filename = match[1];
4640
+ const parts = filename.split("/");
4641
+ return parts[parts.length - 1] || null;
4642
+ }
4643
+ function extractSubscriptionMeta(headers) {
4543
4644
  return {
4544
- current: currentDisplay,
4545
- latest: latestVersion,
4546
- needsUpdate,
4547
- assets: latest.assets,
4548
- release: latest
4645
+ userInfo: parseUserInfo(headers?.get("subscription-userinfo") ?? null),
4646
+ updateInterval: parsePositiveInterval(headers?.get("profile-update-interval")),
4647
+ webPageUrl: headers?.get("profile-web-page-url") || null,
4648
+ username: parseUsernameFromContentDisposition(headers?.get("content-disposition") ?? null)
4549
4649
  };
4550
4650
  }
4551
- function findBinaryInDir(dir, maxDepth = 4) {
4552
- if (maxDepth <= 0) return null;
4553
- const files = fs6.readdirSync(dir);
4554
- for (const f of files) {
4555
- const fullPath = path4.join(dir, f);
4556
- const stat = fs6.statSync(fullPath);
4557
- if (stat.isDirectory()) {
4558
- const found = findBinaryInDir(fullPath, maxDepth - 1);
4559
- if (found) return found;
4560
- continue;
4561
- }
4562
- if (f === "mihomo") return fullPath;
4563
- if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
4651
+ function saveSubscriptionMeta(subName, meta) {
4652
+ const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4653
+ if (meta.userInfo) {
4654
+ cacheData.upload = meta.userInfo.upload;
4655
+ cacheData.download = meta.userInfo.download;
4656
+ cacheData.total = meta.userInfo.total;
4657
+ cacheData.expire = meta.userInfo.expire;
4564
4658
  }
4565
- return null;
4659
+ if (meta.updateInterval) cacheData.update_interval = meta.updateInterval;
4660
+ if (meta.webPageUrl) cacheData.web_page_url = meta.webPageUrl;
4661
+ if (meta.username) cacheData.username = meta.username;
4662
+ saveSubscriptionCache(subName, cacheData);
4566
4663
  }
4567
- async function downloadKernel(progressCallback, mirror, releaseInfo) {
4568
- ensureDirs();
4569
- const latest = releaseInfo || await getLatestRelease(GITHUB_REPO, mirror);
4570
- const arch = getArch();
4571
- const platform = process.platform;
4572
- const asset = findMatchingAsset(latest.assets, platform, arch);
4573
- if (!asset) {
4574
- const available = latest.assets.map((a) => a.name).join(", ");
4575
- let hint = "";
4576
- if (available) hint = `
4577
- \u53EF\u7528\u7248\u672C: ${available}`;
4578
- throw new Error(`\u672A\u627E\u5230\u5339\u914D\u7684\u5185\u6838\u6587\u4EF6
4579
- \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
4664
+ function formatProxySummary(info) {
4665
+ const parts = [];
4666
+ if (info.proxyGroups && info.proxyGroups > 0) parts.push(`${info.proxyGroups} \u7EC4`);
4667
+ parts.push(`${info.proxies || 0} \u8282\u70B9`);
4668
+ return parts.join(", ");
4669
+ }
4670
+ function getActiveSubscription() {
4671
+ const subs = getSubscriptions();
4672
+ if (subs.length === 0) return null;
4673
+ const settings = readSettings();
4674
+ const activeName = settings.active_subscription;
4675
+ if (activeName) {
4676
+ const found = subs.find((s) => s.name === activeName);
4677
+ if (found) return found;
4580
4678
  }
4581
- const downloadUrl = withMirror(asset.browser_download_url, mirror);
4582
- const tempPath = path4.join(DIRS.kernel, asset.name);
4583
- const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
4584
- if (mirror && progressCallback) {
4585
- progressCallback("\u63D0\u793A: \u7ECF\u7B2C\u4E09\u65B9\u955C\u50CF\u4E2D\u8F6C\u4E0B\u8F7D\uFF0C\u65E0\u6CD5\u9A8C\u8BC1\u6765\u6E90\u5B8C\u6574\u6027\uFF0C\u5EFA\u8BAE\u76F4\u8FDE\u6216\u81EA\u884C\u6821\u9A8C\u4EA7\u7269");
4586
- }
4587
- if (progressCallback) {
4588
- progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
4589
- }
4590
- const curlResult = spawnSync4(
4591
- "curl",
4592
- ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
4593
- { stdio: "inherit" }
4594
- );
4595
- if (curlResult.status !== 0) {
4596
- try {
4597
- fs6.unlinkSync(tempPath);
4598
- } catch {
4679
+ return subs[0];
4680
+ }
4681
+ function findSubscriptionFuzzy(subs, pattern) {
4682
+ const lowerPattern = pattern.toLowerCase();
4683
+ const exact = [];
4684
+ const prefix = [];
4685
+ const includes = [];
4686
+ for (const s of subs) {
4687
+ const name = s.name.toLowerCase();
4688
+ if (name === lowerPattern) {
4689
+ exact.push(s);
4690
+ } else if (name.startsWith(lowerPattern)) {
4691
+ prefix.push(s);
4692
+ } else if (name.includes(lowerPattern)) {
4693
+ includes.push(s);
4599
4694
  }
4600
- throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
4601
- }
4602
- if (!fs6.existsSync(tempPath)) {
4603
- throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
4604
4695
  }
4605
- if (progressCallback) {
4606
- progressCallback("\u89E3\u538B\u5185\u6838...");
4696
+ if (exact.length > 0) return exact;
4697
+ if (prefix.length > 0) return prefix;
4698
+ return includes;
4699
+ }
4700
+ function pickSingleSubscription(subs, pattern) {
4701
+ if (subs.length === 0) {
4702
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${pattern}" \u7684\u8BA2\u9605`);
4703
+ process.exit(1);
4607
4704
  }
4608
- const extractPath = DIRS.kernel;
4609
- let extractedBinary = null;
4705
+ if (subs.length === 1) return subs[0];
4706
+ console.error("\u9519\u8BEF: \u5339\u914D\u5230\u591A\u4E2A\u8BA2\u9605\uFF0C\u8BF7\u66F4\u7CBE\u786E\u6307\u5B9A");
4707
+ console.log("\n\u5339\u914D\u7684\u8BA2\u9605:");
4708
+ for (const s of subs) console.log(` ${s.name}`);
4709
+ process.exit(1);
4710
+ }
4711
+ async function downloadSubscription(url, subName = "default", signal) {
4712
+ let response;
4610
4713
  try {
4611
- if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
4612
- const tarResult = spawnSync4("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
4613
- if (tarResult.error) throw tarResult.error;
4614
- if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
4615
- } else if (tempPath.endsWith(".gz")) {
4616
- const baseName = path4.basename(tempPath, ".gz");
4617
- const outputPath = path4.join(extractPath, baseName);
4618
- const gzipResult = spawnSync4("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
4619
- if (gzipResult.error) throw gzipResult.error;
4620
- if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
4621
- fs6.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
4622
- extractedBinary = outputPath;
4623
- }
4714
+ response = await HTTP_CLIENT.get(url, { responseType: "text", signal });
4624
4715
  } catch (e) {
4625
- try {
4626
- fs6.unlinkSync(tempPath);
4627
- } catch {
4716
+ const maskedUrl = maskUrl(url);
4717
+ let errorMsg = `\u83B7\u53D6\u8BA2\u9605\u5931\u8D25: ${e.message}`;
4718
+ const err = e;
4719
+ if (err.response) {
4720
+ errorMsg += ` (HTTP ${err.response.status})`;
4628
4721
  }
4629
- throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
4722
+ errorMsg += `
4723
+ URL: ${maskedUrl}`;
4724
+ throw new Error(errorMsg);
4630
4725
  }
4631
- const foundBinary = extractedBinary || findBinaryInDir(extractPath);
4632
- if (!foundBinary) {
4633
- try {
4634
- fs6.unlinkSync(tempPath);
4635
- } catch {
4636
- }
4637
- throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
4726
+ const content = response.data;
4727
+ if (!content?.trim()) {
4728
+ throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4638
4729
  }
4639
- const targetPath = PATHS.mihomoBinary;
4640
- if (foundBinary !== targetPath) {
4641
- if (fs6.existsSync(targetPath)) {
4642
- fs6.chmodSync(targetPath, 493);
4730
+ const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
4731
+ if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4732
+ saveSubscriptionRawConfig(subName, content);
4733
+ const meta = extractSubscriptionMeta(response.headers);
4734
+ saveSubscriptionMeta(subName, meta);
4735
+ const proxies = parsed.proxies;
4736
+ const proxyGroups = parsed["proxy-groups"];
4737
+ return {
4738
+ proxies: proxies ? proxies.length : 0,
4739
+ proxyGroups: proxyGroups ? proxyGroups.length : 0,
4740
+ userInfo: meta.userInfo,
4741
+ updateInterval: meta.updateInterval,
4742
+ webPageUrl: meta.webPageUrl,
4743
+ username: meta.username
4744
+ };
4745
+ }
4746
+ async function downloadMergedSubscription(urls, subName, signal) {
4747
+ const responses = await Promise.all(
4748
+ urls.map(async (url, index) => {
4643
4749
  try {
4644
- fs6.unlinkSync(targetPath);
4645
- } catch {
4750
+ const response = await HTTP_CLIENT.get(url, { responseType: "text", signal });
4751
+ return { url, index, response, error: null };
4752
+ } catch (e) {
4753
+ return { url, index, response: null, error: e };
4646
4754
  }
4755
+ })
4756
+ );
4757
+ for (const r of responses) {
4758
+ if (r.error) {
4759
+ const maskedUrl = maskUrl(r.url);
4760
+ throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${r.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${r.error.message}
4761
+ URL: ${maskedUrl}`);
4647
4762
  }
4648
- fs6.renameSync(foundBinary, targetPath);
4649
- }
4650
- fs6.chmodSync(targetPath, 493);
4651
- if (progressCallback) {
4652
- progressCallback("\u6821\u9A8C\u5185\u6838...");
4653
4763
  }
4654
- const check = spawnSync4(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
4655
- const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
4656
- if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
4657
- try {
4658
- fs6.unlinkSync(targetPath);
4659
- } catch {
4660
- }
4661
- try {
4662
- fs6.unlinkSync(tempPath);
4663
- } catch {
4764
+ const parsed = responses.map((r, i) => {
4765
+ const content = r.response?.data;
4766
+ if (!content?.trim()) throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A URL \u5185\u5BB9\u4E3A\u7A7A`);
4767
+ return parseYamlOrJson(content, `\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A`);
4768
+ });
4769
+ const base = parsed[0];
4770
+ const baseProxies = base.proxies || [];
4771
+ const seenNames = new Set(baseProxies.map((p) => p.name));
4772
+ for (let i = 1; i < parsed.length; i++) {
4773
+ const extraProxies = parsed[i].proxies || [];
4774
+ for (const proxy of extraProxies) {
4775
+ if (!seenNames.has(proxy.name)) {
4776
+ baseProxies.push(proxy);
4777
+ seenNames.add(proxy.name);
4778
+ }
4664
4779
  }
4665
- throw new Error(`\u5185\u6838\u81EA\u68C0\u5931\u8D25\uFF08\u53EF\u80FD\u4E0B\u8F7D\u635F\u574F\u6216\u67B6\u6784\u4E0D\u5339\u914D\uFF09\uFF0C\u5DF2\u5220\u9664
4666
- \u9000\u51FA\u7801: ${check.status}
4667
- \u8F93\u51FA: ${checkOutput || "(\u7A7A)"}`);
4668
- }
4669
- try {
4670
- fs6.unlinkSync(tempPath);
4671
- } catch {
4672
4780
  }
4673
- clearKernelVersionCache();
4674
- return { version: latest.tag_name, path: targetPath };
4781
+ base.proxies = baseProxies;
4782
+ const mergedContent = dumpYaml(base);
4783
+ saveSubscriptionRawConfig(subName, mergedContent);
4784
+ const meta = extractSubscriptionMeta(responses[0].response?.headers);
4785
+ saveSubscriptionMeta(subName, meta);
4786
+ const proxyGroups = base["proxy-groups"];
4787
+ return {
4788
+ proxies: baseProxies.length,
4789
+ proxyGroups: proxyGroups ? proxyGroups.length : 0,
4790
+ userInfo: meta.userInfo,
4791
+ updateInterval: meta.updateInterval,
4792
+ webPageUrl: meta.webPageUrl,
4793
+ username: meta.username
4794
+ };
4675
4795
  }
4676
-
4677
- // src/commands/kernel.ts
4678
- async function cmdKernel(args) {
4679
- const mirrorInfo = parseMirrorArg(args);
4680
- const effectiveMirror = mirrorInfo.mirror;
4681
- if (effectiveMirror) {
4682
- const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
4683
- console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
4796
+ function prepareConfigForStart(mode, subName = "default") {
4797
+ const rawContent = readSubscriptionRawConfig(subName);
4798
+ if (!rawContent) {
4799
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
4684
4800
  }
4685
- console.log("\n\u63D0\u793A: \u5982\u679C\u4E0B\u8F7D\u901F\u5EA6\u8FC7\u6162\u6216\u76F4\u8FDE\u5931\u8D25\uFF0C\u53EF\u4F7F\u7528 --mirror \u53C2\u6570\u901A\u8FC7\u955C\u50CF\u4E0B\u8F7D");
4686
- console.log("\n\u7528\u6CD5:");
4687
- console.log(" mihomo kernel # \u76F4\u8FDE");
4688
- console.log(" mihomo kernel --mirror # \u4E0B\u8F7D\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF (v6.gh-proxy.org)");
4689
- console.log(" mihomo kernel --mirror hk.gh-proxy.org # \u4E0B\u8F7D\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
4690
- console.log(" mihomo kernel --mirror-all # API\u8BF7\u6C42\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF");
4691
- console.log(" mihomo kernel --mirror-all hk.gh-proxy.org # API\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
4692
- console.log("\n\u53EF\u7528\u955C\u50CF:");
4693
- for (const m of AVAILABLE_MIRRORS) {
4694
- const isCurrent = effectiveMirror && (effectiveMirror.includes(`//${m}/`) || effectiveMirror.includes(`//${m}:`) || effectiveMirror.endsWith(`//${m}`));
4695
- console.log(` ${m}${isCurrent ? " (\u5F53\u524D)" : ""}`);
4801
+ const buildResult = buildConfig(rawContent, mode);
4802
+ if (buildResult.warnings.length > 0) {
4803
+ for (const warning of buildResult.warnings) {
4804
+ console.log(`${colors.yellow("\u81EA\u52A8\u4FEE\u590D:")} ${warning}`);
4805
+ }
4806
+ console.log("");
4696
4807
  }
4697
- console.log("");
4698
- console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
4808
+ writeMihomoConfig(buildResult.config);
4809
+ writeDebugConfig(buildResult);
4810
+ const proxies = buildResult.config.proxies;
4811
+ const proxyGroups = buildResult.config["proxy-groups"];
4812
+ return {
4813
+ proxies: proxies ? proxies.length : 0,
4814
+ proxyGroups: proxyGroups ? proxyGroups.length : 0
4815
+ };
4816
+ }
4817
+ function needsAutoUpdate(sub) {
4818
+ if (!sub.updated_at) return true;
4819
+ const lastUpdate = new Date(sub.updated_at).getTime();
4820
+ if (Number.isNaN(lastUpdate)) return true;
4821
+ const intervalHours = resolveUpdateInterval(sub.url, sub.update_interval);
4822
+ const intervalMs = intervalHours * 60 * 60 * 1e3;
4823
+ return Date.now() - lastUpdate > intervalMs;
4824
+ }
4825
+ async function tryUpdateOne(sub, signal) {
4699
4826
  try {
4700
- const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
4701
- const info = await checkUpdate(apiMirror);
4702
- console.log(`\u5F53\u524D: ${info.current}`);
4703
- console.log(`\u6700\u65B0: ${info.latest}`);
4704
- if (!info.needsUpdate) {
4705
- console.log("\u5DF2\u662F\u6700\u65B0\u7248\u672C");
4827
+ let info;
4828
+ if (isMultiUrl(sub.url)) {
4829
+ info = await downloadMergedSubscription(splitUrls(sub.url), sub.name, signal);
4706
4830
  } else {
4707
- console.log("\n\u6B63\u5728\u4E0B\u8F7D...");
4708
- const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
4709
- console.log(`
4710
- \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
4831
+ info = await downloadSubscription(sub.url, sub.name, signal);
4711
4832
  }
4833
+ return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
4712
4834
  } catch (e) {
4713
- console.error(`
4714
- \u66F4\u65B0\u5931\u8D25: ${e.message}`);
4715
- const err = e;
4716
- if (err.response?.data) {
4717
- if (err.response.data.message) {
4718
- console.error(`\u539F\u56E0: ${err.response.data.message}`);
4719
- }
4720
- if (err.response.data.documentation_url) {
4721
- console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
4722
- }
4723
- }
4724
- process.exit(1);
4835
+ return { name: sub.name, success: false, error: e.message };
4725
4836
  }
4726
4837
  }
4727
-
4728
- // src/commands/log.ts
4729
- function cmdLog(args) {
4730
- const logPath = getLogPath();
4731
- if (hasFlag(args, "-o", "--open")) {
4732
- openLogFile(logPath);
4733
- return;
4838
+ function printUpdateResult(r) {
4839
+ if (r.success) {
4840
+ console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
4841
+ } else {
4842
+ console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
4734
4843
  }
4735
- viewLogWithTail(logPath, { follow: true, lines: 50 });
4736
4844
  }
4737
- function cmdLogs(args) {
4738
- const targetName = getNonFlagArg(args, 1);
4739
- const lines = parseIntArg(args, "-n", "--lines", 100);
4740
- const openInViewer = hasFlag(args, "-o", "--open");
4741
- if (targetName) {
4742
- let logPath;
4743
- if (targetName === "current" || targetName === "0") {
4744
- logPath = getLogPath();
4745
- } else {
4746
- const parsedIdx = parseInt(targetName, 10);
4747
- if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
4748
- const archiveLogs = listLogs();
4749
- const archive = archiveLogs.archives[parsedIdx - 1];
4750
- if (!archive) {
4751
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
4752
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
4753
- process.exit(1);
4754
- }
4755
- logPath = archive.path;
4756
- } else {
4757
- logPath = getLogPathByName(targetName);
4758
- }
4759
- }
4760
- if (!logPath) {
4761
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
4762
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
4763
- process.exit(1);
4764
- }
4765
- if (openInViewer) {
4766
- openLogFile(logPath);
4767
- return;
4768
- }
4769
- viewLogWithTail(logPath, { follow: false, lines });
4770
- return;
4845
+ async function autoUpdateStaleSubscription(options = {}) {
4846
+ const allSubs = getSubscriptionsWithCache();
4847
+ const staleSubs = allSubs.filter(needsAutoUpdate);
4848
+ if (staleSubs.length === 0) {
4849
+ return { total: 0, updated: 0, failed: 0 };
4771
4850
  }
4772
- const logs = listLogs();
4773
- const all = [];
4774
- if (logs.current) all.push(logs.current);
4775
- all.push(...logs.archives);
4776
- if (all.length === 0) {
4777
- console.log("\u6682\u65E0\u65E5\u5FD7");
4778
- return;
4851
+ if (staleSubs.length === 1) {
4852
+ const sub = staleSubs[0];
4853
+ const interval = resolveUpdateInterval(sub.url, sub.update_interval);
4854
+ console.log(`\u8BA2\u9605 "${sub.name}" \u8D85\u8FC7 ${interval} \u5C0F\u65F6\u672A\u66F4\u65B0\uFF0C\u6B63\u5728\u66F4\u65B0...`);
4855
+ } else {
4856
+ console.log(`\u68C0\u67E5\u5230 ${staleSubs.length} \u4E2A\u8BA2\u9605\u9700\u8981\u66F4\u65B0\uFF0C\u6B63\u5728\u5E76\u884C\u66F4\u65B0...`);
4779
4857
  }
4780
- console.log("");
4781
- console.log("\u65E5\u5FD7\u5217\u8868:");
4782
- console.log("");
4783
- let archiveCounter = 0;
4784
- for (const log of all) {
4785
- let num;
4786
- if (log.isCurrent) {
4787
- num = " 0";
4788
- } else {
4789
- archiveCounter++;
4790
- num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
4791
- }
4792
- const time = formatDate(log.mtime);
4793
- const size = formatBytes(log.size);
4794
- const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
4795
- console.log(` ${num}. ${name}`);
4796
- console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
4797
- if (!log.isCurrent) {
4798
- console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
4858
+ const timeoutMs = options.timeout ?? DEFAULT_AUTO_UPDATE_TIMEOUT;
4859
+ const controller = new AbortController();
4860
+ let results;
4861
+ try {
4862
+ results = await withTimeout(Promise.all(staleSubs.map((sub) => tryUpdateOne(sub, controller.signal))), timeoutMs);
4863
+ } catch (e) {
4864
+ if (e instanceof TimeoutError) {
4865
+ controller.abort();
4866
+ console.log(colors.yellow(`\u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 (${timeoutMs / 1e3}s)\uFF0C\u8DF3\u8FC7\u66F4\u65B0\uFF0C\u4F7F\u7528\u7F13\u5B58\u914D\u7F6E`));
4867
+ return { total: staleSubs.length, updated: 0, failed: staleSubs.length };
4799
4868
  }
4800
- console.log("");
4869
+ throw e;
4801
4870
  }
4802
- console.log("\u7528\u6CD5:");
4803
- console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
4804
- console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
4805
- console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
4806
- console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
4807
- console.log("");
4808
- }
4809
-
4810
- // src/commands/overwrite.ts
4811
- import path6 from "path";
4812
-
4813
- // src/subscription.ts
4814
- function isGithubUrl(url) {
4815
- return /github\.com|raw\.githubusercontent\.com/i.test(url);
4816
- }
4817
- function getDefaultUpdateInterval(url) {
4818
- return isGithubUrl(url) ? DEFAULT_UPDATE_INTERVAL_HOURS_GITHUB : DEFAULT_UPDATE_INTERVAL_HOURS;
4819
- }
4820
- function resolveUpdateInterval(url, cachedInterval) {
4821
- return cachedInterval && cachedInterval > 0 ? cachedInterval : getDefaultUpdateInterval(url);
4822
- }
4823
- var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, schema: CORE_SCHEMA };
4824
- var HTTP_CLIENT2 = createHttpClient({ timeout: 6e4 });
4825
- function isMultiUrl(url) {
4826
- return url.includes(",");
4827
- }
4828
- function splitUrls(url) {
4829
- return url.split(",").map((u) => u.trim()).filter(Boolean);
4830
- }
4831
- function loadSubscriptionConfig(subName) {
4832
- const rawContent = readSubscriptionRawConfig(subName);
4833
- if (!rawContent) {
4834
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4871
+ let updatedCount = 0;
4872
+ for (const r of results) {
4873
+ if (r.success) updatedCount++;
4874
+ printUpdateResult(r);
4835
4875
  }
4836
- const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4837
- return {
4838
- raw,
4839
- proxies: raw.proxies || [],
4840
- proxyGroups: raw["proxy-groups"] || []
4841
- };
4842
- }
4843
- function saveSubscriptionConfig(subName, parsed) {
4844
- normalizeProxyNamesBeforeSave(parsed);
4845
- parsed.raw.proxies = parsed.proxies;
4846
- parsed.raw["proxy-groups"] = parsed.proxyGroups;
4847
- saveSubscriptionRawConfig(subName, dump(parsed.raw, YAML_DUMP_OPTS));
4876
+ return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
4848
4877
  }
4849
- function parseUserInfo(header) {
4850
- if (!header) return null;
4851
- const info = {};
4852
- const parts = header.split(";").map((p) => p.trim());
4853
- for (const part of parts) {
4854
- const [key, val] = part.split("=").map((s) => s.trim());
4855
- if (key && val !== void 0) {
4856
- const numVal = parseFloat(val);
4857
- info[key] = Number.isNaN(numVal) ? 0 : numVal;
4878
+ async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = CONTROLLER_BASE_URL) {
4879
+ const encodedName = encodeURIComponent(proxyName);
4880
+ const url = `${apiBase}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4881
+ try {
4882
+ const response = await client.get(url);
4883
+ const data = JSON.parse(response.data);
4884
+ if (data.delay && data.delay > 0) {
4885
+ return { name: proxyName, delay: data.delay };
4886
+ }
4887
+ return { name: proxyName, delay: null, error: data.message || "no delay" };
4888
+ } catch (e) {
4889
+ const err = e;
4890
+ let errorMsg = "timeout";
4891
+ if (err.response?.data?.message) {
4892
+ errorMsg = String(err.response.data.message);
4893
+ } else if (err.message) {
4894
+ errorMsg = err.message;
4858
4895
  }
4896
+ return { name: proxyName, delay: null, error: errorMsg };
4859
4897
  }
4860
- return info;
4861
- }
4862
- function parsePositiveInterval(header) {
4863
- if (!header) return null;
4864
- const n = parseInt(header, 10);
4865
- return Number.isFinite(n) && n > 0 ? n : null;
4866
- }
4867
- function parseUsernameFromContentDisposition(header) {
4868
- if (!header) return null;
4869
- const match = header.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
4870
- if (!match) return null;
4871
- const filename = match[1];
4872
- const parts = filename.split("/");
4873
- return parts[parts.length - 1] || null;
4874
- }
4875
- function extractSubscriptionMeta(headers) {
4876
- return {
4877
- userInfo: parseUserInfo(headers?.get("subscription-userinfo") ?? null),
4878
- updateInterval: parsePositiveInterval(headers?.get("profile-update-interval")),
4879
- webPageUrl: headers?.get("profile-web-page-url") || null,
4880
- username: parseUsernameFromContentDisposition(headers?.get("content-disposition") ?? null)
4881
- };
4882
4898
  }
4883
- function saveSubscriptionMeta(subName, meta) {
4884
- const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4885
- if (meta.userInfo) {
4886
- cacheData.upload = meta.userInfo.upload;
4887
- cacheData.download = meta.userInfo.download;
4888
- cacheData.total = meta.userInfo.total;
4889
- cacheData.expire = meta.userInfo.expire;
4899
+ async function testSubscriptionProxies(subName, options = {}) {
4900
+ const {
4901
+ timeout = DEFAULT_TEST_TIMEOUT,
4902
+ concurrency = DEFAULT_TEST_CONCURRENCY,
4903
+ testUrl = DEFAULT_TEST_URL,
4904
+ apiBase = CONTROLLER_BASE_URL,
4905
+ onResult
4906
+ } = options;
4907
+ const { proxies } = options.parsed || loadSubscriptionConfig(subName);
4908
+ if (proxies.length === 0) {
4909
+ return { total: 0, alive: 0, dead: 0, results: [] };
4890
4910
  }
4891
- if (meta.updateInterval) cacheData.update_interval = meta.updateInterval;
4892
- if (meta.webPageUrl) cacheData.web_page_url = meta.webPageUrl;
4893
- if (meta.username) cacheData.username = meta.username;
4894
- saveSubscriptionCache(subName, cacheData);
4895
- }
4896
- function formatProxySummary(info) {
4897
- const parts = [];
4898
- if (info.proxyGroups && info.proxyGroups > 0) parts.push(`${info.proxyGroups} \u7EC4`);
4899
- parts.push(`${info.proxies || 0} \u8282\u70B9`);
4900
- return parts.join(", ");
4901
- }
4902
- function getActiveSubscription() {
4903
- const subs = getSubscriptions();
4904
- if (subs.length === 0) return null;
4905
- const settings = readSettings();
4906
- const activeName = settings.active_subscription;
4907
- if (activeName) {
4908
- const found = subs.find((s) => s.name === activeName);
4909
- if (found) return found;
4911
+ const client = createHttpClient({ timeout: timeout + 3e3 });
4912
+ const results = new Array(proxies.length);
4913
+ let completedCount = 0;
4914
+ let nextIndex = 0;
4915
+ async function runNext() {
4916
+ while (nextIndex < proxies.length) {
4917
+ const idx = nextIndex++;
4918
+ const result = await testProxyDelay(proxies[idx].name, timeout, testUrl, client, apiBase);
4919
+ results[idx] = result;
4920
+ onResult?.(result, completedCount, proxies.length);
4921
+ completedCount++;
4922
+ }
4910
4923
  }
4911
- return subs[0];
4924
+ const workers = Array.from({ length: Math.min(concurrency, proxies.length) }, () => runNext());
4925
+ await Promise.all(workers);
4926
+ const alive = results.filter((r) => r.delay !== null).length;
4927
+ return { total: results.length, alive, dead: results.length - alive, results };
4912
4928
  }
4913
- function findSubscriptionFuzzy(subs, pattern) {
4914
- const lowerPattern = pattern.toLowerCase();
4915
- const exact = [];
4916
- const prefix = [];
4917
- const includes = [];
4918
- for (const s of subs) {
4919
- const name = s.name.toLowerCase();
4920
- if (name === lowerPattern) {
4921
- exact.push(s);
4922
- } else if (name.startsWith(lowerPattern)) {
4923
- prefix.push(s);
4924
- } else if (name.includes(lowerPattern)) {
4925
- includes.push(s);
4929
+ function normalizeProxyNamesBeforeSave(parsed) {
4930
+ const { proxies, proxyGroups } = parsed;
4931
+ const renameMap = /* @__PURE__ */ new Map();
4932
+ const usedNames = /* @__PURE__ */ new Set();
4933
+ for (const proxy of proxies) {
4934
+ const shortened = proxy.name.replace(/_github\.com\/[^_]+/, "");
4935
+ if (shortened !== proxy.name && !usedNames.has(shortened)) {
4936
+ renameMap.set(proxy.name, shortened);
4937
+ usedNames.add(shortened);
4938
+ } else {
4939
+ usedNames.add(proxy.name);
4926
4940
  }
4927
4941
  }
4928
- if (exact.length > 0) return exact;
4929
- if (prefix.length > 0) return prefix;
4930
- return includes;
4931
- }
4932
- function pickSingleSubscription(subs, pattern) {
4933
- if (subs.length === 0) {
4934
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${pattern}" \u7684\u8BA2\u9605`);
4935
- process.exit(1);
4942
+ if (renameMap.size === 0) return 0;
4943
+ for (const proxy of proxies) {
4944
+ const newName = renameMap.get(proxy.name);
4945
+ if (newName) proxy.name = newName;
4936
4946
  }
4937
- if (subs.length === 1) return subs[0];
4938
- console.error("\u9519\u8BEF: \u5339\u914D\u5230\u591A\u4E2A\u8BA2\u9605\uFF0C\u8BF7\u66F4\u7CBE\u786E\u6307\u5B9A");
4939
- console.log("\n\u5339\u914D\u7684\u8BA2\u9605:");
4940
- for (const s of subs) console.log(` ${s.name}`);
4941
- process.exit(1);
4947
+ for (const group of proxyGroups) {
4948
+ if (Array.isArray(group.proxies)) {
4949
+ group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
4950
+ }
4951
+ }
4952
+ return renameMap.size;
4942
4953
  }
4943
- async function downloadSubscription(url, subName = "default", signal) {
4944
- let response;
4945
- try {
4946
- response = await HTTP_CLIENT2.get(url, { responseType: "text", signal });
4947
- } catch (e) {
4948
- const maskedUrl = maskUrl(url);
4949
- let errorMsg = `\u83B7\u53D6\u8BA2\u9605\u5931\u8D25: ${e.message}`;
4950
- const err = e;
4951
- if (err.response) {
4952
- errorMsg += ` (HTTP ${err.response.status})`;
4954
+ function cleanDeadProxies(parsed, deadNames) {
4955
+ const { proxies, proxyGroups } = parsed;
4956
+ const originalCount = proxies.length;
4957
+ parsed.proxies = proxies.filter((p) => !deadNames.has(p.name));
4958
+ const removedProxies = originalCount - parsed.proxies.length;
4959
+ let updatedGroups = 0;
4960
+ const removedGroupNames = /* @__PURE__ */ new Set();
4961
+ for (const group of proxyGroups) {
4962
+ if (Array.isArray(group.proxies)) {
4963
+ const before = group.proxies.length;
4964
+ group.proxies = group.proxies.filter((name) => !deadNames.has(name));
4965
+ if (group.proxies.length < before) {
4966
+ updatedGroups++;
4967
+ }
4968
+ const hasOtherSource = group.use || group["include-all"] || group["include-all-proxies"];
4969
+ if (group.proxies.length === 0 && !hasOtherSource) {
4970
+ removedGroupNames.add(group.name);
4971
+ }
4953
4972
  }
4954
- errorMsg += `
4955
- URL: ${maskedUrl}`;
4956
- throw new Error(errorMsg);
4957
4973
  }
4958
- const content = response.data;
4959
- if (!content?.trim()) {
4960
- throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4974
+ if (removedGroupNames.size > 0) {
4975
+ parsed.proxyGroups = proxyGroups.filter((g) => !removedGroupNames.has(g.name));
4976
+ for (const group of parsed.proxyGroups) {
4977
+ if (Array.isArray(group.proxies)) {
4978
+ group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
4979
+ }
4980
+ }
4981
+ const rules = parsed.raw.rules;
4982
+ if (Array.isArray(rules)) {
4983
+ parsed.raw.rules = rules.filter((rule) => {
4984
+ if (typeof rule !== "string") return true;
4985
+ return !removedGroupNames.has(getRuleTarget(rule));
4986
+ });
4987
+ }
4961
4988
  }
4962
- const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
4963
- if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4964
- saveSubscriptionRawConfig(subName, content);
4965
- const meta = extractSubscriptionMeta(response.headers);
4966
- saveSubscriptionMeta(subName, meta);
4967
- const proxies = parsed.proxies;
4968
- const proxyGroups = parsed["proxy-groups"];
4969
- return {
4970
- proxies: proxies ? proxies.length : 0,
4971
- proxyGroups: proxyGroups ? proxyGroups.length : 0,
4972
- userInfo: meta.userInfo,
4973
- updateInterval: meta.updateInterval,
4974
- webPageUrl: meta.webPageUrl,
4975
- username: meta.username
4976
- };
4989
+ return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
4977
4990
  }
4978
- async function downloadMergedSubscription(urls, subName, signal) {
4979
- const responses = await Promise.all(
4980
- urls.map(async (url, index) => {
4981
- try {
4982
- const response = await HTTP_CLIENT2.get(url, { responseType: "text", signal });
4983
- return { url, index, response, error: null };
4984
- } catch (e) {
4985
- return { url, index, response: null, error: e };
4991
+ async function autoCleanSubscription(subName, options = {}) {
4992
+ const parsed = loadSubscriptionConfig(subName);
4993
+ const { onResult, onRetryRound, rounds = DEFAULT_CLEAN_ROUNDS, ...testOptions } = options;
4994
+ const wrapOnResult = (round) => onResult ? (r, i, t) => onResult(r, i, t, round) : void 0;
4995
+ const summary = await testSubscriptionProxies(subName, {
4996
+ ...testOptions,
4997
+ parsed,
4998
+ onResult: wrapOnResult(1)
4999
+ });
5000
+ let removedProxies = 0;
5001
+ let updatedGroups = 0;
5002
+ let removedGroups = 0;
5003
+ let skipped = false;
5004
+ if (summary.dead > 0) {
5005
+ if (summary.alive === 0 || summary.alive / summary.total < 0.01) {
5006
+ skipped = true;
5007
+ } else {
5008
+ const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
5009
+ const deadProxies = parsed.proxies.filter((p) => deadNames.has(p.name));
5010
+ for (let retry = 0; retry < rounds - 1; retry++) {
5011
+ const round = retry + 2;
5012
+ const retryTargets = deadProxies.filter((p) => deadNames.has(p.name));
5013
+ if (retryTargets.length === 0) break;
5014
+ onRetryRound?.(round, retryTargets.length);
5015
+ const retryParsed = { raw: {}, proxies: retryTargets, proxyGroups: [] };
5016
+ const retrySummary = await testSubscriptionProxies(subName, {
5017
+ ...testOptions,
5018
+ parsed: retryParsed,
5019
+ onResult: wrapOnResult(round)
5020
+ });
5021
+ for (const r of retrySummary.results) {
5022
+ if (r.delay !== null) {
5023
+ deadNames.delete(r.name);
5024
+ }
5025
+ }
5026
+ }
5027
+ summary.dead = deadNames.size;
5028
+ summary.alive = summary.total - summary.dead;
5029
+ if (deadNames.size > 0) {
5030
+ const cleanResult = cleanDeadProxies(parsed, deadNames);
5031
+ removedProxies = cleanResult.removedProxies;
5032
+ updatedGroups = cleanResult.updatedGroups;
5033
+ removedGroups = cleanResult.removedGroups;
4986
5034
  }
4987
- })
4988
- );
4989
- for (const r of responses) {
4990
- if (r.error) {
4991
- const maskedUrl = maskUrl(r.url);
4992
- throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${r.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${r.error.message}
4993
- URL: ${maskedUrl}`);
4994
5035
  }
4995
5036
  }
4996
- const parsed = responses.map((r, i) => {
4997
- const content = r.response?.data;
4998
- if (!content?.trim()) throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A URL \u5185\u5BB9\u4E3A\u7A7A`);
4999
- return parseYamlOrJson(content, `\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A`);
5000
- });
5001
- const base = parsed[0];
5002
- const baseProxies = base.proxies || [];
5003
- const seenNames = new Set(baseProxies.map((p) => p.name));
5004
- for (let i = 1; i < parsed.length; i++) {
5005
- const extraProxies = parsed[i].proxies || [];
5006
- for (const proxy of extraProxies) {
5007
- if (!seenNames.has(proxy.name)) {
5008
- baseProxies.push(proxy);
5009
- seenNames.add(proxy.name);
5037
+ if (!skipped && removedProxies > 0) {
5038
+ saveSubscriptionConfig(subName, parsed);
5039
+ }
5040
+ return { summary, removedProxies, updatedGroups, removedGroups, skipped };
5041
+ }
5042
+
5043
+ // src/commands/daemon.ts
5044
+ function printDaemonStatus() {
5045
+ const status = getDaemonStatus();
5046
+ const stateText = status.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
5047
+ console.log(`${colors.gray("\u4FDD\u6D3B: ")}${stateText}`);
5048
+ if (status.enabled) {
5049
+ const runText = isDaemonRunning(status) ? colors.green(`\u8FD0\u884C\u4E2D (PID ${status.pid})`) : colors.yellow("\u672A\u8FD0\u884C");
5050
+ console.log(`${colors.gray("\u5185\u6838: ")}${runText}`);
5051
+ }
5052
+ console.log("");
5053
+ if (status.enabled) {
5054
+ console.log("\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
5055
+ } else {
5056
+ console.log("\u5F00\u542F\u4FDD\u6D3B: mihomo daemon on");
5057
+ console.log(colors.gray(" \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF08\u4EC5 Mixed \u6A21\u5F0F\uFF09"));
5058
+ }
5059
+ console.log("");
5060
+ }
5061
+ async function cmdDaemon(args) {
5062
+ const action = args?.[1];
5063
+ if (action === "on" || action === "enable") {
5064
+ if (!hasKernel()) {
5065
+ console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5066
+ process.exit(1);
5067
+ }
5068
+ const sub = getActiveSubscription();
5069
+ if (!sub) {
5070
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
5071
+ process.exit(1);
5072
+ }
5073
+ let configInfo;
5074
+ try {
5075
+ configInfo = prepareConfigForStart("mixed", sub.name);
5076
+ } catch (e) {
5077
+ console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
5078
+ process.exit(1);
5079
+ }
5080
+ console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u5B89\u88C5\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1\uFF08LaunchDaemon\uFF09"));
5081
+ console.log(colors.gray("\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u9700\u8981 root\uFF0C\u4EE5\u89E3\u51B3\u5C40\u57DF\u7F51\u8BBF\u95EE\u53D7\u9650\u95EE\u9898"));
5082
+ try {
5083
+ enableDaemon();
5084
+ } catch (e) {
5085
+ console.error(`${colors.red("\u542F\u7528\u4FDD\u6D3B\u5931\u8D25:")} ${e.message}`);
5086
+ process.exit(1);
5087
+ }
5088
+ console.log(`${colors.green("\u5DF2\u542F\u7528\u4FDD\u6D3B")} \xB7 ${sub.name} \xB7 ${formatProxySummary(configInfo)}`);
5089
+ console.log(colors.gray("\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF0C\u4EE3\u7406\u5C06\u5728\u540E\u53F0\u5E38\u9A7B"));
5090
+ console.log("");
5091
+ await sleep(DAEMON_BOOT_WAIT_MS);
5092
+ printDaemonStatus();
5093
+ return;
5094
+ }
5095
+ if (action === "off" || action === "disable") {
5096
+ if (!isDaemonEnabled()) {
5097
+ console.log("\u4FDD\u6D3B\u5DF2\u662F\u5173\u95ED\u72B6\u6001");
5098
+ console.log("");
5099
+ printDaemonStatus();
5100
+ return;
5101
+ }
5102
+ console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u79FB\u9664\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1"));
5103
+ try {
5104
+ disableDaemon();
5105
+ } catch (e) {
5106
+ console.error(`${colors.red("\u5173\u95ED\u4FDD\u6D3B\u5931\u8D25:")} ${e.message}`);
5107
+ process.exit(1);
5108
+ }
5109
+ console.log(`${colors.green("\u5DF2\u5173\u95ED\u4FDD\u6D3B")}\uFF0C\u4EE3\u7406\u5DF2\u505C\u6B62`);
5110
+ console.log(colors.gray("\u91CD\u65B0\u542F\u7528: mihomo daemon on"));
5111
+ console.log("");
5112
+ return;
5113
+ }
5114
+ if (action !== void 0 && action !== "status") {
5115
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684 daemon \u5B50\u547D\u4EE4: ${action}`);
5116
+ console.log("");
5117
+ console.log("\u53EF\u7528\u5B50\u547D\u4EE4: on, off, status");
5118
+ process.exit(1);
5119
+ }
5120
+ console.log("");
5121
+ printDaemonStatus();
5122
+ }
5123
+
5124
+ // src/commands/directory.ts
5125
+ function cmdDirectory(args) {
5126
+ const action = args?.[1];
5127
+ if (action === "open") {
5128
+ const target = args[2];
5129
+ if (!target || target === "root") {
5130
+ console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
5131
+ const success = openUrl(USER_DATA_DIR);
5132
+ if (!success) {
5133
+ console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
5010
5134
  }
5135
+ return;
5136
+ }
5137
+ const key = target.toLowerCase();
5138
+ const targetInfo = Object.hasOwn(DIRECTORY_TARGETS, key) ? DIRECTORY_TARGETS[key] : void 0;
5139
+ if (targetInfo) {
5140
+ const targetPath = targetInfo.path || USER_DATA_DIR;
5141
+ console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
5142
+ const success = openUrl(targetPath);
5143
+ if (!success) {
5144
+ console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${targetPath}`);
5145
+ }
5146
+ return;
5147
+ }
5148
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`);
5149
+ console.log("");
5150
+ console.log("\u53EF\u7528\u76EE\u6807:");
5151
+ console.log(" root (\u9ED8\u8BA4) \u6839\u76EE\u5F55");
5152
+ for (const [key2, val] of Object.entries(DIRECTORY_TARGETS)) {
5153
+ if (key2 !== "root") {
5154
+ console.log(` ${key2.padEnd(14)}${val.label}`);
5155
+ }
5156
+ }
5157
+ console.log("");
5158
+ process.exit(1);
5159
+ }
5160
+ console.log("");
5161
+ console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
5162
+ console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
5163
+ console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
5164
+ console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
5165
+ console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
5166
+ console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
5167
+ console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
5168
+ console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
5169
+ console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
5170
+ console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5171
+ console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5172
+ console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
5173
+ console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
5174
+ console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
5175
+ console.log("");
5176
+ console.log("\u6253\u5F00\u76EE\u5F55:");
5177
+ console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
5178
+ console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
5179
+ console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
5180
+ console.log(" mihomo dir open data \u6253\u5F00 mihomo \u6570\u636E\u76EE\u5F55");
5181
+ console.log(" mihomo dir open runtime \u6253\u5F00\u8FD0\u884C\u65F6\u76EE\u5F55");
5182
+ console.log(" mihomo dir open kernel \u6253\u5F00\u5185\u6838\u76EE\u5F55");
5183
+ console.log("");
5184
+ console.log("\u73AF\u5883\u53D8\u91CF:");
5185
+ console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
5186
+ console.log("");
5187
+ }
5188
+
5189
+ // src/kernel.ts
5190
+ import { spawnSync as spawnSync5 } from "child_process";
5191
+ import fs7 from "fs";
5192
+ import path6 from "path";
5193
+
5194
+ // node_modules/compare-versions/lib/esm/utils.js
5195
+ var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
5196
+ var validateAndParse = (version) => {
5197
+ if (typeof version !== "string") {
5198
+ throw new TypeError("Invalid argument expected string");
5199
+ }
5200
+ const match = version.match(semver);
5201
+ if (!match) {
5202
+ throw new Error(`Invalid argument not valid semver ('${version}' received)`);
5203
+ }
5204
+ match.shift();
5205
+ return match;
5206
+ };
5207
+ var isWildcard = (s) => s === "*" || s === "x" || s === "X";
5208
+ var tryParse = (v) => {
5209
+ const n = parseInt(v, 10);
5210
+ return isNaN(n) ? v : n;
5211
+ };
5212
+ var forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b];
5213
+ var compareStrings = (a, b) => {
5214
+ if (isWildcard(a) || isWildcard(b))
5215
+ return 0;
5216
+ const [ap, bp] = forceType(tryParse(a), tryParse(b));
5217
+ if (ap > bp)
5218
+ return 1;
5219
+ if (ap < bp)
5220
+ return -1;
5221
+ return 0;
5222
+ };
5223
+ var compareSegments = (a, b) => {
5224
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
5225
+ const r = compareStrings(a[i] || "0", b[i] || "0");
5226
+ if (r !== 0)
5227
+ return r;
5228
+ }
5229
+ return 0;
5230
+ };
5231
+
5232
+ // node_modules/compare-versions/lib/esm/compareVersions.js
5233
+ var compareVersions = (v1, v2) => {
5234
+ const n1 = validateAndParse(v1);
5235
+ const n2 = validateAndParse(v2);
5236
+ const p1 = n1.pop();
5237
+ const p2 = n2.pop();
5238
+ const r = compareSegments(n1, n2);
5239
+ if (r !== 0)
5240
+ return r;
5241
+ if (p1 && p2) {
5242
+ return compareSegments(p1.split("."), p2.split("."));
5243
+ } else if (p1 || p2) {
5244
+ return p1 ? -1 : 1;
5245
+ }
5246
+ return 0;
5247
+ };
5248
+
5249
+ // src/kernel.ts
5250
+ var GITHUB_REPO = "MetaCubeX/mihomo";
5251
+ var KERNEL_HTTP_TIMEOUT = 12e4;
5252
+ var KERNEL_DOWNLOAD_TIMEOUT = 18e4;
5253
+ var HTTP_CLIENT2 = createHttpClient({ timeout: KERNEL_HTTP_TIMEOUT });
5254
+ function withMirror(url, mirror) {
5255
+ if (mirror && (url.startsWith("https://github.com/") || url.startsWith("https://api.github.com/"))) {
5256
+ return mirror + url;
5257
+ }
5258
+ return url;
5259
+ }
5260
+ function getArch() {
5261
+ const arch = process.arch;
5262
+ if (arch === "arm64") return "arm64";
5263
+ if (arch === "x64") return "amd64";
5264
+ return arch;
5265
+ }
5266
+ function findMatchingAsset(assets, platform, arch) {
5267
+ const prefix = `mihomo-${platform}-${arch}`;
5268
+ const matchingAssets = assets.filter(
5269
+ (a) => a.name.startsWith(prefix) && a.name.endsWith(".gz") || a.name.startsWith(`${prefix}-`) && a.name.endsWith(".gz")
5270
+ );
5271
+ if (matchingAssets.length === 0) return null;
5272
+ if (matchingAssets.length === 1) return matchingAssets[0];
5273
+ const standardAsset = matchingAssets.find((a) => {
5274
+ const nameWithoutGz = a.name.slice(0, -3);
5275
+ const parts = nameWithoutGz.split("-");
5276
+ const lastPart = parts[parts.length - 1];
5277
+ return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go") && !nameWithoutGz.includes("-compatible");
5278
+ });
5279
+ return standardAsset || matchingAssets[0];
5280
+ }
5281
+ async function getLatestRelease(repo, mirror) {
5282
+ const url = withMirror(`https://api.github.com/repos/${repo}/releases`, mirror);
5283
+ const response = await HTTP_CLIENT2.get(url, { responseType: "json" });
5284
+ const releases = response.data;
5285
+ if (!Array.isArray(releases) || releases.length === 0) {
5286
+ throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7248\u672C\u4FE1\u606F");
5287
+ }
5288
+ const stableReleases = releases.filter(
5289
+ (r) => !r.prerelease && !r.tag_name.toLowerCase().includes("alpha") && !r.tag_name.toLowerCase().includes("beta") && !r.tag_name.toLowerCase().includes("prerelease")
5290
+ );
5291
+ return stableReleases.length > 0 ? stableReleases[0] : releases[0];
5292
+ }
5293
+ async function checkUpdate(mirror) {
5294
+ const currentVersion = getKernelVersion();
5295
+ const latest = await getLatestRelease(GITHUB_REPO, mirror);
5296
+ const latestVersion = latest.tag_name;
5297
+ let needsUpdate = false;
5298
+ const currentDisplay = currentVersion || "\u672A\u5B89\u88C5";
5299
+ if (!currentVersion) {
5300
+ needsUpdate = true;
5301
+ } else {
5302
+ try {
5303
+ needsUpdate = compareVersions(latestVersion.replace(/^v/, ""), currentVersion.replace(/^v/, "")) > 0;
5304
+ } catch {
5305
+ needsUpdate = latestVersion !== currentVersion;
5011
5306
  }
5012
5307
  }
5013
- base.proxies = baseProxies;
5014
- const mergedContent = dump(base, YAML_DUMP_OPTS);
5015
- saveSubscriptionRawConfig(subName, mergedContent);
5016
- const meta = extractSubscriptionMeta(responses[0].response?.headers);
5017
- saveSubscriptionMeta(subName, meta);
5018
- const proxyGroups = base["proxy-groups"];
5019
5308
  return {
5020
- proxies: baseProxies.length,
5021
- proxyGroups: proxyGroups ? proxyGroups.length : 0,
5022
- userInfo: meta.userInfo,
5023
- updateInterval: meta.updateInterval,
5024
- webPageUrl: meta.webPageUrl,
5025
- username: meta.username
5309
+ current: currentDisplay,
5310
+ latest: latestVersion,
5311
+ needsUpdate,
5312
+ assets: latest.assets,
5313
+ release: latest
5026
5314
  };
5027
5315
  }
5028
- function prepareConfigForStart(mode, subName = "default") {
5029
- const rawContent = readSubscriptionRawConfig(subName);
5030
- if (!rawContent) {
5031
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
5316
+ function findBinaryInDir(dir, maxDepth = 4) {
5317
+ if (maxDepth <= 0) return null;
5318
+ const files = fs7.readdirSync(dir);
5319
+ for (const f of files) {
5320
+ const fullPath = path6.join(dir, f);
5321
+ const stat = fs7.statSync(fullPath);
5322
+ if (stat.isDirectory()) {
5323
+ const found = findBinaryInDir(fullPath, maxDepth - 1);
5324
+ if (found) return found;
5325
+ continue;
5326
+ }
5327
+ if (f === "mihomo") return fullPath;
5328
+ if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
5329
+ }
5330
+ return null;
5331
+ }
5332
+ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5333
+ ensureDirs();
5334
+ const latest = releaseInfo || await getLatestRelease(GITHUB_REPO, mirror);
5335
+ const arch = getArch();
5336
+ const platform = process.platform;
5337
+ const asset = findMatchingAsset(latest.assets, platform, arch);
5338
+ if (!asset) {
5339
+ const available = latest.assets.map((a) => a.name).join(", ");
5340
+ let hint = "";
5341
+ if (available) hint = `
5342
+ \u53EF\u7528\u7248\u672C: ${available}`;
5343
+ throw new Error(`\u672A\u627E\u5230\u5339\u914D\u7684\u5185\u6838\u6587\u4EF6
5344
+ \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
5032
5345
  }
5033
- const buildResult = buildConfig(rawContent, mode);
5034
- if (buildResult.warnings.length > 0) {
5035
- for (const warning of buildResult.warnings) {
5036
- console.log(`${colors.yellow("\u81EA\u52A8\u4FEE\u590D:")} ${warning}`);
5346
+ const downloadUrl = withMirror(asset.browser_download_url, mirror);
5347
+ const tempPath = path6.join(DIRS.kernel, asset.name);
5348
+ const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
5349
+ if (mirror && progressCallback) {
5350
+ progressCallback("\u63D0\u793A: \u7ECF\u7B2C\u4E09\u65B9\u955C\u50CF\u4E2D\u8F6C\u4E0B\u8F7D\uFF0C\u65E0\u6CD5\u9A8C\u8BC1\u6765\u6E90\u5B8C\u6574\u6027\uFF0C\u5EFA\u8BAE\u76F4\u8FDE\u6216\u81EA\u884C\u6821\u9A8C\u4EA7\u7269");
5351
+ }
5352
+ if (progressCallback) {
5353
+ progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
5354
+ }
5355
+ const curlResult = spawnSync5(
5356
+ "curl",
5357
+ ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
5358
+ { stdio: "inherit" }
5359
+ );
5360
+ if (curlResult.status !== 0) {
5361
+ try {
5362
+ fs7.unlinkSync(tempPath);
5363
+ } catch {
5037
5364
  }
5038
- console.log("");
5365
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
5039
5366
  }
5040
- writeMihomoConfig(buildResult.config);
5041
- writeDebugConfig(buildResult);
5042
- const proxies = buildResult.config.proxies;
5043
- const proxyGroups = buildResult.config["proxy-groups"];
5044
- return {
5045
- proxies: proxies ? proxies.length : 0,
5046
- proxyGroups: proxyGroups ? proxyGroups.length : 0
5047
- };
5048
- }
5049
- function needsAutoUpdate(sub) {
5050
- if (!sub.updated_at) return true;
5051
- const lastUpdate = new Date(sub.updated_at).getTime();
5052
- if (Number.isNaN(lastUpdate)) return true;
5053
- const intervalHours = resolveUpdateInterval(sub.url, sub.update_interval);
5054
- const intervalMs = intervalHours * 60 * 60 * 1e3;
5055
- return Date.now() - lastUpdate > intervalMs;
5056
- }
5057
- async function tryUpdateOne(sub, signal) {
5367
+ if (!fs7.existsSync(tempPath)) {
5368
+ throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
5369
+ }
5370
+ if (progressCallback) {
5371
+ progressCallback("\u89E3\u538B\u5185\u6838...");
5372
+ }
5373
+ const extractPath = DIRS.kernel;
5374
+ let extractedBinary = null;
5058
5375
  try {
5059
- let info;
5060
- if (isMultiUrl(sub.url)) {
5061
- info = await downloadMergedSubscription(splitUrls(sub.url), sub.name, signal);
5062
- } else {
5063
- info = await downloadSubscription(sub.url, sub.name, signal);
5376
+ if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5377
+ const tarResult = spawnSync5("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
5378
+ if (tarResult.error) throw tarResult.error;
5379
+ if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
5380
+ } else if (tempPath.endsWith(".gz")) {
5381
+ const baseName = path6.basename(tempPath, ".gz");
5382
+ const outputPath = path6.join(extractPath, baseName);
5383
+ const gzipResult = spawnSync5("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
5384
+ if (gzipResult.error) throw gzipResult.error;
5385
+ if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
5386
+ fs7.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
5387
+ extractedBinary = outputPath;
5064
5388
  }
5065
- return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
5066
5389
  } catch (e) {
5067
- return { name: sub.name, success: false, error: e.message };
5390
+ try {
5391
+ fs7.unlinkSync(tempPath);
5392
+ } catch {
5393
+ }
5394
+ throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
5068
5395
  }
5069
- }
5070
- function printUpdateResult(r) {
5071
- if (r.success) {
5072
- console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
5073
- } else {
5074
- console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
5396
+ const foundBinary = extractedBinary || findBinaryInDir(extractPath);
5397
+ if (!foundBinary) {
5398
+ try {
5399
+ fs7.unlinkSync(tempPath);
5400
+ } catch {
5401
+ }
5402
+ throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
5075
5403
  }
5076
- }
5077
- async function autoUpdateStaleSubscription(options = {}) {
5078
- const allSubs = getSubscriptionsWithCache();
5079
- const staleSubs = allSubs.filter(needsAutoUpdate);
5080
- if (staleSubs.length === 0) {
5081
- return { total: 0, updated: 0, failed: 0 };
5404
+ const targetPath = PATHS.mihomoBinary;
5405
+ if (foundBinary !== targetPath) {
5406
+ if (fs7.existsSync(targetPath)) {
5407
+ fs7.chmodSync(targetPath, 493);
5408
+ try {
5409
+ fs7.unlinkSync(targetPath);
5410
+ } catch {
5411
+ }
5412
+ }
5413
+ fs7.renameSync(foundBinary, targetPath);
5082
5414
  }
5083
- if (staleSubs.length === 1) {
5084
- const sub = staleSubs[0];
5085
- const interval = resolveUpdateInterval(sub.url, sub.update_interval);
5086
- console.log(`\u8BA2\u9605 "${sub.name}" \u8D85\u8FC7 ${interval} \u5C0F\u65F6\u672A\u66F4\u65B0\uFF0C\u6B63\u5728\u66F4\u65B0...`);
5087
- } else {
5088
- console.log(`\u68C0\u67E5\u5230 ${staleSubs.length} \u4E2A\u8BA2\u9605\u9700\u8981\u66F4\u65B0\uFF0C\u6B63\u5728\u5E76\u884C\u66F4\u65B0...`);
5415
+ fs7.chmodSync(targetPath, 493);
5416
+ if (progressCallback) {
5417
+ progressCallback("\u6821\u9A8C\u5185\u6838...");
5089
5418
  }
5090
- const timeoutMs = options.timeout ?? DEFAULT_AUTO_UPDATE_TIMEOUT;
5091
- const controller = new AbortController();
5092
- let results;
5093
- try {
5094
- results = await withTimeout(Promise.all(staleSubs.map((sub) => tryUpdateOne(sub, controller.signal))), timeoutMs);
5095
- } catch (e) {
5096
- if (e instanceof TimeoutError) {
5097
- controller.abort();
5098
- console.log(colors.yellow(`\u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 (${timeoutMs / 1e3}s)\uFF0C\u8DF3\u8FC7\u66F4\u65B0\uFF0C\u4F7F\u7528\u7F13\u5B58\u914D\u7F6E`));
5099
- return { total: staleSubs.length, updated: 0, failed: staleSubs.length };
5419
+ const check = spawnSync5(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
5420
+ const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
5421
+ if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
5422
+ try {
5423
+ fs7.unlinkSync(targetPath);
5424
+ } catch {
5100
5425
  }
5101
- throw e;
5426
+ try {
5427
+ fs7.unlinkSync(tempPath);
5428
+ } catch {
5429
+ }
5430
+ throw new Error(`\u5185\u6838\u81EA\u68C0\u5931\u8D25\uFF08\u53EF\u80FD\u4E0B\u8F7D\u635F\u574F\u6216\u67B6\u6784\u4E0D\u5339\u914D\uFF09\uFF0C\u5DF2\u5220\u9664
5431
+ \u9000\u51FA\u7801: ${check.status}
5432
+ \u8F93\u51FA: ${checkOutput || "(\u7A7A)"}`);
5102
5433
  }
5103
- let updatedCount = 0;
5104
- for (const r of results) {
5105
- if (r.success) updatedCount++;
5106
- printUpdateResult(r);
5434
+ try {
5435
+ fs7.unlinkSync(tempPath);
5436
+ } catch {
5107
5437
  }
5108
- return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
5438
+ clearKernelVersionCache();
5439
+ return { version: latest.tag_name, path: targetPath };
5109
5440
  }
5110
- var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
5111
- async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API_BASE) {
5112
- const encodedName = encodeURIComponent(proxyName);
5113
- const url = `${apiBase}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
5441
+
5442
+ // src/commands/kernel.ts
5443
+ async function cmdKernel(args) {
5444
+ const mirrorInfo = parseMirrorArg(args);
5445
+ const effectiveMirror = mirrorInfo.mirror;
5446
+ if (effectiveMirror) {
5447
+ const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
5448
+ console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
5449
+ console.log("");
5450
+ }
5451
+ console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
5114
5452
  try {
5115
- const response = await client.get(url);
5116
- const data = JSON.parse(response.data);
5117
- if (data.delay && data.delay > 0) {
5118
- return { name: proxyName, delay: data.delay };
5453
+ const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
5454
+ const info = await checkUpdate(apiMirror);
5455
+ console.log(`\u5F53\u524D: ${info.current}`);
5456
+ console.log(`\u6700\u65B0: ${info.latest}`);
5457
+ if (!info.needsUpdate) {
5458
+ console.log("\u5DF2\u662F\u6700\u65B0\u7248\u672C");
5459
+ } else {
5460
+ console.log("\n\u6B63\u5728\u4E0B\u8F7D...");
5461
+ const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
5462
+ console.log(`
5463
+ \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
5119
5464
  }
5120
- return { name: proxyName, delay: null, error: data.message || "no delay" };
5121
5465
  } catch (e) {
5466
+ console.error(`
5467
+ \u66F4\u65B0\u5931\u8D25: ${e.message}`);
5122
5468
  const err = e;
5123
- let errorMsg = "timeout";
5124
- if (err.response?.data?.message) {
5125
- errorMsg = String(err.response.data.message);
5126
- } else if (err.message) {
5127
- errorMsg = err.message;
5469
+ if (err.response?.data) {
5470
+ if (err.response.data.message) {
5471
+ console.error(`\u539F\u56E0: ${err.response.data.message}`);
5472
+ }
5473
+ if (err.response.data.documentation_url) {
5474
+ console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
5475
+ }
5128
5476
  }
5129
- return { name: proxyName, delay: null, error: errorMsg };
5477
+ if (!effectiveMirror) {
5478
+ console.error("");
5479
+ console.error("\u63D0\u793A: \u76F4\u8FDE\u5931\u8D25\u6216\u4E0B\u8F7D\u8FC7\u6162\u65F6\u53EF\u4F7F\u7528\u955C\u50CF:");
5480
+ console.error(" mihomo kernel --mirror [\u955C\u50CF] # \u4E0B\u8F7D\u8D70\u955C\u50CF\uFF08\u9ED8\u8BA4 v6.gh-proxy.org\uFF09");
5481
+ console.error(" mihomo kernel --mirror-all [\u955C\u50CF] # API \u548C\u4E0B\u8F7D\u90FD\u8D70\u955C\u50CF");
5482
+ console.error(` \u53EF\u7528\u955C\u50CF: ${AVAILABLE_MIRRORS.join(", ")}`);
5483
+ }
5484
+ process.exit(1);
5130
5485
  }
5131
5486
  }
5132
- async function testSubscriptionProxies(subName, options = {}) {
5133
- const { timeout = DEFAULT_TEST_TIMEOUT, concurrency = DEFAULT_TEST_CONCURRENCY, testUrl = DEFAULT_TEST_URL, apiBase = API_BASE, onResult } = options;
5134
- const { proxies } = options.parsed || loadSubscriptionConfig(subName);
5135
- if (proxies.length === 0) {
5136
- return { total: 0, alive: 0, dead: 0, results: [] };
5137
- }
5138
- const client = createHttpClient({ timeout: timeout + 3e3 });
5139
- const results = new Array(proxies.length);
5140
- let completedCount = 0;
5141
- let nextIndex = 0;
5142
- async function runNext() {
5143
- while (nextIndex < proxies.length) {
5144
- const idx = nextIndex++;
5145
- const result = await testProxyDelay(proxies[idx].name, timeout, testUrl, client, apiBase);
5146
- results[idx] = result;
5147
- onResult?.(result, completedCount, proxies.length);
5148
- completedCount++;
5149
- }
5487
+
5488
+ // src/commands/log.ts
5489
+ function cmdLog(args) {
5490
+ const logPath = getLogPath();
5491
+ if (hasFlag(args, "-o", "--open")) {
5492
+ openLogFile(logPath);
5493
+ return;
5150
5494
  }
5151
- const workers = Array.from({ length: Math.min(concurrency, proxies.length) }, () => runNext());
5152
- await Promise.all(workers);
5153
- const alive = results.filter((r) => r.delay !== null).length;
5154
- return { total: results.length, alive, dead: results.length - alive, results };
5495
+ viewLogWithTail(logPath, { follow: true, lines: 50 });
5155
5496
  }
5156
- function normalizeProxyNamesBeforeSave(parsed) {
5157
- const { proxies, proxyGroups } = parsed;
5158
- const renameMap = /* @__PURE__ */ new Map();
5159
- const usedNames = /* @__PURE__ */ new Set();
5160
- for (const proxy of proxies) {
5161
- const shortened = proxy.name.replace(/_github\.com\/[^_]+/, "");
5162
- if (shortened !== proxy.name && !usedNames.has(shortened)) {
5163
- renameMap.set(proxy.name, shortened);
5164
- usedNames.add(shortened);
5497
+ function cmdLogs(args) {
5498
+ const targetName = getNonFlagArg(args, 1);
5499
+ const lines = parseIntArg(args, "-n", "--lines", 100);
5500
+ const openInViewer = hasFlag(args, "-o", "--open");
5501
+ if (targetName) {
5502
+ let logPath;
5503
+ if (targetName === "current" || targetName === "0") {
5504
+ logPath = getLogPath();
5165
5505
  } else {
5166
- usedNames.add(proxy.name);
5506
+ const parsedIdx = parseInt(targetName, 10);
5507
+ if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
5508
+ const archiveLogs = listLogs();
5509
+ const archive = archiveLogs.archives[parsedIdx - 1];
5510
+ if (!archive) {
5511
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
5512
+ console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5513
+ process.exit(1);
5514
+ }
5515
+ logPath = archive.path;
5516
+ } else {
5517
+ logPath = getLogPathByName(targetName);
5518
+ }
5519
+ }
5520
+ if (!logPath) {
5521
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
5522
+ console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5523
+ process.exit(1);
5167
5524
  }
5168
- }
5169
- if (renameMap.size === 0) return 0;
5170
- for (const proxy of proxies) {
5171
- const newName = renameMap.get(proxy.name);
5172
- if (newName) proxy.name = newName;
5173
- }
5174
- for (const group of proxyGroups) {
5175
- if (Array.isArray(group.proxies)) {
5176
- group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
5525
+ if (openInViewer) {
5526
+ openLogFile(logPath);
5527
+ return;
5177
5528
  }
5529
+ viewLogWithTail(logPath, { follow: false, lines });
5530
+ return;
5178
5531
  }
5179
- return renameMap.size;
5180
- }
5181
- function cleanDeadProxies(parsed, deadNames) {
5182
- const { proxies, proxyGroups } = parsed;
5183
- const originalCount = proxies.length;
5184
- parsed.proxies = proxies.filter((p) => !deadNames.has(p.name));
5185
- const removedProxies = originalCount - parsed.proxies.length;
5186
- let updatedGroups = 0;
5187
- const removedGroupNames = /* @__PURE__ */ new Set();
5188
- for (const group of proxyGroups) {
5189
- if (Array.isArray(group.proxies)) {
5190
- const before = group.proxies.length;
5191
- group.proxies = group.proxies.filter((name) => !deadNames.has(name));
5192
- if (group.proxies.length < before) {
5193
- updatedGroups++;
5194
- }
5195
- if (group.proxies.length === 0) {
5196
- removedGroupNames.add(group.name);
5197
- }
5198
- }
5532
+ const logs = listLogs();
5533
+ const all = [];
5534
+ if (logs.current) all.push(logs.current);
5535
+ all.push(...logs.archives);
5536
+ if (all.length === 0) {
5537
+ console.log("\u6682\u65E0\u65E5\u5FD7");
5538
+ return;
5199
5539
  }
5200
- if (removedGroupNames.size > 0) {
5201
- parsed.proxyGroups = proxyGroups.filter((g) => !removedGroupNames.has(g.name));
5202
- for (const group of parsed.proxyGroups) {
5203
- if (Array.isArray(group.proxies)) {
5204
- group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
5205
- }
5540
+ console.log("");
5541
+ console.log("\u65E5\u5FD7\u5217\u8868:");
5542
+ console.log("");
5543
+ let archiveCounter = 0;
5544
+ for (const log of all) {
5545
+ let num;
5546
+ if (log.isCurrent) {
5547
+ num = " 0";
5548
+ } else {
5549
+ archiveCounter++;
5550
+ num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
5206
5551
  }
5207
- const rules = parsed.raw.rules;
5208
- if (Array.isArray(rules)) {
5209
- parsed.raw.rules = rules.filter((rule) => {
5210
- if (typeof rule !== "string") return true;
5211
- const parts = rule.split(",");
5212
- if (parts.length < 2) return true;
5213
- return !removedGroupNames.has(parts[parts.length - 1].trim());
5214
- });
5552
+ const time = formatDate(log.mtime);
5553
+ const size = formatBytes(log.size);
5554
+ const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
5555
+ console.log(` ${num}. ${name}`);
5556
+ console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
5557
+ if (!log.isCurrent) {
5558
+ console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
5215
5559
  }
5560
+ console.log("");
5216
5561
  }
5217
- return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
5562
+ console.log("\u7528\u6CD5:");
5563
+ console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
5564
+ console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
5565
+ console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
5566
+ console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
5567
+ console.log("");
5218
5568
  }
5219
- async function autoCleanSubscription(subName, options = {}) {
5220
- const parsed = loadSubscriptionConfig(subName);
5221
- const { onResult, onRetryRound, rounds = DEFAULT_CLEAN_ROUNDS, ...testOptions } = options;
5222
- const wrapOnResult = (round) => onResult ? (r, i, t) => onResult(r, i, t, round) : void 0;
5223
- const summary = await testSubscriptionProxies(subName, {
5224
- ...testOptions,
5225
- parsed,
5226
- onResult: wrapOnResult(1)
5227
- });
5228
- let removedProxies = 0;
5229
- let updatedGroups = 0;
5230
- let removedGroups = 0;
5231
- let skipped = false;
5232
- if (summary.dead > 0) {
5233
- if (summary.alive === 0 || summary.alive / summary.total < 0.01) {
5234
- skipped = true;
5235
- } else {
5236
- const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
5237
- const deadProxies = parsed.proxies.filter((p) => deadNames.has(p.name));
5238
- for (let retry = 0; retry < rounds - 1; retry++) {
5239
- const round = retry + 2;
5240
- const retryTargets = deadProxies.filter((p) => deadNames.has(p.name));
5241
- if (retryTargets.length === 0) break;
5242
- onRetryRound?.(round, retryTargets.length);
5243
- const retryParsed = { raw: {}, proxies: retryTargets, proxyGroups: [] };
5244
- const retrySummary = await testSubscriptionProxies(subName, {
5245
- ...testOptions,
5246
- parsed: retryParsed,
5247
- onResult: wrapOnResult(round)
5248
- });
5249
- for (const r of retrySummary.results) {
5250
- if (r.delay !== null) {
5251
- deadNames.delete(r.name);
5252
- }
5253
- }
5254
- }
5255
- summary.dead = deadNames.size;
5256
- summary.alive = summary.total - summary.dead;
5257
- if (deadNames.size > 0) {
5258
- const cleanResult = cleanDeadProxies(parsed, deadNames);
5259
- removedProxies = cleanResult.removedProxies;
5260
- updatedGroups = cleanResult.updatedGroups;
5261
- removedGroups = cleanResult.removedGroups;
5262
- }
5263
- }
5569
+
5570
+ // src/commands/overwrite.ts
5571
+ import path8 from "path";
5572
+
5573
+ // src/runtime.ts
5574
+ function getRuntimeMode() {
5575
+ if (isDaemonEnabled()) return "mixed";
5576
+ return getConfigInfo()?.tun ? "tun" : "mixed";
5577
+ }
5578
+ function getRunningState() {
5579
+ if (isDaemonEnabled()) {
5580
+ const daemon = getDaemonStatus();
5581
+ return { running: isDaemonRunning(daemon), pid: daemon.pid, daemon: true };
5264
5582
  }
5265
- if (!skipped && removedProxies > 0) {
5266
- saveSubscriptionConfig(subName, parsed);
5583
+ const status = getStatus();
5584
+ return { running: status.running, pid: status.pid, daemon: false };
5585
+ }
5586
+ function isRestartNeededOnChange() {
5587
+ return isDaemonEnabled() || getStatus().running;
5588
+ }
5589
+ async function launchOrRestart(mode) {
5590
+ if (isDaemonEnabled()) {
5591
+ await restartDaemon();
5592
+ await sleep(DAEMON_BOOT_WAIT_MS);
5593
+ return getDaemonStatus().pid;
5267
5594
  }
5268
- return { summary, removedProxies, updatedGroups, removedGroups, skipped };
5595
+ const result = await start(mode);
5596
+ return result.pid;
5269
5597
  }
5270
5598
 
5271
5599
  // src/commands/status.ts
5272
5600
  function printStatus() {
5273
5601
  const status = getStatus();
5602
+ const state = getRunningState();
5274
5603
  const info = getConfigInfo();
5275
5604
  const overwriteEnabled = isOverwriteEnabled();
5276
5605
  const overwriteFiles = listOverwriteFile().files;
5277
5606
  const activeSub = getActiveSubscription();
5607
+ const { running, pid, daemon: daemonManaged } = state;
5278
5608
  console.log("");
5279
5609
  let modeLabel = "";
5280
- if (info && status.running) {
5610
+ if (info && running) {
5281
5611
  modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
5282
5612
  }
5283
- const statusText = status.running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
5613
+ const statusText = running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
5284
5614
  console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}${modeLabel}`);
5285
5615
  console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
5286
- if (status.pid) {
5287
- console.log(`${colors.gray("PID: ")}${status.pid}`);
5288
- if (status.processInfo) {
5616
+ if (pid) {
5617
+ console.log(`${colors.gray("PID: ")}${pid}`);
5618
+ if (!daemonManaged && status.processInfo) {
5289
5619
  console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
5290
5620
  }
5291
5621
  }
@@ -5316,6 +5646,9 @@ function printStatus() {
5316
5646
  } else {
5317
5647
  console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
5318
5648
  }
5649
+ if (isDaemonEnabled()) {
5650
+ console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
5651
+ }
5319
5652
  console.log("");
5320
5653
  }
5321
5654
 
@@ -5328,7 +5661,12 @@ function handleStopResult(result) {
5328
5661
  }
5329
5662
  }
5330
5663
  async function cmdStop() {
5331
- const pids = getAllMihomoPids();
5664
+ if (isDaemonEnabled()) {
5665
+ console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u4EE3\u7406\u7531 launchd \u6258\u7BA1"));
5666
+ console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
5667
+ return;
5668
+ }
5669
+ const pids = getMihomoPids();
5332
5670
  if (pids.length === 0) {
5333
5671
  console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
5334
5672
  return;
@@ -5340,8 +5678,8 @@ async function cmdStop() {
5340
5678
 
5341
5679
  // src/test-instance.ts
5342
5680
  import { spawn as spawn2 } from "child_process";
5343
- import fs7 from "fs";
5344
- import path5 from "path";
5681
+ import fs8 from "fs";
5682
+ import path7 from "path";
5345
5683
 
5346
5684
  // src/lifecycle.ts
5347
5685
  var cleanupFns = /* @__PURE__ */ new Set();
@@ -5362,20 +5700,20 @@ function runCleanup() {
5362
5700
  }
5363
5701
 
5364
5702
  // src/test-instance.ts
5365
- var TEST_DIR = path5.join(USER_DATA_DIR, "test");
5703
+ var TEST_DIR = path7.join(USER_DATA_DIR, "test");
5366
5704
  var TEST_DIRS = {
5367
- data: path5.join(TEST_DIR, "data"),
5368
- runtime: path5.join(TEST_DIR, "runtime")
5705
+ data: path7.join(TEST_DIR, "data"),
5706
+ runtime: path7.join(TEST_DIR, "runtime")
5369
5707
  };
5370
5708
  var TEST_PATHS = {
5371
- configFile: path5.join(TEST_DIRS.runtime, "config.yaml"),
5372
- pidFile: path5.join(TEST_DIRS.runtime, "pid"),
5373
- logFile: path5.join(TEST_DIR, "test.log")
5709
+ configFile: path7.join(TEST_DIRS.runtime, "config.yaml"),
5710
+ pidFile: path7.join(TEST_DIRS.runtime, "pid"),
5711
+ logFile: path7.join(TEST_DIR, "test.log")
5374
5712
  };
5375
5713
  var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
5376
5714
  function ensureTestDirs() {
5377
5715
  for (const dir of Object.values(TEST_DIRS)) {
5378
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
5716
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
5379
5717
  }
5380
5718
  }
5381
5719
  function cleanupTestDir() {
@@ -5412,22 +5750,22 @@ function buildTestConfig(subName) {
5412
5750
  ],
5413
5751
  rules: ["MATCH,PROXY"]
5414
5752
  };
5415
- const content = dump(config, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
5416
- fs7.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
5753
+ const content = dumpYaml(config);
5754
+ fs8.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
5417
5755
  }
5418
5756
  async function startTestInstance() {
5419
5757
  const binary = PATHS.mihomoBinary;
5420
- if (!fs7.existsSync(binary)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
5758
+ if (!fs8.existsSync(binary)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
5421
5759
  stopTestInstance();
5422
- const logFd = fs7.openSync(TEST_PATHS.logFile, "a");
5760
+ const logFd = fs8.openSync(TEST_PATHS.logFile, "a");
5423
5761
  const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
5424
5762
  detached: true,
5425
5763
  stdio: ["ignore", logFd, logFd]
5426
5764
  });
5427
- fs7.closeSync(logFd);
5765
+ fs8.closeSync(logFd);
5428
5766
  child.unref();
5429
5767
  const pid = child.pid;
5430
- fs7.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
5768
+ fs8.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
5431
5769
  const client = createHttpClient({ timeout: 2e3 });
5432
5770
  let ready = false;
5433
5771
  for (let i = 0; i < 60; i++) {
@@ -5443,7 +5781,7 @@ async function startTestInstance() {
5443
5781
  if (!isProcessRunning(pid)) {
5444
5782
  let errorDetail = "";
5445
5783
  try {
5446
- errorDetail = fs7.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
5784
+ errorDetail = fs8.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
5447
5785
  } catch {
5448
5786
  }
5449
5787
  throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
@@ -5456,7 +5794,7 @@ ${errorDetail}` : ""}`);
5456
5794
  function stopTestInstance() {
5457
5795
  let pid;
5458
5796
  try {
5459
- pid = parseInt(fs7.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
5797
+ pid = parseInt(fs8.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
5460
5798
  } catch {
5461
5799
  return;
5462
5800
  }
@@ -5468,7 +5806,7 @@ function stopTestInstance() {
5468
5806
  }
5469
5807
  }
5470
5808
  try {
5471
- fs7.unlinkSync(TEST_PATHS.pidFile);
5809
+ fs8.unlinkSync(TEST_PATHS.pidFile);
5472
5810
  } catch {
5473
5811
  }
5474
5812
  }
@@ -5582,6 +5920,12 @@ function resolveTestTarget(args) {
5582
5920
  }
5583
5921
  return { target, timeout, concurrency };
5584
5922
  }
5923
+ function printRestartHintIfRunning() {
5924
+ if (getRunningState().running) {
5925
+ console.log(colors.yellow("\u63D0\u793A: \u8FD0\u884C\u4E2D\u7684\u5B9E\u4F8B\u4ECD\u4F7F\u7528\u65E7\u914D\u7F6E\uFF0C\u6267\u884C mihomo start \u4F7F\u66F4\u65B0\u751F\u6548"));
5926
+ console.log("");
5927
+ }
5928
+ }
5585
5929
  async function printSubscriptionList(options) {
5586
5930
  if (options?.autoUpdate !== false) {
5587
5931
  const updateResult = await autoUpdateStaleSubscription();
@@ -5705,6 +6049,7 @@ async function cmdSubscription(args) {
5705
6049
  }
5706
6050
  if (ok === 0) process.exit(1);
5707
6051
  console.log("");
6052
+ printRestartHintIfRunning();
5708
6053
  await printSubscriptionList();
5709
6054
  return;
5710
6055
  }
@@ -5718,6 +6063,7 @@ async function cmdSubscription(args) {
5718
6063
  }
5719
6064
  console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(result)})`);
5720
6065
  console.log("");
6066
+ printRestartHintIfRunning();
5721
6067
  await printSubscriptionList();
5722
6068
  return;
5723
6069
  }
@@ -5742,9 +6088,8 @@ async function cmdSubscription(args) {
5742
6088
  await printSubscriptionList();
5743
6089
  return;
5744
6090
  }
5745
- const status = getStatus();
5746
- const configInfo = getConfigInfo();
5747
- const currentMode = configInfo?.tun ? "tun" : "mixed";
6091
+ const currentMode = getRuntimeMode();
6092
+ const restartNeeded = isRestartNeededOnChange();
5748
6093
  const success = setDefaultSubscription(target.name);
5749
6094
  if (success) {
5750
6095
  console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
@@ -5752,7 +6097,7 @@ async function cmdSubscription(args) {
5752
6097
  console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
5753
6098
  process.exit(1);
5754
6099
  }
5755
- if (status.running) {
6100
+ if (restartNeeded) {
5756
6101
  console.log("");
5757
6102
  await cmdStart(["start", currentMode]);
5758
6103
  return;
@@ -5846,8 +6191,7 @@ async function cmdSubscription(args) {
5846
6191
  console.log(colors.yellow("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548"));
5847
6192
  } else if (result.removedProxies > 0) {
5848
6193
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
5849
- const status = getStatus();
5850
- if (status.running) {
6194
+ if (getRunningState().running) {
5851
6195
  console.log("");
5852
6196
  console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
5853
6197
  }
@@ -5884,6 +6228,12 @@ async function cmdStart(args) {
5884
6228
  process.exit(1);
5885
6229
  }
5886
6230
  const targetMode = args[1] === "tun" ? "tun" : "mixed";
6231
+ const daemonEnabled = isDaemonEnabled();
6232
+ if (targetMode === "tun" && daemonEnabled) {
6233
+ console.error(`${colors.red("\u9519\u8BEF:")} \u4FDD\u6D3B\u5DF2\u542F\u7528\uFF08\u4EC5\u652F\u6301 Mixed \u6A21\u5F0F\uFF09\uFF0C\u65E0\u6CD5\u542F\u52A8 TUN`);
6234
+ console.error("\u8BF7\u5148\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
6235
+ process.exit(1);
6236
+ }
5887
6237
  const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
5888
6238
  const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
5889
6239
  const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
@@ -5897,16 +6247,18 @@ async function cmdStart(args) {
5897
6247
  if (!skipUpdate) {
5898
6248
  await autoUpdateStaleSubscription({ timeout: updateTimeout });
5899
6249
  }
5900
- const status = getStatus();
5901
- const hasProcess = status.running || status.allProcesses.length > 0;
5902
- if (hasProcess) {
5903
- const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
5904
- console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
5905
- }
5906
- handleStopResult(stop());
5907
- if (hasProcess) {
5908
- console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
6250
+ if (!daemonEnabled) {
6251
+ const status = getStatus();
6252
+ const hasProcess = status.running || status.allProcesses.length > 0;
6253
+ if (hasProcess) {
6254
+ const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
6255
+ console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
6256
+ }
6257
+ handleStopResult(stop());
6258
+ if (hasProcess) {
6259
+ console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
5909
6260
  `);
6261
+ }
5910
6262
  }
5911
6263
  let configInfo;
5912
6264
  try {
@@ -5918,8 +6270,9 @@ async function cmdStart(args) {
5918
6270
  const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
5919
6271
  console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
5920
6272
  try {
5921
- const result = await start(targetMode);
5922
- console.log(`${colors.green("\u5DF2\u542F\u52A8")} (PID ${result.pid})`);
6273
+ const pid = await launchOrRestart(targetMode);
6274
+ const label = daemonEnabled ? "\u5DF2\u542F\u52A8 (\u4FDD\u6D3B)" : "\u5DF2\u542F\u52A8";
6275
+ console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""}`);
5923
6276
  } catch (e) {
5924
6277
  const msg = e.message;
5925
6278
  const lines = msg.split("\n");
@@ -5951,11 +6304,11 @@ async function cmdStart(args) {
5951
6304
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
5952
6305
  console.log("");
5953
6306
  console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
5954
- handleStopResult(stop());
6307
+ if (!daemonEnabled) handleStopResult(stop());
5955
6308
  try {
5956
6309
  configInfo = prepareConfigForStart(targetMode, sub.name);
5957
- const result = await start(targetMode);
5958
- console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${result.pid}) \xB7 ${formatProxySummary(configInfo)}`);
6310
+ const pid = await launchOrRestart(targetMode);
6311
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
5959
6312
  } catch (e) {
5960
6313
  console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
5961
6314
  process.exit(1);
@@ -5975,8 +6328,8 @@ function printOverwriteList() {
5975
6328
  if (info.files.length === 0) {
5976
6329
  console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
5977
6330
  console.log("");
5978
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path6.join(info.dir, "overwrite.yaml")}`);
5979
- console.log(` \u6216 ${path6.join(info.dir, "overwrite.dns.yaml")}`);
6331
+ console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path8.join(info.dir, "overwrite.yaml")}`);
6332
+ console.log(` \u6216 ${path8.join(info.dir, "overwrite.dns.yaml")}`);
5980
6333
  console.log("");
5981
6334
  } else {
5982
6335
  console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
@@ -5996,9 +6349,8 @@ function printOverwriteList() {
5996
6349
  }
5997
6350
  async function cmdOverwrite(args) {
5998
6351
  const action = args?.[1];
5999
- const status = getStatus();
6000
- const configInfo = getConfigInfo();
6001
- const currentMode = configInfo?.tun ? "tun" : "mixed";
6352
+ const currentMode = getRuntimeMode();
6353
+ const restartNeeded = isRestartNeededOnChange();
6002
6354
  if (action === "on" || action === "enable") {
6003
6355
  if (isOverwriteEnabled()) {
6004
6356
  console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u542F\u7528\u72B6\u6001");
@@ -6008,7 +6360,7 @@ async function cmdOverwrite(args) {
6008
6360
  }
6009
6361
  setOverwriteEnabled(true);
6010
6362
  console.log("\u5DF2\u542F\u7528\u8986\u5199\u914D\u7F6E");
6011
- if (status.running) {
6363
+ if (restartNeeded) {
6012
6364
  console.log("");
6013
6365
  await cmdStart(["start", currentMode]);
6014
6366
  return;
@@ -6026,7 +6378,7 @@ async function cmdOverwrite(args) {
6026
6378
  }
6027
6379
  setOverwriteEnabled(false);
6028
6380
  console.log("\u5DF2\u7981\u7528\u8986\u5199\u914D\u7F6E");
6029
- if (status.running) {
6381
+ if (restartNeeded) {
6030
6382
  console.log("");
6031
6383
  await cmdStart(["start", currentMode]);
6032
6384
  return;
@@ -6040,7 +6392,7 @@ async function cmdOverwrite(args) {
6040
6392
  }
6041
6393
 
6042
6394
  // src/commands/reset.ts
6043
- import fs8 from "fs";
6395
+ import fs9 from "fs";
6044
6396
  import readline from "readline";
6045
6397
  var RESET_TARGETS = [
6046
6398
  {
@@ -6095,10 +6447,23 @@ var RESET_TARGETS = [
6095
6447
  label: "\u8986\u5199",
6096
6448
  paths: () => {
6097
6449
  const dir = USER_DATA_DIR;
6098
- if (!fs8.existsSync(dir)) return [];
6099
- return fs8.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
6450
+ if (!fs9.existsSync(dir)) return [];
6451
+ return fs9.readdirSync(dir).filter(isOverwriteFilename).map((f) => `${dir}/${f}`);
6100
6452
  },
6101
6453
  needsStop: false
6454
+ },
6455
+ {
6456
+ id: "daemon",
6457
+ aliases: ["daemon"],
6458
+ label: "\u4FDD\u6D3B",
6459
+ // 卸载由确认后的 disablesDaemon 段统一处理(需 sudo,受取消保护);
6460
+ // 此处 paths 返回空(plist 在系统目录,用户态删不掉,且不应提前删破坏卸载),
6461
+ // onAfter 因幂等守卫(plist 已删)成为 no-op,仅作单独 reset 未走前段时的兜底。
6462
+ paths: () => [],
6463
+ needsStop: false,
6464
+ onAfter: () => disableDaemon(),
6465
+ checkEmpty: () => !isDaemonEnabled(),
6466
+ emptyMsg: "\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u9700\u5220\u9664"
6102
6467
  }
6103
6468
  ];
6104
6469
  function resolveResetTargets(names) {
@@ -6148,7 +6513,7 @@ async function cmdReset(args) {
6148
6513
  }
6149
6514
  targets = matched;
6150
6515
  } else {
6151
- targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites"].includes(t.id));
6516
+ targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
6152
6517
  }
6153
6518
  for (const t of targets) {
6154
6519
  if (t.checkEmpty?.()) {
@@ -6160,25 +6525,40 @@ async function cmdReset(args) {
6160
6525
  }
6161
6526
  const needsStop = targets.some((t) => t.needsStop);
6162
6527
  const warnRunning = targets.some((t) => t.warnIfRunning);
6163
- const pids = needsStop || warnRunning ? getAllMihomoPids() : [];
6164
- if (needsStop && pids.length > 0) {
6165
- console.log(`\u505C\u6B62 ${pids.length} \u4E2A\u8FDB\u7A0B...`);
6166
- cleanupAll();
6167
- for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6168
- if (getAllMihomoPids().length === 0) break;
6169
- await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6170
- }
6171
- } else if (warnRunning && pids.length > 0) {
6528
+ const kernelTargeted = targets.some((t) => t.id === "kernel");
6529
+ const daemonTargeted = targets.some((t) => t.id === "daemon");
6530
+ const disablesDaemon = needsStop || kernelTargeted || daemonTargeted;
6531
+ const pids = needsStop || warnRunning ? getMihomoPids() : [];
6532
+ if (warnRunning && pids.length > 0) {
6172
6533
  console.log(colors.yellow(`\u8B66\u544A: mihomo \u6B63\u5728\u8FD0\u884C (PID ${pids.join(", ")})\uFF0C\u5220\u9664\u5185\u6838\u540E\u5C06\u65E0\u6CD5\u91CD\u65B0\u542F\u52A8`));
6173
6534
  }
6535
+ if (disablesDaemon && isDaemonEnabled()) {
6536
+ console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u91CD\u7F6E\u5C06\u4E00\u5E76\u5173\u95ED\u4FDD\u6D3B\uFF08\u79FB\u9664\u5F00\u673A\u81EA\u542F\uFF09"));
6537
+ }
6174
6538
  console.log(`\u5C06\u5220\u9664: ${targets.map((t) => t.label).join("\u3001")}`);
6175
6539
  if (!skipConfirm && !await confirmPrompt("\u786E\u8BA4?")) {
6176
6540
  console.log("\u5DF2\u53D6\u6D88");
6177
6541
  return;
6178
6542
  }
6543
+ if (disablesDaemon && isDaemonEnabled()) {
6544
+ try {
6545
+ disableDaemon();
6546
+ } catch (e) {
6547
+ console.error(`${colors.red("\u4FDD\u6D3B\u5173\u95ED\u5DF2\u53D6\u6D88\uFF0C\u91CD\u7F6E\u4E2D\u6B62:")} ${e.message.split("\n")[0]}`);
6548
+ return;
6549
+ }
6550
+ }
6551
+ if (needsStop && getMihomoPids().length > 0) {
6552
+ console.log("\u505C\u6B62\u8FDB\u7A0B...");
6553
+ cleanupAll();
6554
+ for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6555
+ if (getMihomoPids().length === 0) break;
6556
+ await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6557
+ }
6558
+ }
6179
6559
  for (const t of targets) {
6180
6560
  for (const p of t.paths()) {
6181
- if (fs8.existsSync(p)) {
6561
+ if (fs9.existsSync(p)) {
6182
6562
  try {
6183
6563
  rmrf(p);
6184
6564
  } catch (e) {
@@ -6197,9 +6577,10 @@ async function cmdReset(args) {
6197
6577
 
6198
6578
  // src/commands/test.ts
6199
6579
  function requireRunning() {
6200
- const status = getStatus();
6201
- if (!status.running) {
6202
- console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
6580
+ const state = getRunningState();
6581
+ if (!state.running) {
6582
+ const hint = state.daemon ? "mihomo daemon on" : "mihomo start";
6583
+ console.error(`\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (${hint})`);
6203
6584
  process.exit(1);
6204
6585
  }
6205
6586
  }
@@ -6256,11 +6637,18 @@ async function cmdClean(args) {
6256
6637
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6257
6638
  console.log("");
6258
6639
  console.log("\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548...");
6259
- const currentMode = getConfigInfo()?.tun ? "tun" : "mixed";
6260
- handleStopResult(stop());
6261
- const configInfo = prepareConfigForStart(currentMode, activeSub.name);
6262
- const startResult = await start(currentMode);
6263
- console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${startResult.pid}) \xB7 ${formatProxySummary(configInfo)}`);
6640
+ const mode = getRuntimeMode();
6641
+ const daemonManaged = isDaemonEnabled();
6642
+ try {
6643
+ if (!daemonManaged) handleStopResult(stop());
6644
+ const configInfo = prepareConfigForStart(mode, activeSub.name);
6645
+ const pid = await launchOrRestart(mode);
6646
+ const label = daemonManaged ? "\u5DF2\u91CD\u542F (\u4FDD\u6D3B)" : "\u5DF2\u91CD\u542F";
6647
+ console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
6648
+ } catch (e) {
6649
+ console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6650
+ process.exit(1);
6651
+ }
6264
6652
  }
6265
6653
  }
6266
6654
 
@@ -6320,6 +6708,199 @@ async function cmdUpdate() {
6320
6708
  }
6321
6709
  }
6322
6710
 
6711
+ // src/commands/registry.ts
6712
+ var COMMANDS = [
6713
+ // === 控制 ===
6714
+ {
6715
+ name: "start",
6716
+ aliases: ["up"],
6717
+ handler: cmdStart,
6718
+ group: "control",
6719
+ usage: ["start [tun|mixed] [-s] [-u ms] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)", " [-r N] [-t ms] [-j N]"]
6720
+ },
6721
+ {
6722
+ name: "tun",
6723
+ aliases: [],
6724
+ handler: cmdStart,
6725
+ rewrite: (args) => ["start", "tun", ...args.slice(1)],
6726
+ group: "control",
6727
+ usage: []
6728
+ },
6729
+ {
6730
+ name: "stop",
6731
+ aliases: ["down"],
6732
+ handler: cmdStop,
6733
+ group: "control",
6734
+ usage: ["stop \u505C\u6B62\u4EE3\u7406"]
6735
+ },
6736
+ {
6737
+ name: "status",
6738
+ aliases: [],
6739
+ handler: printStatus,
6740
+ group: "control",
6741
+ usage: ["status \u67E5\u770B\u72B6\u6001"]
6742
+ },
6743
+ // === 界面 ===
6744
+ {
6745
+ name: "ui",
6746
+ aliases: [],
6747
+ handler: cmdUI,
6748
+ group: "interface",
6749
+ usage: ["ui [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)"]
6750
+ },
6751
+ {
6752
+ name: "log",
6753
+ aliases: [],
6754
+ handler: cmdLog,
6755
+ group: "interface",
6756
+ usage: ["log [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09"]
6757
+ },
6758
+ {
6759
+ name: "logs",
6760
+ aliases: [],
6761
+ handler: cmdLogs,
6762
+ group: "interface",
6763
+ usage: ["logs [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09"]
6764
+ },
6765
+ // === 订阅 ===
6766
+ {
6767
+ name: "subscription",
6768
+ aliases: ["sub", "subscriptions"],
6769
+ handler: cmdSubscription,
6770
+ group: "subscription",
6771
+ usage: [
6772
+ "subscription \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09",
6773
+ "subscription use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605",
6774
+ "subscription add <url> [name] \u6DFB\u52A0\u8BA2\u9605",
6775
+ "subscription update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09",
6776
+ "subscription remove <name> \u5220\u9664\u8BA2\u9605",
6777
+ "subscription web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762",
6778
+ "subscription test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027",
6779
+ "subscription clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9"
6780
+ ]
6781
+ },
6782
+ {
6783
+ name: "use",
6784
+ aliases: [],
6785
+ handler: cmdSubscription,
6786
+ rewrite: (args) => ["sub", "use", ...args.slice(1)],
6787
+ group: "subscription",
6788
+ usage: []
6789
+ },
6790
+ {
6791
+ name: "test",
6792
+ aliases: [],
6793
+ handler: cmdTest,
6794
+ group: "subscription",
6795
+ usage: ["test [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027"]
6796
+ },
6797
+ {
6798
+ name: "clean",
6799
+ aliases: [],
6800
+ handler: cmdClean,
6801
+ group: "subscription",
6802
+ usage: ["clean [-t ms] [-j N] [-r N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F"]
6803
+ },
6804
+ // === 配置 ===
6805
+ {
6806
+ name: "overwrite",
6807
+ aliases: ["ow"],
6808
+ handler: cmdOverwrite,
6809
+ group: "config",
6810
+ usage: ["overwrite \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09", "overwrite on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E"]
6811
+ },
6812
+ {
6813
+ name: "on",
6814
+ aliases: [],
6815
+ handler: cmdOverwrite,
6816
+ rewrite: () => ["ow", "on"],
6817
+ group: "config",
6818
+ usage: []
6819
+ },
6820
+ {
6821
+ name: "off",
6822
+ aliases: [],
6823
+ handler: cmdOverwrite,
6824
+ rewrite: () => ["ow", "off"],
6825
+ group: "config",
6826
+ usage: []
6827
+ },
6828
+ {
6829
+ name: "directory",
6830
+ aliases: ["dir", "dirs", "directories"],
6831
+ handler: cmdDirectory,
6832
+ group: "config",
6833
+ usage: ["directory \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09", "directory open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|..."]
6834
+ },
6835
+ {
6836
+ name: "open",
6837
+ aliases: [],
6838
+ handler: cmdDirectory,
6839
+ rewrite: (args) => ["dir", "open", ...args.slice(1)],
6840
+ group: "config",
6841
+ usage: []
6842
+ },
6843
+ // === 系统 ===
6844
+ {
6845
+ name: "kernel",
6846
+ aliases: [],
6847
+ handler: cmdKernel,
6848
+ group: "system",
6849
+ usage: ["kernel [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09"]
6850
+ },
6851
+ {
6852
+ name: "daemon",
6853
+ aliases: [],
6854
+ handler: cmdDaemon,
6855
+ group: "system",
6856
+ usage: ["daemon on|off \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF08\u4EC5 Mixed\uFF0C\u9700\u7BA1\u7406\u5458\u5BC6\u7801\uFF09", "daemon status \u67E5\u770B\u4FDD\u6D3B\u72B6\u6001"]
6857
+ },
6858
+ {
6859
+ name: "update",
6860
+ aliases: ["upd", "upgrade"],
6861
+ handler: cmdUpdate,
6862
+ group: "system",
6863
+ usage: ["update \u66F4\u65B0 mihomo-cli (npm install -g)"]
6864
+ },
6865
+ {
6866
+ name: "reset",
6867
+ aliases: [],
6868
+ handler: cmdReset,
6869
+ group: "system",
6870
+ usage: ["reset [\u76EE\u6807...] [--full] \u91CD\u7F6E: \u7559\u7A7A\u4FDD\u7559\u8BBE\u7F6E/\u5185\u6838/\u8986\u5199, \u6307\u5B9A\u76EE\u6807\u5220\u5BF9\u5E94\u9879, --full \u5220\u5168\u90E8"]
6871
+ },
6872
+ // === meta(不在分组清单展示,help 末尾单列) ===
6873
+ {
6874
+ name: "help",
6875
+ aliases: ["-h", "--help"],
6876
+ handler: () => printHelp(COMMANDS),
6877
+ group: "meta",
6878
+ usage: ["help, -h \u663E\u793A\u5E2E\u52A9"]
6879
+ },
6880
+ {
6881
+ name: "version",
6882
+ aliases: ["-v", "--version"],
6883
+ handler: printVersion,
6884
+ group: "meta",
6885
+ usage: ["version, -v \u663E\u793A\u7248\u672C"]
6886
+ }
6887
+ ];
6888
+ var COMMAND_INDEX = (() => {
6889
+ const index = /* @__PURE__ */ new Map();
6890
+ for (const cmd of COMMANDS) {
6891
+ for (const token of [cmd.name, ...cmd.aliases]) {
6892
+ if (index.has(token)) {
6893
+ throw new Error(`\u547D\u4EE4\u6CE8\u518C\u8868\u5B58\u5728\u91CD\u590D token: "${token}"\uFF08${index.get(token)?.name} \u4E0E ${cmd.name}\uFF09`);
6894
+ }
6895
+ index.set(token, cmd);
6896
+ }
6897
+ }
6898
+ return index;
6899
+ })();
6900
+ function findCommand(token) {
6901
+ return COMMAND_INDEX.get(token);
6902
+ }
6903
+
6323
6904
  // src/index.ts
6324
6905
  process.on("SIGINT", () => {
6325
6906
  console.log("\n\u6B63\u5728\u9000\u51FA...");
@@ -6336,12 +6917,14 @@ process.on("uncaughtException", (e) => {
6336
6917
  if (e.stack) {
6337
6918
  console.error(e.stack.split("\n").slice(1).join("\n"));
6338
6919
  }
6920
+ runCleanup();
6339
6921
  process.exit(1);
6340
6922
  });
6341
6923
  process.on("unhandledRejection", (reason) => {
6342
6924
  const msg = reason instanceof Error ? reason.message : String(reason);
6343
6925
  console.error(`
6344
6926
  \u672A\u5904\u7406\u7684 Promise \u62D2\u7EDD: ${msg}`);
6927
+ runCleanup();
6345
6928
  process.exit(1);
6346
6929
  });
6347
6930
  function clearProxyEnv() {
@@ -6361,91 +6944,18 @@ async function main() {
6361
6944
  printShortHelp();
6362
6945
  return;
6363
6946
  }
6364
- const cmd = args[0].toLowerCase();
6365
- if (["help", "-h", "--help"].includes(cmd)) {
6366
- printHelp();
6367
- return;
6368
- }
6369
- if (["version", "-v", "--version"].includes(cmd)) {
6370
- printVersion();
6371
- return;
6372
- }
6373
- switch (cmd) {
6374
- case "up":
6375
- case "start":
6376
- await cmdStart(args);
6377
- break;
6378
- case "tun":
6379
- await cmdStart(["start", "tun", ...args.slice(1)]);
6380
- break;
6381
- case "down":
6382
- case "stop":
6383
- await cmdStop();
6384
- break;
6385
- case "status":
6386
- printStatus();
6387
- break;
6388
- case "log":
6389
- cmdLog(args);
6390
- break;
6391
- case "logs":
6392
- cmdLogs(args);
6393
- break;
6394
- case "open":
6395
- cmdDirectory(["dir", "open", ...args.slice(1)]);
6396
- break;
6397
- case "ui":
6398
- cmdUI(args);
6399
- break;
6400
- case "kernel":
6401
- await cmdKernel(args);
6402
- break;
6403
- case "upd":
6404
- case "update":
6405
- case "upgrade":
6406
- await cmdUpdate();
6407
- break;
6408
- case "use":
6409
- await cmdSubscription(["sub", "use", ...args.slice(1)]);
6410
- break;
6411
- case "sub":
6412
- case "subscription":
6413
- case "subscriptions":
6414
- await cmdSubscription(args);
6415
- break;
6416
- case "dir":
6417
- case "dirs":
6418
- case "directory":
6419
- case "directories":
6420
- cmdDirectory(args);
6421
- break;
6422
- case "reset":
6423
- await cmdReset(args);
6424
- break;
6425
- case "on":
6426
- await cmdOverwrite(["ow", "on"]);
6427
- break;
6428
- case "off":
6429
- await cmdOverwrite(["ow", "off"]);
6430
- break;
6431
- case "ow":
6432
- case "overwrite":
6433
- await cmdOverwrite(args);
6434
- break;
6435
- case "test":
6436
- await cmdTest(args);
6437
- break;
6438
- case "clean":
6439
- await cmdClean(args);
6440
- break;
6441
- default:
6442
- console.error(`\u672A\u77E5\u547D\u4EE4: ${cmd}`);
6443
- console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');
6444
- process.exit(1);
6947
+ const token = args[0].toLowerCase();
6948
+ const command = findCommand(token);
6949
+ if (!command) {
6950
+ console.error(`\u672A\u77E5\u547D\u4EE4: ${token}`);
6951
+ console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');
6952
+ process.exit(1);
6445
6953
  }
6954
+ await command.handler(command.rewrite ? command.rewrite(args) : args);
6446
6955
  }
6447
6956
  main().catch((e) => {
6448
6957
  console.error(`\u9519\u8BEF: ${e.message}`);
6958
+ runCleanup();
6449
6959
  process.exit(1);
6450
6960
  });
6451
6961
  /*! Bundled license information: