mihomo-cli 3.0.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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config.ts
4
- import { spawnSync } from "child_process";
4
+ import { spawnSync as spawnSync2 } from "child_process";
5
5
  import fs4 from "fs";
6
6
 
7
7
  // node_modules/js-yaml/dist/js-yaml.mjs
@@ -2923,7 +2923,11 @@ var UI_URLS = {
2923
2923
  dash: "https://metacubex.github.io/metacubexd",
2924
2924
  yacd: "https://yacd.metacubex.one"
2925
2925
  };
2926
- var LAUNCH_AGENT_LABEL = "com.mihomo-cli.daemon";
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";
2927
2931
  var TUN_CONFIG = {
2928
2932
  tun: {
2929
2933
  enable: true,
@@ -2937,14 +2941,14 @@ var TUN_CONFIG = {
2937
2941
  var TEST_CONFIG = {
2938
2942
  "mixed-port": 27890,
2939
2943
  "allow-lan": false,
2940
- "external-controller": "127.0.0.1:29090",
2944
+ "external-controller": TEST_CONTROLLER_ADDR,
2941
2945
  "log-level": "error",
2942
2946
  "geodata-mode": true
2943
2947
  };
2944
2948
  var BASE_CONFIG = {
2945
2949
  "mixed-port": 7890,
2946
2950
  "allow-lan": false,
2947
- "external-controller": "127.0.0.1:9090",
2951
+ "external-controller": CONTROLLER_ADDR,
2948
2952
  "unified-delay": true,
2949
2953
  "tcp-concurrent": true,
2950
2954
  "geo-auto-update": true,
@@ -2973,7 +2977,7 @@ var AUTO_CLEAN_THRESHOLD_GITHUB = 50;
2973
2977
 
2974
2978
  // src/overwrite.ts
2975
2979
  import fs3 from "fs";
2976
- import path2 from "path";
2980
+ import path3 from "path";
2977
2981
 
2978
2982
  // src/paths.ts
2979
2983
  import fs from "fs";
@@ -3003,8 +3007,8 @@ var PATHS = {
3003
3007
  configStage1Subscription: path.join(DIRS.runtime, "1.subscription.yaml"),
3004
3008
  configStage2Overwrite: path.join(DIRS.runtime, "2.overwrite.yaml"),
3005
3009
  configStage3System: path.join(DIRS.runtime, "3.system.yaml"),
3006
- // launchd LaunchAgent plist 必须位于真实用户主目录,不受 MIHOMO_CLI_DIR 影响
3007
- launchAgentPlist: path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`)
3010
+ // launchd LaunchDaemon plist 位于系统级 /Library/LaunchDaemons/,root:wheel 拥有,与 homedir / MIHOMO_CLI_DIR 无关
3011
+ launchDaemonPlist: path.join("/Library/LaunchDaemons", `${LAUNCH_DAEMON_LABEL}.plist`)
3008
3012
  };
3009
3013
  var DIRECTORY_TARGETS = {
3010
3014
  root: { path: null, label: "\u6839\u76EE\u5F55" },
@@ -3040,6 +3044,7 @@ function rmrf(dir) {
3040
3044
 
3041
3045
  // src/settings.ts
3042
3046
  import fs2 from "fs";
3047
+ import path2 from "path";
3043
3048
  var settingsCache = null;
3044
3049
  function readSettings() {
3045
3050
  if (settingsCache !== null) return settingsCache;
@@ -3143,12 +3148,10 @@ function addSubscription(url, name = "default") {
3143
3148
  validateSubscriptionName(name);
3144
3149
  const settings = readSettings();
3145
3150
  const subs = [...settings.subscriptions || []];
3146
- const existingIndex = subs.findIndex((s) => s.name === name);
3147
- if (existingIndex >= 0) {
3148
- subs[existingIndex] = { name, url };
3149
- } else {
3150
- 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`);
3151
3153
  }
3154
+ subs.push({ name, url });
3152
3155
  const updates = { subscriptions: subs };
3153
3156
  if (!settings.active_subscription && subs.length === 1) {
3154
3157
  updates.active_subscription = name;
@@ -3173,7 +3176,10 @@ function removeSubscription(name) {
3173
3176
  delete cache[name];
3174
3177
  writeSubscriptionCache(cache);
3175
3178
  }
3176
- fs2.rmSync(getSubscriptionRawConfigPath(name), { force: true });
3179
+ try {
3180
+ fs2.rmSync(getSubscriptionRawConfigPath(name), { force: true });
3181
+ } catch {
3182
+ }
3177
3183
  return switchedTo;
3178
3184
  }
3179
3185
  function setDefaultSubscription(name) {
@@ -3186,7 +3192,10 @@ function setDefaultSubscription(name) {
3186
3192
  return true;
3187
3193
  }
3188
3194
  function getSubscriptionRawConfigPath(subName) {
3189
- 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`);
3190
3199
  }
3191
3200
  function saveSubscriptionRawConfig(subName, content) {
3192
3201
  ensureDirs();
@@ -3284,17 +3293,20 @@ function isOverwriteEnabled() {
3284
3293
  function setOverwriteEnabled(enabled) {
3285
3294
  writeSettings({ overwrite_enabled: enabled });
3286
3295
  }
3296
+ function isOverwriteFilename(filename) {
3297
+ return filename === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(filename);
3298
+ }
3287
3299
  function loadOverwriteFile() {
3288
3300
  const dir = USER_DATA_DIR;
3289
3301
  if (!fs3.existsSync(dir)) return [];
3290
- 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) => {
3291
3303
  if (a === "overwrite.yaml") return -1;
3292
3304
  if (b === "overwrite.yaml") return 1;
3293
3305
  return a.localeCompare(b);
3294
3306
  });
3295
3307
  const results = [];
3296
3308
  for (const file of files) {
3297
- const filePath = path2.join(dir, file);
3309
+ const filePath = path3.join(dir, file);
3298
3310
  try {
3299
3311
  const content = fs3.readFileSync(filePath, "utf8");
3300
3312
  const parsed = load(content);
@@ -3331,6 +3343,213 @@ function listOverwriteFile() {
3331
3343
  };
3332
3344
  }
3333
3345
 
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";
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";
3411
+ try {
3412
+ return new Date(ts * 1e3).toLocaleString("zh-CN");
3413
+ } catch {
3414
+ return "\u672A\u77E5";
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";
3423
+ try {
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");
3427
+ } catch {
3428
+ return "\u672A\u77E5";
3429
+ }
3430
+ }
3431
+ function hasFlag(args, short, long) {
3432
+ return !!args && (args.includes(short) || args.includes(long));
3433
+ }
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
+ }
3442
+ }
3443
+ }
3444
+ return defaultValue;
3445
+ }
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
+
3334
3553
  // src/config.ts
3335
3554
  function parseYamlOrJson(content, errorMsg) {
3336
3555
  if (!content?.trim()) {
@@ -3347,6 +3566,9 @@ function parseYamlOrJson(content, errorMsg) {
3347
3566
  throw new Error(`${errorMsg || "\u5185\u5BB9"}\u683C\u5F0F\u9519\u8BEF\uFF0C\u65E0\u6CD5\u89E3\u6790\u4E3A YAML \u6216 JSON`);
3348
3567
  }
3349
3568
  }
3569
+ function dumpYaml(obj) {
3570
+ return dump(obj, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
3571
+ }
3350
3572
  function collectOverwriteProxyNames(overwriteFiles) {
3351
3573
  const names = [];
3352
3574
  for (const file of overwriteFiles) {
@@ -3367,7 +3589,7 @@ function excludeOverwriteProxiesFromIncludeAll(config, overwriteFiles) {
3367
3589
  if (injectedNames.length === 0) return;
3368
3590
  const groups = config["proxy-groups"];
3369
3591
  if (!groups) return;
3370
- const excludePattern = injectedNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
3592
+ const excludePattern = injectedNames.map((n) => escapeRegExp(n)).join("|");
3371
3593
  for (const group of groups) {
3372
3594
  if (!group["include-all"] && !group["include-all-proxies"]) continue;
3373
3595
  const existing = group["exclude-filter"];
@@ -3392,6 +3614,15 @@ function deduplicateByName(items) {
3392
3614
  });
3393
3615
  return { result, names, duplicates };
3394
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
+ }
3395
3626
  function validateConfig(config) {
3396
3627
  const warnings = [];
3397
3628
  const proxies = config.proxies || [];
@@ -3436,9 +3667,7 @@ function validateConfig(config) {
3436
3667
  if (rules.length > 0) {
3437
3668
  const removedRules = [];
3438
3669
  config.rules = rules.filter((rule) => {
3439
- const parts = rule.split(",");
3440
- if (parts.length < 2) return true;
3441
- const target = parts[parts.length - 1].trim();
3670
+ const target = getRuleTarget(rule);
3442
3671
  if (!target || validNames.has(target)) return true;
3443
3672
  removedRules.push(rule);
3444
3673
  return false;
@@ -3485,6 +3714,8 @@ function buildConfig(subRawContent, mode) {
3485
3714
  if (Object.keys(dns).length > 0) {
3486
3715
  systemConfig.dns = dns;
3487
3716
  }
3717
+ } else {
3718
+ delete withOverwrites.tun;
3488
3719
  }
3489
3720
  const merged = { ...withOverwrites, ...systemConfig };
3490
3721
  if (systemConfig.dns) {
@@ -3507,20 +3738,19 @@ function buildConfig(subRawContent, mode) {
3507
3738
  }
3508
3739
  function writeMihomoConfig(configObj) {
3509
3740
  ensureDirs();
3510
- const content = dump(configObj, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
3741
+ const content = dumpYaml(configObj);
3511
3742
  atomicWriteFileSync(PATHS.configFile, content, { mode: 384 });
3512
3743
  }
3513
3744
  function writeDebugConfig(buildResult) {
3514
3745
  ensureDirs();
3515
- const dumpOpts = { indent: 2, lineWidth: -1, schema: CORE_SCHEMA };
3516
- fs4.writeFileSync(PATHS.configStage1Subscription, dump(buildResult.subscriptionConfig, dumpOpts), { mode: 384 });
3746
+ fs4.writeFileSync(PATHS.configStage1Subscription, dumpYaml(buildResult.subscriptionConfig), { mode: 384 });
3517
3747
  const overwriteMerged = {};
3518
3748
  for (const f of buildResult.overwriteFiles) {
3519
3749
  Object.assign(overwriteMerged, f.config);
3520
3750
  }
3521
- 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";
3522
3752
  fs4.writeFileSync(PATHS.configStage2Overwrite, overwriteContent, { mode: 384 });
3523
- fs4.writeFileSync(PATHS.configStage3System, dump(buildResult.systemConfig, dumpOpts), { mode: 384 });
3753
+ fs4.writeFileSync(PATHS.configStage3System, dumpYaml(buildResult.systemConfig), { mode: 384 });
3524
3754
  }
3525
3755
  function hasConfig() {
3526
3756
  return fs4.existsSync(PATHS.configFile);
@@ -3560,7 +3790,7 @@ function getKernelVersion() {
3560
3790
  }
3561
3791
  if (kernelVersionCached) return kernelVersionCache;
3562
3792
  try {
3563
- const result = spawnSync(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
3793
+ const result = spawnSync2(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
3564
3794
  const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
3565
3795
  if (output) {
3566
3796
  const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
@@ -3579,214 +3809,89 @@ function clearKernelVersionCache() {
3579
3809
  kernelVersionCached = false;
3580
3810
  }
3581
3811
 
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
+ );
3825
+ }
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"));
3873
+ }
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)}`;
3878
+ }
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}`);
3884
+ }
3885
+
3582
3886
  // src/daemon.ts
3583
3887
  import { spawnSync as spawnSync4 } from "child_process";
3584
3888
  import fs6 from "fs";
3585
- import path4 from "path";
3889
+ import path5 from "path";
3586
3890
 
3587
3891
  // src/process.ts
3588
3892
  import { spawn, spawnSync as spawnSync3 } from "child_process";
3589
3893
  import fs5 from "fs";
3590
- import path3 from "path";
3591
-
3592
- // src/utils.ts
3593
- import { spawnSync as spawnSync2 } from "child_process";
3594
- import { createRequire } from "module";
3595
- var require2 = createRequire(import.meta.url);
3596
- var pkg = require2("../package.json");
3597
- var VERSION = pkg.version;
3598
- var sleepBuf = new Int32Array(new SharedArrayBuffer(4));
3599
- var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
3600
- function colorize(code, str) {
3601
- if (NO_COLOR) return String(str);
3602
- return `${code + String(str)}\x1B[0m`;
3603
- }
3604
- var colors = {
3605
- bold: (s) => colorize("\x1B[1m", s),
3606
- red: (s) => colorize("\x1B[31m", s),
3607
- green: (s) => colorize("\x1B[32m", s),
3608
- yellow: (s) => colorize("\x1B[33m", s),
3609
- cyan: (s) => colorize("\x1B[36m", s),
3610
- gray: (s) => colorize("\x1B[90m", s)
3611
- };
3612
- function sleepSync(ms) {
3613
- Atomics.wait(sleepBuf, 0, 0, ms);
3614
- }
3615
- function sleep(ms) {
3616
- return new Promise((resolve) => setTimeout(resolve, ms));
3617
- }
3618
- var TimeoutError = class extends Error {
3619
- constructor() {
3620
- super("timeout");
3621
- this.name = "TimeoutError";
3622
- }
3623
- };
3624
- function withTimeout(promise, ms) {
3625
- return new Promise((resolve, reject) => {
3626
- const timer = setTimeout(() => reject(new TimeoutError()), ms);
3627
- promise.then(
3628
- (v) => {
3629
- clearTimeout(timer);
3630
- resolve(v);
3631
- },
3632
- (e) => {
3633
- clearTimeout(timer);
3634
- reject(e);
3635
- }
3636
- );
3637
- });
3638
- }
3639
- function formatBytes(bytes) {
3640
- if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
3641
- const num = Number(bytes);
3642
- if (!Number.isFinite(num) || num < 0) return "\u672A\u77E5";
3643
- if (num === 0) return "0 B";
3644
- const k = 1024;
3645
- const sizes = ["B", "KB", "MB", "GB", "TB"];
3646
- const i = Math.min(Math.floor(Math.log(num) / Math.log(k)), sizes.length - 1);
3647
- return `${parseFloat((num / k ** i).toFixed(2))} ${sizes[i]}`;
3648
- }
3649
- function formatTimestamp(ts) {
3650
- if (ts === void 0 || ts === null) return "\u672A\u77E5";
3651
- try {
3652
- return new Date(ts * 1e3).toLocaleString("zh-CN");
3653
- } catch {
3654
- return "\u672A\u77E5";
3655
- }
3656
- }
3657
- function formatDate(dateOrIso) {
3658
- if (dateOrIso === void 0 || dateOrIso === null) return "\u672A\u77E5";
3659
- try {
3660
- const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
3661
- if (Number.isNaN(d.getTime())) return "\u672A\u77E5";
3662
- return d.toLocaleString("zh-CN");
3663
- } catch {
3664
- return "\u672A\u77E5";
3665
- }
3666
- }
3667
- function hasFlag(args, short, long) {
3668
- return !!args && (args.includes(short) || args.includes(long));
3669
- }
3670
- function parseIntArg(args, short, long, defaultValue) {
3671
- if (!args) return defaultValue;
3672
- for (let i = 0; i < args.length; i++) {
3673
- if (args[i] === short || args[i] === long) {
3674
- if (i + 1 < args.length) {
3675
- const val = parseInt(args[i + 1], 10);
3676
- return Number.isNaN(val) ? defaultValue : val;
3677
- }
3678
- }
3679
- }
3680
- return defaultValue;
3681
- }
3682
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["-t", "--timeout", "-j", "--concurrency", "-r", "--rounds", "-n", "--lines", "-u", "--update-timeout"]);
3683
- function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
3684
- if (!args) return null;
3685
- for (let i = startIdx; i < args.length; i++) {
3686
- const a = args[i];
3687
- if (a.startsWith("-")) {
3688
- if (valueFlags.has(a)) i++;
3689
- continue;
3690
- }
3691
- return a;
3692
- }
3693
- return null;
3694
- }
3695
- function isProcessRunning(pid) {
3696
- if (!pid) return false;
3697
- try {
3698
- const result = spawnSync2("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: 5e3 });
3699
- return (result.stdout || "").trim().length > 0;
3700
- } catch {
3701
- return false;
3702
- }
3703
- }
3704
- function isProcessRoot(pid) {
3705
- if (!pid) return false;
3706
- try {
3707
- const result = spawnSync2("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: 5e3 });
3708
- return (result.stdout || "").trim() === "0";
3709
- } catch {
3710
- return false;
3711
- }
3712
- }
3713
- function createHttpClient(options = {}) {
3714
- const { timeout = 6e4 } = options;
3715
- return {
3716
- async get(url, config) {
3717
- const controller = new AbortController();
3718
- const timer = setTimeout(() => controller.abort(), timeout);
3719
- const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
3720
- try {
3721
- const response = await fetch(url, {
3722
- signal,
3723
- headers: { "User-Agent": `mihomo-cli/${VERSION}` }
3724
- });
3725
- if (!response.ok) {
3726
- const error = new Error(`HTTP ${response.status}`);
3727
- error.response = { status: response.status };
3728
- try {
3729
- error.response.data = await response.json();
3730
- } catch {
3731
- }
3732
- throw error;
3733
- }
3734
- const data = config?.responseType === "json" ? await response.json() : await response.text();
3735
- return { data, headers: response.headers, status: response.status };
3736
- } finally {
3737
- clearTimeout(timer);
3738
- }
3739
- }
3740
- };
3741
- }
3742
- function normalizeMirrorUrl(val) {
3743
- if (!val) return null;
3744
- if (val === "direct" || val === "no" || val === "none") return null;
3745
- let url = val;
3746
- if (!url.startsWith("http")) {
3747
- url = `https://${url}`;
3748
- }
3749
- if (!url.endsWith("/")) {
3750
- url += "/";
3751
- }
3752
- return url;
3753
- }
3754
- function parseMirrorArg(args) {
3755
- if (!args || args.length < 2) {
3756
- return { mirror: null, isOverride: false, type: "download" };
3757
- }
3758
- if (args.includes("--no-mirror") || args.includes("--direct")) {
3759
- return { mirror: null, isOverride: true, type: "download" };
3760
- }
3761
- const mirrorAllIdx = args.indexOf("--mirror-all");
3762
- if (mirrorAllIdx >= 0) {
3763
- const nextArg = args[mirrorAllIdx + 1];
3764
- if (!nextArg || nextArg.startsWith("-")) {
3765
- return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "all" };
3766
- }
3767
- return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "all" };
3768
- }
3769
- const mirrorIdx = args.indexOf("--mirror");
3770
- if (mirrorIdx >= 0) {
3771
- const nextArg = args[mirrorIdx + 1];
3772
- if (!nextArg || nextArg.startsWith("-")) {
3773
- return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "download" };
3774
- }
3775
- return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "download" };
3776
- }
3777
- return { mirror: null, isOverride: false, type: "download" };
3778
- }
3779
- function isProxyValid(proxy) {
3780
- if (!proxy.name || !proxy.server || !proxy.port) return false;
3781
- if (!proxy.type) return false;
3782
- if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3783
- const pw = String(proxy.password || "");
3784
- if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
3785
- }
3786
- return true;
3787
- }
3788
-
3789
- // src/process.ts
3894
+ import path4 from "path";
3790
3895
  var PROCESS_WAIT_ATTEMPTS = 50;
3791
3896
  var PROCESS_WAIT_INTERVAL = 100;
3792
3897
  var STARTUP_WAIT_MS = 800;
@@ -3794,9 +3899,7 @@ var SUDO_TIMEOUT_MS = 6e4;
3794
3899
  var TUN_MODE_POST_WAIT_MS = 500;
3795
3900
  var BATCH_KILL_THRESHOLD = 3;
3796
3901
  var DEFAULT_LOG_RETENTION_DAYS = 7;
3797
- function escapeForPgrep(s) {
3798
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3799
- }
3902
+ var MAIN_INSTANCE_PATTERN = `${escapeRegExp(PATHS.mihomoBinary)}.*${escapeRegExp(PATHS.configFile)}`;
3800
3903
  function clearRuntime() {
3801
3904
  if (fs5.existsSync(DIRS.runtime)) {
3802
3905
  rmrf(DIRS.runtime);
@@ -3816,10 +3919,9 @@ function isRunning() {
3816
3919
  const pid = getPid();
3817
3920
  return pid ? isProcessRunning(pid) : false;
3818
3921
  }
3819
- function getAllMihomoPids() {
3820
- const binaryPath = PATHS.mihomoBinary;
3922
+ function getMihomoPids() {
3821
3923
  try {
3822
- const result = spawnSync3("pgrep", ["-f", escapeForPgrep(binaryPath)], { encoding: "utf8", timeout: 1e4 });
3924
+ const result = spawnSync3("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
3823
3925
  const output = (result.stdout || "").trim();
3824
3926
  if (!output) return [];
3825
3927
  return output.split("\n").filter(Boolean).map((p) => parseInt(p, 10)).filter((p) => Number.isInteger(p) && p > 0);
@@ -3837,7 +3939,7 @@ function isPidFileOwnedByRoot() {
3837
3939
  }
3838
3940
  }
3839
3941
  function checkStaleState() {
3840
- const allPids = getAllMihomoPids();
3942
+ const allPids = getMihomoPids();
3841
3943
  const hasRootProcess = allPids.some((p) => isProcessRoot(p));
3842
3944
  const hasRootPidFile = isPidFileOwnedByRoot();
3843
3945
  return {
@@ -3888,7 +3990,7 @@ function killProcess(pid, needsSudo = false) {
3888
3990
  }
3889
3991
  }
3890
3992
  function killAllMihomo(forceSudo = false) {
3891
- const pattern = escapeForPgrep(PATHS.mihomoBinary);
3993
+ const pattern = MAIN_INSTANCE_PATTERN;
3892
3994
  if (forceSudo) {
3893
3995
  try {
3894
3996
  spawnSync3("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
@@ -3906,7 +4008,7 @@ function killAllMihomo(forceSudo = false) {
3906
4008
  }
3907
4009
  }
3908
4010
  function cleanupAll(forceSudo = false) {
3909
- const pids = getAllMihomoPids();
4011
+ const pids = getMihomoPids();
3910
4012
  if (pids.length === 0) {
3911
4013
  clearPid();
3912
4014
  return { killed: 0, failed: 0, remaining: [] };
@@ -3937,26 +4039,26 @@ function cleanupAll(forceSudo = false) {
3937
4039
  }
3938
4040
  }
3939
4041
  for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
3940
- if (getAllMihomoPids().length === 0) break;
4042
+ if (getMihomoPids().length === 0) break;
3941
4043
  sleepSync(PROCESS_WAIT_INTERVAL);
3942
4044
  }
3943
4045
  clearPid();
3944
- return { killed: killedCount, failed: failedPids.length, remaining: getAllMihomoPids() };
4046
+ return { killed: killedCount, failed: failedPids.length, remaining: getMihomoPids() };
3945
4047
  }
3946
4048
  function createTunLaunchScript() {
3947
- const binary = PATHS.mihomoBinary;
3948
- const configFile = PATHS.configFile;
3949
- const logFile = PATHS.logFile;
3950
- const pidFile = PATHS.pidFile;
3951
- const dataDir = DIRS.data;
3952
- 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);
3953
4055
  const scriptContent = `#!/bin/bash
3954
- BINARY="${binary}"
3955
- CONFIG_FILE="${configFile}"
3956
- LOG_FILE="${logFile}"
3957
- PID_FILE="${pidFile}"
3958
- DATA_DIR="${dataDir}"
3959
- 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}
3960
4062
 
3961
4063
  # \u7EC8\u6B62\u65E7\u8FDB\u7A0B
3962
4064
  pkill -9 -f "\${KILL_PATTERN}" 2>/dev/null || true
@@ -3987,7 +4089,7 @@ echo "--- \u65E5\u5FD7 ---"
3987
4089
  tail -25 "\${LOG_FILE}" 2>/dev/null
3988
4090
  exit 1
3989
4091
  `;
3990
- const scriptPath = path3.join(DIRS.runtime, "launch-tun.sh");
4092
+ const scriptPath = path4.join(DIRS.runtime, "launch-tun.sh");
3991
4093
  fs5.writeFileSync(scriptPath, scriptContent, { mode: 448 });
3992
4094
  return scriptPath;
3993
4095
  }
@@ -4009,7 +4111,7 @@ function getProcessInfo(pid) {
4009
4111
  function getStatus() {
4010
4112
  const running = isRunning();
4011
4113
  const pid = getPid();
4012
- const allPids = getAllMihomoPids();
4114
+ const allPids = getMihomoPids();
4013
4115
  return {
4014
4116
  running,
4015
4117
  pid: running ? pid : null,
@@ -4059,6 +4161,16 @@ async function startMixedMode(staleState) {
4059
4161
  const configFile = PATHS.configFile;
4060
4162
  const logFile = PATHS.logFile;
4061
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
+ }
4062
4174
  const logFd = fs5.openSync(logFile, "a");
4063
4175
  const child = spawn(PATHS.mihomoBinary, args, {
4064
4176
  detached: true,
@@ -4121,14 +4233,14 @@ async function startTunMode(staleState) {
4121
4233
  return { success: true, pid: finalPid, mode: "tun" };
4122
4234
  }
4123
4235
  function stop(forceSudo = false) {
4124
- const allPids = getAllMihomoPids();
4236
+ const allPids = getMihomoPids();
4125
4237
  if (allPids.length === 0) {
4126
4238
  clearPid();
4127
4239
  clearRuntime();
4128
4240
  return { success: true, notRunning: true };
4129
4241
  }
4130
4242
  const result = cleanupAll(forceSudo);
4131
- const remaining = getAllMihomoPids();
4243
+ const remaining = getMihomoPids();
4132
4244
  if (remaining.length > 0) {
4133
4245
  console.log("");
4134
4246
  console.log("\u4ECD\u6709\u8FDB\u7A0B\u6B8B\u7559\uFF0C\u9700\u8981\u624B\u52A8\u6E05\u7406:");
@@ -4152,9 +4264,8 @@ function rotateLog() {
4152
4264
  if (!fs5.existsSync(logFile)) return null;
4153
4265
  const stat = fs5.statSync(logFile);
4154
4266
  if (stat.size === 0) return null;
4155
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/T/, "_").replace(/:/g, "-").replace(/\..+/, "");
4156
- const rotatedName = `mihomo.${timestamp}.log`;
4157
- const rotatedPath = path3.join(DIRS.logs, rotatedName);
4267
+ const rotatedName = `mihomo.${formatLocalTimestamp()}.log`;
4268
+ const rotatedPath = path4.join(DIRS.logs, rotatedName);
4158
4269
  fs5.renameSync(logFile, rotatedPath);
4159
4270
  return rotatedPath;
4160
4271
  }
@@ -4169,7 +4280,7 @@ function cleanupOldLogs(maxAgeDays = DEFAULT_LOG_RETENTION_DAYS) {
4169
4280
  for (const file of files) {
4170
4281
  if (!file.match(/^mihomo\.\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$/)) continue;
4171
4282
  try {
4172
- const filePath = path3.join(logsDir, file);
4283
+ const filePath = path4.join(logsDir, file);
4173
4284
  const stat = fs5.statSync(filePath);
4174
4285
  if (now - stat.mtimeMs > maxAgeMs) {
4175
4286
  fs5.unlinkSync(filePath);
@@ -4200,7 +4311,7 @@ function listLogs() {
4200
4311
  const match = file.match(/^mihomo\.(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.log$/);
4201
4312
  if (!match) continue;
4202
4313
  try {
4203
- const filePath = path3.join(logsDir, file);
4314
+ const filePath = path4.join(logsDir, file);
4204
4315
  const stat = fs5.statSync(filePath);
4205
4316
  result.archives.push({
4206
4317
  name: file,
@@ -4216,22 +4327,22 @@ function listLogs() {
4216
4327
  return result;
4217
4328
  }
4218
4329
  function isPathUnderDir(filePath, baseDir) {
4219
- const resolvedPath = path3.resolve(filePath);
4220
- const resolvedBase = path3.resolve(baseDir);
4221
- 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);
4222
4333
  }
4223
4334
  function getLogPathByName(name) {
4224
4335
  const logsDir = DIRS.logs;
4225
4336
  let targetName = name;
4226
4337
  if (!name.endsWith(".log")) targetName = `mihomo.${name}.log`;
4227
4338
  if (!targetName.startsWith("mihomo.")) targetName = `mihomo.${targetName}`;
4228
- const filePath = path3.join(logsDir, targetName);
4339
+ const filePath = path4.join(logsDir, targetName);
4229
4340
  if (fs5.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) return filePath;
4230
4341
  if (fs5.existsSync(logsDir)) {
4231
4342
  const files = fs5.readdirSync(logsDir);
4232
4343
  for (const file of files) {
4233
4344
  if (file.includes(name)) {
4234
- const candidatePath = path3.join(logsDir, file);
4345
+ const candidatePath = path4.join(logsDir, file);
4235
4346
  if (isPathUnderDir(candidatePath, logsDir)) return candidatePath;
4236
4347
  }
4237
4348
  }
@@ -4280,31 +4391,16 @@ function viewLogWithTail(logPath, options) {
4280
4391
  }
4281
4392
 
4282
4393
  // src/daemon.ts
4283
- var LAUNCHCTL_TIMEOUT_MS = 1e4;
4284
- function launchctl(args) {
4285
- const result = spawnSync4("launchctl", args, { encoding: "utf8", timeout: LAUNCHCTL_TIMEOUT_MS });
4286
- return {
4287
- status: result.status,
4288
- stdout: result.stdout || "",
4289
- stderr: result.stderr || ""
4290
- };
4291
- }
4292
- function getUid() {
4293
- const uid = process.getuid?.();
4294
- if (uid === void 0) {
4295
- throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7528\u6237 UID\uFF0C\u4FDD\u6D3B\u529F\u80FD\u4EC5\u652F\u6301 macOS");
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;
4296
4403
  }
4297
- return uid;
4298
- }
4299
- function guiDomain(uid) {
4300
- return `gui/${uid}`;
4301
- }
4302
- function serviceTarget(uid) {
4303
- return `gui/${uid}/${LAUNCH_AGENT_LABEL}`;
4304
- }
4305
- function launchctlError(action, result) {
4306
- const detail = (result.stderr || result.stdout || "").trim() || `\u9000\u51FA\u7801 ${result.status}`;
4307
- return new Error(`launchctl ${action} \u5931\u8D25: ${detail}`);
4308
4404
  }
4309
4405
  function escapeXml(s) {
4310
4406
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -4317,7 +4413,7 @@ function buildPlist() {
4317
4413
  <plist version="1.0">
4318
4414
  <dict>
4319
4415
  <key>Label</key>
4320
- <string>${escapeXml(LAUNCH_AGENT_LABEL)}</string>
4416
+ <string>${escapeXml(LAUNCH_DAEMON_LABEL)}</string>
4321
4417
  <key>ProgramArguments</key>
4322
4418
  <array>
4323
4419
  ${argsXml}
@@ -4336,25 +4432,42 @@ ${argsXml}
4336
4432
  </plist>
4337
4433
  `;
4338
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");
4448
+ }
4449
+ if (result.status == null) {
4450
+ throw new Error(`${opts.action}\u88AB\u4E2D\u65AD\uFF08sudo \u8FDB\u7A0B\u88AB\u4FE1\u53F7\u7EC8\u6B62\uFF09`);
4451
+ }
4452
+ const custom = opts.codeMessages?.[result.status];
4453
+ throw new Error(custom || `${opts.action}\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.status}\uFF09`);
4454
+ }
4455
+ } finally {
4456
+ try {
4457
+ fs6.unlinkSync(scriptPath);
4458
+ } catch {
4459
+ }
4460
+ }
4461
+ }
4339
4462
  function isDaemonEnabled() {
4340
- return fs6.existsSync(PATHS.launchAgentPlist);
4463
+ return fs6.existsSync(PATHS.launchDaemonPlist);
4341
4464
  }
4342
4465
  function getDaemonStatus() {
4343
4466
  if (!isDaemonEnabled()) {
4344
4467
  return { enabled: false, loaded: false, pid: null };
4345
4468
  }
4346
- const uid = process.getuid?.();
4347
- if (uid === void 0) {
4348
- const pids = getAllMihomoPids();
4349
- return { enabled: true, loaded: pids.length > 0, pid: pids[0] ?? null };
4350
- }
4351
- const result = launchctl(["print", serviceTarget(uid)]);
4352
- if (result.status !== 0) {
4353
- return { enabled: true, loaded: false, pid: null };
4354
- }
4355
- const pidMatch = result.stdout.match(/\bpid = (\d+)/);
4356
- const pid = pidMatch ? parseInt(pidMatch[1], 10) : null;
4357
- return { enabled: true, loaded: true, pid };
4469
+ const rootPids = getMihomoPids().filter(isProcessRoot);
4470
+ return { enabled: true, loaded: rootPids.length > 0, pid: rootPids[0] ?? null };
4358
4471
  }
4359
4472
  function isDaemonRunning(status) {
4360
4473
  return status.loaded && status.pid !== null;
@@ -4366,47 +4479,104 @@ function enableDaemon() {
4366
4479
  if (!fs6.existsSync(PATHS.configFile)) {
4367
4480
  throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4368
4481
  }
4369
- const uid = getUid();
4370
- launchctl(["bootout", serviceTarget(uid)]);
4371
- cleanupAll();
4372
- fs6.mkdirSync(path4.dirname(PATHS.launchAgentPlist), { recursive: true });
4373
- atomicWriteFileSync(PATHS.launchAgentPlist, buildPlist(), { mode: 384 });
4374
- const result = launchctl(["bootstrap", guiDomain(uid), PATHS.launchAgentPlist]);
4375
- if (result.status !== 0) {
4376
- launchctl(["bootout", serviceTarget(uid)]);
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"
4506
+ }
4507
+ });
4508
+ } finally {
4377
4509
  try {
4378
- fs6.unlinkSync(PATHS.launchAgentPlist);
4510
+ fs6.unlinkSync(stagePath);
4379
4511
  } catch {
4380
4512
  }
4381
- throw launchctlError("bootstrap", result);
4382
4513
  }
4383
4514
  }
4384
4515
  function disableDaemon() {
4385
- const uid = process.getuid?.();
4386
- if (uid !== void 0) {
4387
- launchctl(["bootout", serviceTarget(uid)]);
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) {
4533
+ console.log("");
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");
4388
4536
  }
4537
+ }
4538
+ async function tryHotReload() {
4539
+ const controller = new AbortController();
4540
+ const timer = setTimeout(() => controller.abort(), HOT_RELOAD_TIMEOUT_MS);
4389
4541
  try {
4390
- fs6.unlinkSync(PATHS.launchAgentPlist);
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;
4391
4549
  } catch {
4550
+ return false;
4551
+ } finally {
4552
+ clearTimeout(timer);
4392
4553
  }
4393
4554
  }
4394
- function restartDaemon() {
4395
- const uid = getUid();
4396
- if (!fs6.existsSync(PATHS.launchAgentPlist)) {
4555
+ async function restartDaemon() {
4556
+ if (!fs6.existsSync(PATHS.launchDaemonPlist)) {
4397
4557
  throw new Error("\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u6CD5\u91CD\u542F");
4398
4558
  }
4399
- if (launchctl(["print", serviceTarget(uid)]).status === 0) {
4400
- const result2 = launchctl(["kickstart", "-k", serviceTarget(uid)]);
4401
- if (result2.status !== 0) {
4402
- throw launchctlError("kickstart", result2);
4403
- }
4404
- return;
4405
- }
4406
- const result = launchctl(["bootstrap", guiDomain(uid), PATHS.launchAgentPlist]);
4407
- if (result.status !== 0) {
4408
- throw launchctlError("bootstrap", result);
4409
- }
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();
4410
4580
  }
4411
4581
 
4412
4582
  // src/subscription.ts
@@ -4419,7 +4589,6 @@ function getDefaultUpdateInterval(url) {
4419
4589
  function resolveUpdateInterval(url, cachedInterval) {
4420
4590
  return cachedInterval && cachedInterval > 0 ? cachedInterval : getDefaultUpdateInterval(url);
4421
4591
  }
4422
- var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, schema: CORE_SCHEMA };
4423
4592
  var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4424
4593
  function isMultiUrl(url) {
4425
4594
  return url.includes(",");
@@ -4443,7 +4612,7 @@ function saveSubscriptionConfig(subName, parsed) {
4443
4612
  normalizeProxyNamesBeforeSave(parsed);
4444
4613
  parsed.raw.proxies = parsed.proxies;
4445
4614
  parsed.raw["proxy-groups"] = parsed.proxyGroups;
4446
- saveSubscriptionRawConfig(subName, dump(parsed.raw, YAML_DUMP_OPTS));
4615
+ saveSubscriptionRawConfig(subName, dumpYaml(parsed.raw));
4447
4616
  }
4448
4617
  function parseUserInfo(header) {
4449
4618
  if (!header) return null;
@@ -4610,7 +4779,7 @@ async function downloadMergedSubscription(urls, subName, signal) {
4610
4779
  }
4611
4780
  }
4612
4781
  base.proxies = baseProxies;
4613
- const mergedContent = dump(base, YAML_DUMP_OPTS);
4782
+ const mergedContent = dumpYaml(base);
4614
4783
  saveSubscriptionRawConfig(subName, mergedContent);
4615
4784
  const meta = extractSubscriptionMeta(responses[0].response?.headers);
4616
4785
  saveSubscriptionMeta(subName, meta);
@@ -4706,8 +4875,7 @@ async function autoUpdateStaleSubscription(options = {}) {
4706
4875
  }
4707
4876
  return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
4708
4877
  }
4709
- var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
4710
- async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API_BASE) {
4878
+ async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = CONTROLLER_BASE_URL) {
4711
4879
  const encodedName = encodeURIComponent(proxyName);
4712
4880
  const url = `${apiBase}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4713
4881
  try {
@@ -4729,7 +4897,13 @@ async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API
4729
4897
  }
4730
4898
  }
4731
4899
  async function testSubscriptionProxies(subName, options = {}) {
4732
- const { timeout = DEFAULT_TEST_TIMEOUT, concurrency = DEFAULT_TEST_CONCURRENCY, testUrl = DEFAULT_TEST_URL, apiBase = API_BASE, onResult } = 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;
4733
4907
  const { proxies } = options.parsed || loadSubscriptionConfig(subName);
4734
4908
  if (proxies.length === 0) {
4735
4909
  return { total: 0, alive: 0, dead: 0, results: [] };
@@ -4791,7 +4965,8 @@ function cleanDeadProxies(parsed, deadNames) {
4791
4965
  if (group.proxies.length < before) {
4792
4966
  updatedGroups++;
4793
4967
  }
4794
- if (group.proxies.length === 0) {
4968
+ const hasOtherSource = group.use || group["include-all"] || group["include-all-proxies"];
4969
+ if (group.proxies.length === 0 && !hasOtherSource) {
4795
4970
  removedGroupNames.add(group.name);
4796
4971
  }
4797
4972
  }
@@ -4807,9 +4982,7 @@ function cleanDeadProxies(parsed, deadNames) {
4807
4982
  if (Array.isArray(rules)) {
4808
4983
  parsed.raw.rules = rules.filter((rule) => {
4809
4984
  if (typeof rule !== "string") return true;
4810
- const parts = rule.split(",");
4811
- if (parts.length < 2) return true;
4812
- return !removedGroupNames.has(parts[parts.length - 1].trim());
4985
+ return !removedGroupNames.has(getRuleTarget(rule));
4813
4986
  });
4814
4987
  }
4815
4988
  }
@@ -4904,6 +5077,8 @@ async function cmdDaemon(args) {
4904
5077
  console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
4905
5078
  process.exit(1);
4906
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"));
4907
5082
  try {
4908
5083
  enableDaemon();
4909
5084
  } catch (e) {
@@ -4913,6 +5088,7 @@ async function cmdDaemon(args) {
4913
5088
  console.log(`${colors.green("\u5DF2\u542F\u7528\u4FDD\u6D3B")} \xB7 ${sub.name} \xB7 ${formatProxySummary(configInfo)}`);
4914
5089
  console.log(colors.gray("\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF0C\u4EE3\u7406\u5C06\u5728\u540E\u53F0\u5E38\u9A7B"));
4915
5090
  console.log("");
5091
+ await sleep(DAEMON_BOOT_WAIT_MS);
4916
5092
  printDaemonStatus();
4917
5093
  return;
4918
5094
  }
@@ -4923,6 +5099,7 @@ async function cmdDaemon(args) {
4923
5099
  printDaemonStatus();
4924
5100
  return;
4925
5101
  }
5102
+ console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u79FB\u9664\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1"));
4926
5103
  try {
4927
5104
  disableDaemon();
4928
5105
  } catch (e) {
@@ -5009,98 +5186,10 @@ function cmdDirectory(args) {
5009
5186
  console.log("");
5010
5187
  }
5011
5188
 
5012
- // src/commands/help.ts
5013
- function printShortHelp() {
5014
- console.log(`
5015
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
5016
- `);
5017
- console.log(
5018
- `\u5E38\u7528\u547D\u4EE4:
5019
- ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
5020
- ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
5021
- ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
5022
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
5023
- `
5024
- );
5025
- }
5026
- function printHelp() {
5027
- console.log(
5028
- `
5029
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}
5030
-
5031
- \u547D\u4EE4\u522B\u540D: mihomo, mhm, mh
5032
-
5033
- \u7528\u6CD5:
5034
- mihomo <\u547D\u4EE4> [\u9009\u9879]
5035
-
5036
- ${colors.cyan("\u63A7\u5236:")}
5037
- ${colors.bold("start")} [tun|mixed] [-s] [-u ms] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)
5038
- [-r N] [-t ms] [-j N]
5039
- ${colors.bold("stop")} \u505C\u6B62\u4EE3\u7406
5040
- ${colors.bold("status")} \u67E5\u770B\u72B6\u6001
5041
-
5042
- ${colors.cyan("\u754C\u9762:")}
5043
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)
5044
- ${colors.bold("log")} [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09
5045
- ${colors.bold("logs")} [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09
5046
-
5047
- ${colors.cyan("\u8BA2\u9605:")}
5048
- ${colors.bold("subscription")} \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09
5049
- ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
5050
- ${colors.bold("subscription")} add <url> [name] \u6DFB\u52A0\u8BA2\u9605
5051
- ${colors.bold("subscription")} update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09
5052
- ${colors.bold("subscription")} remove <name> \u5220\u9664\u8BA2\u9605
5053
- ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
5054
- ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
5055
- ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
5056
- ${colors.bold("test")} [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027
5057
- ${colors.bold("clean")} [-t ms] [-j N] [-r N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F
5058
-
5059
- ${colors.cyan("\u914D\u7F6E:")}
5060
- ${colors.bold("overwrite")} \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09
5061
- ${colors.bold("overwrite")} on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E
5062
- ${colors.bold("directory")} \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09
5063
- ${colors.bold("directory")} open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|...
5064
-
5065
- ${colors.cyan("\u7CFB\u7EDF:")}
5066
- ${colors.bold("kernel")} [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09
5067
- ${colors.bold("daemon")} on|off \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF08\u4EC5 Mixed\uFF09
5068
- ${colors.bold("daemon")} status \u67E5\u770B\u4FDD\u6D3B\u72B6\u6001
5069
- ${colors.bold("update")} \u66F4\u65B0 mihomo-cli (npm install -g)
5070
- ${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
5071
- ${colors.bold("help")}, -h \u663E\u793A\u5E2E\u52A9
5072
- ${colors.bold("version")}, -v \u663E\u793A\u7248\u672C
5073
-
5074
- ${colors.cyan("\u793A\u4F8B:")}
5075
- mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F
5076
- mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F
5077
- mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605
5078
- mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)
5079
- mihomo daemon on # \u5F00\u542F\u4FDD\u6D3B\uFF08\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF09
5080
- mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)
5081
- mihomo ui # \u6253\u5F00 Web UI
5082
-
5083
- ${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}
5084
- mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)
5085
- tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo
5086
-
5087
- ${colors.cyan("\u6570\u636E\u76EE\u5F55:")}
5088
- \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E
5089
- \u9ED8\u8BA4: ${USER_DATA_DIR}
5090
- `
5091
- );
5092
- }
5093
- function printVersion() {
5094
- const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
5095
- console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
5096
- console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
5097
- console.log(`${colors.gray("\u6570\u636E\u76EE\u5F55: ")}${USER_DATA_DIR}`);
5098
- }
5099
-
5100
5189
  // src/kernel.ts
5101
5190
  import { spawnSync as spawnSync5 } from "child_process";
5102
5191
  import fs7 from "fs";
5103
- import path5 from "path";
5192
+ import path6 from "path";
5104
5193
 
5105
5194
  // node_modules/compare-versions/lib/esm/utils.js
5106
5195
  var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
@@ -5185,7 +5274,7 @@ function findMatchingAsset(assets, platform, arch) {
5185
5274
  const nameWithoutGz = a.name.slice(0, -3);
5186
5275
  const parts = nameWithoutGz.split("-");
5187
5276
  const lastPart = parts[parts.length - 1];
5188
- return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go");
5277
+ return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go") && !nameWithoutGz.includes("-compatible");
5189
5278
  });
5190
5279
  return standardAsset || matchingAssets[0];
5191
5280
  }
@@ -5228,7 +5317,7 @@ function findBinaryInDir(dir, maxDepth = 4) {
5228
5317
  if (maxDepth <= 0) return null;
5229
5318
  const files = fs7.readdirSync(dir);
5230
5319
  for (const f of files) {
5231
- const fullPath = path5.join(dir, f);
5320
+ const fullPath = path6.join(dir, f);
5232
5321
  const stat = fs7.statSync(fullPath);
5233
5322
  if (stat.isDirectory()) {
5234
5323
  const found = findBinaryInDir(fullPath, maxDepth - 1);
@@ -5255,7 +5344,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5255
5344
  \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
5256
5345
  }
5257
5346
  const downloadUrl = withMirror(asset.browser_download_url, mirror);
5258
- const tempPath = path5.join(DIRS.kernel, asset.name);
5347
+ const tempPath = path6.join(DIRS.kernel, asset.name);
5259
5348
  const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
5260
5349
  if (mirror && progressCallback) {
5261
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");
@@ -5289,8 +5378,8 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5289
5378
  if (tarResult.error) throw tarResult.error;
5290
5379
  if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
5291
5380
  } else if (tempPath.endsWith(".gz")) {
5292
- const baseName = path5.basename(tempPath, ".gz");
5293
- const outputPath = path5.join(extractPath, baseName);
5381
+ const baseName = path6.basename(tempPath, ".gz");
5382
+ const outputPath = path6.join(extractPath, baseName);
5294
5383
  const gzipResult = spawnSync5("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
5295
5384
  if (gzipResult.error) throw gzipResult.error;
5296
5385
  if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
@@ -5357,20 +5446,8 @@ async function cmdKernel(args) {
5357
5446
  if (effectiveMirror) {
5358
5447
  const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
5359
5448
  console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
5449
+ console.log("");
5360
5450
  }
5361
- 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");
5362
- console.log("\n\u7528\u6CD5:");
5363
- console.log(" mihomo kernel # \u76F4\u8FDE");
5364
- console.log(" mihomo kernel --mirror # \u4E0B\u8F7D\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF (v6.gh-proxy.org)");
5365
- console.log(" mihomo kernel --mirror hk.gh-proxy.org # \u4E0B\u8F7D\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
5366
- console.log(" mihomo kernel --mirror-all # API\u8BF7\u6C42\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF");
5367
- console.log(" mihomo kernel --mirror-all hk.gh-proxy.org # API\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
5368
- console.log("\n\u53EF\u7528\u955C\u50CF:");
5369
- for (const m of AVAILABLE_MIRRORS) {
5370
- const isCurrent = effectiveMirror && (effectiveMirror.includes(`//${m}/`) || effectiveMirror.includes(`//${m}:`) || effectiveMirror.endsWith(`//${m}`));
5371
- console.log(` ${m}${isCurrent ? " (\u5F53\u524D)" : ""}`);
5372
- }
5373
- console.log("");
5374
5451
  console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
5375
5452
  try {
5376
5453
  const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
@@ -5397,6 +5474,13 @@ async function cmdKernel(args) {
5397
5474
  console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
5398
5475
  }
5399
5476
  }
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
+ }
5400
5484
  process.exit(1);
5401
5485
  }
5402
5486
  }
@@ -5484,18 +5568,43 @@ function cmdLogs(args) {
5484
5568
  }
5485
5569
 
5486
5570
  // src/commands/overwrite.ts
5487
- import path7 from "path";
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 };
5582
+ }
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;
5594
+ }
5595
+ const result = await start(mode);
5596
+ return result.pid;
5597
+ }
5488
5598
 
5489
5599
  // src/commands/status.ts
5490
5600
  function printStatus() {
5491
5601
  const status = getStatus();
5492
- const daemon = getDaemonStatus();
5602
+ const state = getRunningState();
5493
5603
  const info = getConfigInfo();
5494
5604
  const overwriteEnabled = isOverwriteEnabled();
5495
5605
  const overwriteFiles = listOverwriteFile().files;
5496
5606
  const activeSub = getActiveSubscription();
5497
- const running = daemon.enabled ? isDaemonRunning(daemon) : status.running;
5498
- const pid = daemon.enabled ? daemon.pid : status.pid;
5607
+ const { running, pid, daemon: daemonManaged } = state;
5499
5608
  console.log("");
5500
5609
  let modeLabel = "";
5501
5610
  if (info && running) {
@@ -5506,7 +5615,7 @@ function printStatus() {
5506
5615
  console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
5507
5616
  if (pid) {
5508
5617
  console.log(`${colors.gray("PID: ")}${pid}`);
5509
- if (!daemon.enabled && status.processInfo) {
5618
+ if (!daemonManaged && status.processInfo) {
5510
5619
  console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
5511
5620
  }
5512
5621
  }
@@ -5537,7 +5646,7 @@ function printStatus() {
5537
5646
  } else {
5538
5647
  console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
5539
5648
  }
5540
- if (daemon.enabled) {
5649
+ if (isDaemonEnabled()) {
5541
5650
  console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
5542
5651
  }
5543
5652
  console.log("");
@@ -5557,7 +5666,7 @@ async function cmdStop() {
5557
5666
  console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
5558
5667
  return;
5559
5668
  }
5560
- const pids = getAllMihomoPids();
5669
+ const pids = getMihomoPids();
5561
5670
  if (pids.length === 0) {
5562
5671
  console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
5563
5672
  return;
@@ -5570,7 +5679,7 @@ async function cmdStop() {
5570
5679
  // src/test-instance.ts
5571
5680
  import { spawn as spawn2 } from "child_process";
5572
5681
  import fs8 from "fs";
5573
- import path6 from "path";
5682
+ import path7 from "path";
5574
5683
 
5575
5684
  // src/lifecycle.ts
5576
5685
  var cleanupFns = /* @__PURE__ */ new Set();
@@ -5591,15 +5700,15 @@ function runCleanup() {
5591
5700
  }
5592
5701
 
5593
5702
  // src/test-instance.ts
5594
- var TEST_DIR = path6.join(USER_DATA_DIR, "test");
5703
+ var TEST_DIR = path7.join(USER_DATA_DIR, "test");
5595
5704
  var TEST_DIRS = {
5596
- data: path6.join(TEST_DIR, "data"),
5597
- runtime: path6.join(TEST_DIR, "runtime")
5705
+ data: path7.join(TEST_DIR, "data"),
5706
+ runtime: path7.join(TEST_DIR, "runtime")
5598
5707
  };
5599
5708
  var TEST_PATHS = {
5600
- configFile: path6.join(TEST_DIRS.runtime, "config.yaml"),
5601
- pidFile: path6.join(TEST_DIRS.runtime, "pid"),
5602
- logFile: path6.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")
5603
5712
  };
5604
5713
  var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
5605
5714
  function ensureTestDirs() {
@@ -5641,7 +5750,7 @@ function buildTestConfig(subName) {
5641
5750
  ],
5642
5751
  rules: ["MATCH,PROXY"]
5643
5752
  };
5644
- const content = dump(config, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
5753
+ const content = dumpYaml(config);
5645
5754
  fs8.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
5646
5755
  }
5647
5756
  async function startTestInstance() {
@@ -5811,6 +5920,12 @@ function resolveTestTarget(args) {
5811
5920
  }
5812
5921
  return { target, timeout, concurrency };
5813
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
+ }
5814
5929
  async function printSubscriptionList(options) {
5815
5930
  if (options?.autoUpdate !== false) {
5816
5931
  const updateResult = await autoUpdateStaleSubscription();
@@ -5934,6 +6049,7 @@ async function cmdSubscription(args) {
5934
6049
  }
5935
6050
  if (ok === 0) process.exit(1);
5936
6051
  console.log("");
6052
+ printRestartHintIfRunning();
5937
6053
  await printSubscriptionList();
5938
6054
  return;
5939
6055
  }
@@ -5947,6 +6063,7 @@ async function cmdSubscription(args) {
5947
6063
  }
5948
6064
  console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(result)})`);
5949
6065
  console.log("");
6066
+ printRestartHintIfRunning();
5950
6067
  await printSubscriptionList();
5951
6068
  return;
5952
6069
  }
@@ -5971,9 +6088,8 @@ async function cmdSubscription(args) {
5971
6088
  await printSubscriptionList();
5972
6089
  return;
5973
6090
  }
5974
- const status = getStatus();
5975
- const configInfo = getConfigInfo();
5976
- const currentMode = isDaemonEnabled() ? "mixed" : configInfo?.tun ? "tun" : "mixed";
6091
+ const currentMode = getRuntimeMode();
6092
+ const restartNeeded = isRestartNeededOnChange();
5977
6093
  const success = setDefaultSubscription(target.name);
5978
6094
  if (success) {
5979
6095
  console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
@@ -5981,7 +6097,7 @@ async function cmdSubscription(args) {
5981
6097
  console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
5982
6098
  process.exit(1);
5983
6099
  }
5984
- if (status.running || isDaemonEnabled()) {
6100
+ if (restartNeeded) {
5985
6101
  console.log("");
5986
6102
  await cmdStart(["start", currentMode]);
5987
6103
  return;
@@ -6075,8 +6191,7 @@ async function cmdSubscription(args) {
6075
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"));
6076
6192
  } else if (result.removedProxies > 0) {
6077
6193
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6078
- const status = getStatus();
6079
- if (status.running) {
6194
+ if (getRunningState().running) {
6080
6195
  console.log("");
6081
6196
  console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
6082
6197
  }
@@ -6107,7 +6222,6 @@ async function cmdSubscription(args) {
6107
6222
  }
6108
6223
 
6109
6224
  // src/commands/start.ts
6110
- var DAEMON_RESTART_WAIT_MS = 500;
6111
6225
  async function cmdStart(args) {
6112
6226
  if (!hasKernel()) {
6113
6227
  console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
@@ -6133,15 +6247,6 @@ async function cmdStart(args) {
6133
6247
  if (!skipUpdate) {
6134
6248
  await autoUpdateStaleSubscription({ timeout: updateTimeout });
6135
6249
  }
6136
- async function launchOrRestart() {
6137
- if (daemonEnabled) {
6138
- restartDaemon();
6139
- await sleep(DAEMON_RESTART_WAIT_MS);
6140
- return getDaemonStatus().pid;
6141
- }
6142
- const result = await start(targetMode);
6143
- return result.pid;
6144
- }
6145
6250
  if (!daemonEnabled) {
6146
6251
  const status = getStatus();
6147
6252
  const hasProcess = status.running || status.allProcesses.length > 0;
@@ -6165,7 +6270,7 @@ async function cmdStart(args) {
6165
6270
  const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
6166
6271
  console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
6167
6272
  try {
6168
- const pid = await launchOrRestart();
6273
+ const pid = await launchOrRestart(targetMode);
6169
6274
  const label = daemonEnabled ? "\u5DF2\u542F\u52A8 (\u4FDD\u6D3B)" : "\u5DF2\u542F\u52A8";
6170
6275
  console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""}`);
6171
6276
  } catch (e) {
@@ -6202,7 +6307,7 @@ async function cmdStart(args) {
6202
6307
  if (!daemonEnabled) handleStopResult(stop());
6203
6308
  try {
6204
6309
  configInfo = prepareConfigForStart(targetMode, sub.name);
6205
- const pid = await launchOrRestart();
6310
+ const pid = await launchOrRestart(targetMode);
6206
6311
  console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
6207
6312
  } catch (e) {
6208
6313
  console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
@@ -6223,8 +6328,8 @@ function printOverwriteList() {
6223
6328
  if (info.files.length === 0) {
6224
6329
  console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
6225
6330
  console.log("");
6226
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path7.join(info.dir, "overwrite.yaml")}`);
6227
- console.log(` \u6216 ${path7.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")}`);
6228
6333
  console.log("");
6229
6334
  } else {
6230
6335
  console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
@@ -6244,9 +6349,8 @@ function printOverwriteList() {
6244
6349
  }
6245
6350
  async function cmdOverwrite(args) {
6246
6351
  const action = args?.[1];
6247
- const status = getStatus();
6248
- const configInfo = getConfigInfo();
6249
- const currentMode = isDaemonEnabled() ? "mixed" : configInfo?.tun ? "tun" : "mixed";
6352
+ const currentMode = getRuntimeMode();
6353
+ const restartNeeded = isRestartNeededOnChange();
6250
6354
  if (action === "on" || action === "enable") {
6251
6355
  if (isOverwriteEnabled()) {
6252
6356
  console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u542F\u7528\u72B6\u6001");
@@ -6256,7 +6360,7 @@ async function cmdOverwrite(args) {
6256
6360
  }
6257
6361
  setOverwriteEnabled(true);
6258
6362
  console.log("\u5DF2\u542F\u7528\u8986\u5199\u914D\u7F6E");
6259
- if (status.running || isDaemonEnabled()) {
6363
+ if (restartNeeded) {
6260
6364
  console.log("");
6261
6365
  await cmdStart(["start", currentMode]);
6262
6366
  return;
@@ -6274,7 +6378,7 @@ async function cmdOverwrite(args) {
6274
6378
  }
6275
6379
  setOverwriteEnabled(false);
6276
6380
  console.log("\u5DF2\u7981\u7528\u8986\u5199\u914D\u7F6E");
6277
- if (status.running || isDaemonEnabled()) {
6381
+ if (restartNeeded) {
6278
6382
  console.log("");
6279
6383
  await cmdStart(["start", currentMode]);
6280
6384
  return;
@@ -6344,7 +6448,7 @@ var RESET_TARGETS = [
6344
6448
  paths: () => {
6345
6449
  const dir = USER_DATA_DIR;
6346
6450
  if (!fs9.existsSync(dir)) return [];
6347
- return fs9.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
6451
+ return fs9.readdirSync(dir).filter(isOverwriteFilename).map((f) => `${dir}/${f}`);
6348
6452
  },
6349
6453
  needsStop: false
6350
6454
  },
@@ -6352,8 +6456,9 @@ var RESET_TARGETS = [
6352
6456
  id: "daemon",
6353
6457
  aliases: ["daemon"],
6354
6458
  label: "\u4FDD\u6D3B",
6355
- // 删除全部交给 onAfter:disableDaemon 会先 bootout 卸载、再删 plist、再兜底清理进程,
6356
- // 顺序正确(先卸载再删文件);此处返回空避免提前删掉 plist 破坏卸载。
6459
+ // 卸载由确认后的 disablesDaemon 段统一处理(需 sudo,受取消保护);
6460
+ // 此处 paths 返回空(plist 在系统目录,用户态删不掉,且不应提前删破坏卸载),
6461
+ // onAfter 因幂等守卫(plist 已删)成为 no-op,仅作单独 reset 未走前段时的兜底。
6357
6462
  paths: () => [],
6358
6463
  needsStop: false,
6359
6464
  onAfter: () => disableDaemon(),
@@ -6421,8 +6526,9 @@ async function cmdReset(args) {
6421
6526
  const needsStop = targets.some((t) => t.needsStop);
6422
6527
  const warnRunning = targets.some((t) => t.warnIfRunning);
6423
6528
  const kernelTargeted = targets.some((t) => t.id === "kernel");
6424
- const disablesDaemon = needsStop || kernelTargeted;
6425
- const pids = needsStop || warnRunning ? getAllMihomoPids() : [];
6529
+ const daemonTargeted = targets.some((t) => t.id === "daemon");
6530
+ const disablesDaemon = needsStop || kernelTargeted || daemonTargeted;
6531
+ const pids = needsStop || warnRunning ? getMihomoPids() : [];
6426
6532
  if (warnRunning && pids.length > 0) {
6427
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`));
6428
6534
  }
@@ -6435,13 +6541,18 @@ async function cmdReset(args) {
6435
6541
  return;
6436
6542
  }
6437
6543
  if (disablesDaemon && isDaemonEnabled()) {
6438
- disableDaemon();
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
+ }
6439
6550
  }
6440
- if (needsStop && getAllMihomoPids().length > 0) {
6551
+ if (needsStop && getMihomoPids().length > 0) {
6441
6552
  console.log("\u505C\u6B62\u8FDB\u7A0B...");
6442
6553
  cleanupAll();
6443
6554
  for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6444
- if (getAllMihomoPids().length === 0) break;
6555
+ if (getMihomoPids().length === 0) break;
6445
6556
  await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6446
6557
  }
6447
6558
  }
@@ -6466,16 +6577,10 @@ async function cmdReset(args) {
6466
6577
 
6467
6578
  // src/commands/test.ts
6468
6579
  function requireRunning() {
6469
- if (isDaemonEnabled()) {
6470
- if (!isDaemonRunning(getDaemonStatus())) {
6471
- console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo daemon on)");
6472
- process.exit(1);
6473
- }
6474
- return;
6475
- }
6476
- const status = getStatus();
6477
- if (!status.running) {
6478
- 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})`);
6479
6584
  process.exit(1);
6480
6585
  }
6481
6586
  }
@@ -6532,21 +6637,17 @@ async function cmdClean(args) {
6532
6637
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6533
6638
  console.log("");
6534
6639
  console.log("\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548...");
6535
- if (isDaemonEnabled()) {
6536
- const configInfo = prepareConfigForStart("mixed", activeSub.name);
6537
- try {
6538
- restartDaemon();
6539
- } catch (e) {
6540
- console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6541
- process.exit(1);
6542
- }
6543
- console.log(`${colors.green("\u5DF2\u91CD\u542F (\u4FDD\u6D3B)")} \xB7 ${formatProxySummary(configInfo)}`);
6544
- } else {
6545
- const currentMode = getConfigInfo()?.tun ? "tun" : "mixed";
6546
- handleStopResult(stop());
6547
- const configInfo = prepareConfigForStart(currentMode, activeSub.name);
6548
- const startResult = await start(currentMode);
6549
- 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);
6550
6651
  }
6551
6652
  }
6552
6653
  }
@@ -6607,6 +6708,199 @@ async function cmdUpdate() {
6607
6708
  }
6608
6709
  }
6609
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
+
6610
6904
  // src/index.ts
6611
6905
  process.on("SIGINT", () => {
6612
6906
  console.log("\n\u6B63\u5728\u9000\u51FA...");
@@ -6623,12 +6917,14 @@ process.on("uncaughtException", (e) => {
6623
6917
  if (e.stack) {
6624
6918
  console.error(e.stack.split("\n").slice(1).join("\n"));
6625
6919
  }
6920
+ runCleanup();
6626
6921
  process.exit(1);
6627
6922
  });
6628
6923
  process.on("unhandledRejection", (reason) => {
6629
6924
  const msg = reason instanceof Error ? reason.message : String(reason);
6630
6925
  console.error(`
6631
6926
  \u672A\u5904\u7406\u7684 Promise \u62D2\u7EDD: ${msg}`);
6927
+ runCleanup();
6632
6928
  process.exit(1);
6633
6929
  });
6634
6930
  function clearProxyEnv() {
@@ -6648,94 +6944,18 @@ async function main() {
6648
6944
  printShortHelp();
6649
6945
  return;
6650
6946
  }
6651
- const cmd = args[0].toLowerCase();
6652
- if (["help", "-h", "--help"].includes(cmd)) {
6653
- printHelp();
6654
- return;
6655
- }
6656
- if (["version", "-v", "--version"].includes(cmd)) {
6657
- printVersion();
6658
- return;
6659
- }
6660
- switch (cmd) {
6661
- case "up":
6662
- case "start":
6663
- await cmdStart(args);
6664
- break;
6665
- case "tun":
6666
- await cmdStart(["start", "tun", ...args.slice(1)]);
6667
- break;
6668
- case "down":
6669
- case "stop":
6670
- await cmdStop();
6671
- break;
6672
- case "status":
6673
- printStatus();
6674
- break;
6675
- case "log":
6676
- cmdLog(args);
6677
- break;
6678
- case "logs":
6679
- cmdLogs(args);
6680
- break;
6681
- case "open":
6682
- cmdDirectory(["dir", "open", ...args.slice(1)]);
6683
- break;
6684
- case "ui":
6685
- cmdUI(args);
6686
- break;
6687
- case "kernel":
6688
- await cmdKernel(args);
6689
- break;
6690
- case "upd":
6691
- case "update":
6692
- case "upgrade":
6693
- await cmdUpdate();
6694
- break;
6695
- case "use":
6696
- await cmdSubscription(["sub", "use", ...args.slice(1)]);
6697
- break;
6698
- case "sub":
6699
- case "subscription":
6700
- case "subscriptions":
6701
- await cmdSubscription(args);
6702
- break;
6703
- case "dir":
6704
- case "dirs":
6705
- case "directory":
6706
- case "directories":
6707
- cmdDirectory(args);
6708
- break;
6709
- case "reset":
6710
- await cmdReset(args);
6711
- break;
6712
- case "daemon":
6713
- await cmdDaemon(args);
6714
- break;
6715
- case "on":
6716
- await cmdOverwrite(["ow", "on"]);
6717
- break;
6718
- case "off":
6719
- await cmdOverwrite(["ow", "off"]);
6720
- break;
6721
- case "ow":
6722
- case "overwrite":
6723
- await cmdOverwrite(args);
6724
- break;
6725
- case "test":
6726
- await cmdTest(args);
6727
- break;
6728
- case "clean":
6729
- await cmdClean(args);
6730
- break;
6731
- default:
6732
- console.error(`\u672A\u77E5\u547D\u4EE4: ${cmd}`);
6733
- console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');
6734
- 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);
6735
6953
  }
6954
+ await command.handler(command.rewrite ? command.rewrite(args) : args);
6736
6955
  }
6737
6956
  main().catch((e) => {
6738
6957
  console.error(`\u9519\u8BEF: ${e.message}`);
6958
+ runCleanup();
6739
6959
  process.exit(1);
6740
6960
  });
6741
6961
  /*! Bundled license information: