mihomo-cli 3.4.0 → 3.6.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,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/colors.ts
4
+ var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
5
+ function colorize(code, str) {
6
+ if (NO_COLOR) return String(str);
7
+ return `${code + String(str)}\x1B[0m`;
8
+ }
9
+ var colors = {
10
+ bold: (s) => colorize("\x1B[1m", s),
11
+ red: (s) => colorize("\x1B[31m", s),
12
+ green: (s) => colorize("\x1B[32m", s),
13
+ yellow: (s) => colorize("\x1B[33m", s),
14
+ cyan: (s) => colorize("\x1B[36m", s),
15
+ gray: (s) => colorize("\x1B[90m", s)
16
+ };
17
+
3
18
  // src/config.ts
4
- import { spawnSync as spawnSync2 } from "child_process";
19
+ import { spawnSync } from "child_process";
5
20
  import fs4 from "fs";
6
21
 
7
22
  // node_modules/js-yaml/dist/js-yaml.mjs
@@ -3040,6 +3055,11 @@ var CHOMPING_STRIP = CHOMPING_MODE.STRIP;
3040
3055
  var CHOMPING_KEEP = CHOMPING_MODE.KEEP;
3041
3056
 
3042
3057
  // src/constants.ts
3058
+ import { createRequire } from "module";
3059
+ var require2 = createRequire(import.meta.url);
3060
+ var pkg = require2("../package.json");
3061
+ var VERSION = pkg.version;
3062
+ var PKG_NAME = pkg.name;
3043
3063
  var AVAILABLE_MIRRORS = ["v6.gh-proxy.org", "gh-proxy.org", "hk.gh-proxy.org", "cdn.gh-proxy.org"];
3044
3064
  var DEFAULT_MIRROR = "https://v6.gh-proxy.org/";
3045
3065
  var UI_URLS = {
@@ -3171,6 +3191,43 @@ function rmrf(dir) {
3171
3191
  // src/settings.ts
3172
3192
  import fs2 from "fs";
3173
3193
  import path2 from "path";
3194
+
3195
+ // src/errors.ts
3196
+ var TimeoutError = class extends Error {
3197
+ constructor() {
3198
+ super("timeout");
3199
+ this.name = "TimeoutError";
3200
+ }
3201
+ };
3202
+ var CliError = class extends Error {
3203
+ hint;
3204
+ label;
3205
+ exitCode;
3206
+ constructor(message, options = {}) {
3207
+ super(message);
3208
+ this.name = "CliError";
3209
+ this.label = options.label ?? "\u9519\u8BEF";
3210
+ this.hint = options.hint === void 0 ? [] : Array.isArray(options.hint) ? options.hint : [options.hint];
3211
+ this.exitCode = options.exitCode ?? 1;
3212
+ }
3213
+ };
3214
+ function withTimeout(promise, ms) {
3215
+ return new Promise((resolve, reject) => {
3216
+ const timer = setTimeout(() => reject(new TimeoutError()), ms);
3217
+ promise.then(
3218
+ (v) => {
3219
+ clearTimeout(timer);
3220
+ resolve(v);
3221
+ },
3222
+ (e) => {
3223
+ clearTimeout(timer);
3224
+ reject(e);
3225
+ }
3226
+ );
3227
+ });
3228
+ }
3229
+
3230
+ // src/settings.ts
3174
3231
  var settingsCache = null;
3175
3232
  function readSettings() {
3176
3233
  if (settingsCache !== null) return settingsCache;
@@ -3274,7 +3331,7 @@ function getSubscriptionsWithCache() {
3274
3331
  var SAFE_NAME_RE = /^[\w\-\p{Unified_Ideograph}]{1,64}$/u;
3275
3332
  function validateSubscriptionName(name) {
3276
3333
  if (!name || !SAFE_NAME_RE.test(name)) {
3277
- throw new Error(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${name}"\uFF0C\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u548C\u4E2D\u6587\uFF08\u6700\u957F 64 \u5B57\u7B26\uFF09`);
3334
+ throw new CliError(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${name}"\uFF0C\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u548C\u4E2D\u6587\uFF08\u6700\u957F 64 \u5B57\u7B26\uFF09`);
3278
3335
  }
3279
3336
  }
3280
3337
  function addSubscription(url, name = "default") {
@@ -3282,7 +3339,7 @@ function addSubscription(url, name = "default") {
3282
3339
  const settings = readSettings();
3283
3340
  const subs = [...settings.subscriptions || []];
3284
3341
  if (subs.some((s) => s.name === name)) {
3285
- 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`);
3342
+ throw new CliError(`\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`);
3286
3343
  }
3287
3344
  subs.push({ name, url });
3288
3345
  const updates = { subscriptions: subs };
@@ -3326,7 +3383,7 @@ function setDefaultSubscription(name) {
3326
3383
  }
3327
3384
  function getSubscriptionRawConfigPath(subName) {
3328
3385
  if (!SAFE_NAME_RE.test(subName)) {
3329
- throw new Error(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${subName}"`);
3386
+ throw new CliError(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${subName}"`);
3330
3387
  }
3331
3388
  return path2.join(DIRS.subscriptions, `${subName}.yaml`);
3332
3389
  }
@@ -3564,26 +3621,7 @@ function listOverwriteFile() {
3564
3621
  }
3565
3622
 
3566
3623
  // src/utils.ts
3567
- import { spawnSync } from "child_process";
3568
- import { createRequire } from "module";
3569
- var require2 = createRequire(import.meta.url);
3570
- var pkg = require2("../package.json");
3571
- var VERSION = pkg.version;
3572
- var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
3573
3624
  var sleepBuf = new Int32Array(new SharedArrayBuffer(4));
3574
- var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
3575
- function colorize(code, str) {
3576
- if (NO_COLOR) return String(str);
3577
- return `${code + String(str)}\x1B[0m`;
3578
- }
3579
- var colors = {
3580
- bold: (s) => colorize("\x1B[1m", s),
3581
- red: (s) => colorize("\x1B[31m", s),
3582
- green: (s) => colorize("\x1B[32m", s),
3583
- yellow: (s) => colorize("\x1B[33m", s),
3584
- cyan: (s) => colorize("\x1B[36m", s),
3585
- gray: (s) => colorize("\x1B[90m", s)
3586
- };
3587
3625
  function sleepSync(ms) {
3588
3626
  Atomics.wait(sleepBuf, 0, 0, ms);
3589
3627
  }
@@ -3596,27 +3634,6 @@ function escapeRegExp(s) {
3596
3634
  function shellQuote(s) {
3597
3635
  return `'${s.replace(/'/g, "'\\''")}'`;
3598
3636
  }
3599
- var TimeoutError = class extends Error {
3600
- constructor() {
3601
- super("timeout");
3602
- this.name = "TimeoutError";
3603
- }
3604
- };
3605
- function withTimeout(promise, ms) {
3606
- return new Promise((resolve, reject) => {
3607
- const timer = setTimeout(() => reject(new TimeoutError()), ms);
3608
- promise.then(
3609
- (v) => {
3610
- clearTimeout(timer);
3611
- resolve(v);
3612
- },
3613
- (e) => {
3614
- clearTimeout(timer);
3615
- reject(e);
3616
- }
3617
- );
3618
- });
3619
- }
3620
3637
  function formatBytes(bytes) {
3621
3638
  if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
3622
3639
  const num = Number(bytes);
@@ -3696,90 +3713,35 @@ function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
3696
3713
  }
3697
3714
  return null;
3698
3715
  }
3699
- function isProcessRunning(pid) {
3700
- if (!pid) return false;
3701
- try {
3702
- const result = spawnSync("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: 5e3 });
3703
- return (result.stdout || "").trim().length > 0;
3704
- } catch {
3705
- return false;
3706
- }
3707
- }
3708
- function isProcessCommandMatching(pid, needle) {
3709
- if (!pid) return false;
3710
- try {
3711
- const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 5e3 });
3712
- return (result.stdout || "").includes(needle);
3713
- } catch {
3714
- return false;
3715
- }
3716
- }
3717
- function isProcessRoot(pid) {
3718
- if (!pid) return false;
3719
- try {
3720
- const result = spawnSync("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: 5e3 });
3721
- return (result.stdout || "").trim() === "0";
3722
- } catch {
3723
- return false;
3724
- }
3725
- }
3726
- function createHttpClient(options = {}) {
3727
- const { timeout = 6e4, secret } = options;
3728
- const authHeaders = secret ? { Authorization: `Bearer ${secret}` } : {};
3729
- return {
3730
- async get(url, config) {
3731
- const controller = new AbortController();
3732
- const timer = setTimeout(() => controller.abort(), timeout);
3733
- const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
3734
- try {
3735
- const response = await fetch(url, {
3736
- signal,
3737
- headers: { "User-Agent": `mihomo-cli/${VERSION}`, ...authHeaders }
3738
- });
3739
- if (!response.ok) {
3740
- const error = new Error(`HTTP ${response.status}`);
3741
- error.response = { status: response.status };
3742
- try {
3743
- error.response.data = await response.json();
3744
- } catch {
3745
- }
3746
- throw error;
3747
- }
3748
- const declaredLen = Number(response.headers.get("content-length"));
3749
- if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
3750
- throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
3751
- }
3752
- const text = await readBodyWithLimit(response, controller);
3753
- const data = config?.responseType === "json" ? JSON.parse(text) : text;
3754
- return { data, headers: response.headers, status: response.status };
3755
- } finally {
3756
- clearTimeout(timer);
3757
- }
3716
+ function levenshtein(a, b) {
3717
+ if (a === b) return 0;
3718
+ if (a.length === 0) return b.length;
3719
+ if (b.length === 0) return a.length;
3720
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
3721
+ for (let i = 1; i <= a.length; i++) {
3722
+ const curr = [i];
3723
+ for (let j = 1; j <= b.length; j++) {
3724
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
3758
3725
  }
3759
- };
3726
+ prev = curr;
3727
+ }
3728
+ return prev[b.length];
3760
3729
  }
3761
- async function readBodyWithLimit(response, controller) {
3762
- if (!response.body) return response.text();
3763
- const reader = response.body.getReader();
3764
- const chunks = [];
3765
- let total = 0;
3766
- try {
3767
- while (true) {
3768
- const { done, value } = await reader.read();
3769
- if (done) break;
3770
- if (value) {
3771
- total += value.byteLength;
3772
- if (total > MAX_RESPONSE_BYTES) {
3773
- controller.abort();
3774
- throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
3775
- }
3776
- chunks.push(value);
3777
- }
3730
+ function suggestSimilar(input, candidates) {
3731
+ const lower = input.toLowerCase();
3732
+ const scored = [];
3733
+ for (const cand of candidates) {
3734
+ const c = cand.toLowerCase();
3735
+ if (cand === input) continue;
3736
+ if (c.startsWith(lower)) {
3737
+ scored.push({ name: cand, score: 0, lenDiff: Math.abs(cand.length - input.length) });
3738
+ } else if (lower.length >= 3) {
3739
+ const d = levenshtein(lower, c);
3740
+ if (d <= 2) scored.push({ name: cand, score: d, lenDiff: Math.abs(cand.length - input.length) });
3778
3741
  }
3779
- } finally {
3780
- reader.releaseLock();
3781
3742
  }
3782
- return Buffer.concat(chunks).toString("utf8");
3743
+ scored.sort((a, b) => a.score - b.score || a.lenDiff - b.lenDiff);
3744
+ return scored.slice(0, 3).map((s) => s.name);
3783
3745
  }
3784
3746
  function normalizeMirrorUrl(val) {
3785
3747
  if (!val) return null;
@@ -3822,15 +3784,6 @@ function parseMirrorArg(args) {
3822
3784
  }
3823
3785
  return { mirror: null, isOverride: false, type: "download" };
3824
3786
  }
3825
- function isProxyValid(proxy) {
3826
- if (!proxy.name || !proxy.server || !proxy.port) return false;
3827
- if (!proxy.type) return false;
3828
- if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3829
- const pw = String(proxy.password || "");
3830
- if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
3831
- }
3832
- return true;
3833
- }
3834
3787
 
3835
3788
  // src/config.ts
3836
3789
  var SAFE_YAML_LOAD_OPTIONS = { maxAliases: 200 };
@@ -4085,7 +4038,7 @@ function getKernelVersion() {
4085
4038
  }
4086
4039
  if (kernelVersionCached) return kernelVersionCache;
4087
4040
  try {
4088
- const result = spawnSync2(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
4041
+ const result = spawnSync(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
4089
4042
  const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
4090
4043
  if (output) {
4091
4044
  const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
@@ -4179,12 +4132,12 @@ function printVersion() {
4179
4132
  }
4180
4133
 
4181
4134
  // src/daemon.ts
4182
- import { spawnSync as spawnSync4 } from "child_process";
4135
+ import { spawnSync as spawnSync3 } from "child_process";
4183
4136
  import fs6 from "fs";
4184
4137
  import path5 from "path";
4185
4138
 
4186
4139
  // src/process.ts
4187
- import { spawn, spawnSync as spawnSync3 } from "child_process";
4140
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
4188
4141
  import fs5 from "fs";
4189
4142
  import path4 from "path";
4190
4143
 
@@ -4221,6 +4174,34 @@ var SUDO_TIMEOUT_MS = 6e4;
4221
4174
  var TUN_MODE_POST_WAIT_MS = 500;
4222
4175
  var BATCH_KILL_THRESHOLD = 3;
4223
4176
  var DEFAULT_LOG_RETENTION_DAYS = 7;
4177
+ var PS_TIMEOUT_MS = 5e3;
4178
+ function isProcessRunning(pid) {
4179
+ if (!pid) return false;
4180
+ try {
4181
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
4182
+ return (result.stdout || "").trim().length > 0;
4183
+ } catch {
4184
+ return false;
4185
+ }
4186
+ }
4187
+ function isProcessCommandMatching(pid, needle) {
4188
+ if (!pid) return false;
4189
+ try {
4190
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
4191
+ return (result.stdout || "").includes(needle);
4192
+ } catch {
4193
+ return false;
4194
+ }
4195
+ }
4196
+ function isProcessRoot(pid) {
4197
+ if (!pid) return false;
4198
+ try {
4199
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
4200
+ return (result.stdout || "").trim() === "0";
4201
+ } catch {
4202
+ return false;
4203
+ }
4204
+ }
4224
4205
  var MAIN_INSTANCE_PATTERN = `${escapeRegExp(PATHS.mihomoBinary)}.*${escapeRegExp(PATHS.configFile)}`;
4225
4206
  function clearRuntime() {
4226
4207
  if (fs5.existsSync(DIRS.runtime)) {
@@ -4239,11 +4220,12 @@ function getPid() {
4239
4220
  }
4240
4221
  function isRunning() {
4241
4222
  const pid = getPid();
4242
- return pid ? isProcessRunning(pid) : false;
4223
+ if (!pid) return false;
4224
+ return isProcessRunning(pid) && isProcessCommandMatching(pid, PATHS.mihomoBinary);
4243
4225
  }
4244
4226
  function getMihomoPids() {
4245
4227
  try {
4246
- const result = spawnSync3("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
4228
+ const result = spawnSync2("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
4247
4229
  const output = (result.stdout || "").trim();
4248
4230
  if (!output) return [];
4249
4231
  return output.split("\n").filter(Boolean).map((p) => parseInt(p, 10)).filter((p) => Number.isInteger(p) && p > 0);
@@ -4283,7 +4265,7 @@ function clearPid() {
4283
4265
  if (!fs5.existsSync(PATHS.pidFile)) return;
4284
4266
  if (isPidFileOwnedByRoot()) {
4285
4267
  try {
4286
- spawnSync3("sudo", ["rm", "-f", PATHS.pidFile], { stdio: "inherit", timeout: 1e4 });
4268
+ spawnSync2("sudo", ["rm", "-f", PATHS.pidFile], { stdio: "inherit", timeout: 1e4 });
4287
4269
  } catch {
4288
4270
  }
4289
4271
  } else {
@@ -4305,14 +4287,14 @@ function killAllMihomo(forceSudo = false) {
4305
4287
  const pattern = MAIN_INSTANCE_PATTERN;
4306
4288
  if (forceSudo) {
4307
4289
  try {
4308
- spawnSync3("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
4290
+ spawnSync2("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
4309
4291
  return true;
4310
4292
  } catch {
4311
4293
  return false;
4312
4294
  }
4313
4295
  } else {
4314
4296
  try {
4315
- spawnSync3("pkill", ["-9", "-f", pattern], { timeout: 1e4 });
4297
+ spawnSync2("pkill", ["-9", "-f", pattern], { timeout: 1e4 });
4316
4298
  return true;
4317
4299
  } catch {
4318
4300
  return false;
@@ -4408,7 +4390,7 @@ exit 2
4408
4390
  }
4409
4391
  function getProcessInfo(pid) {
4410
4392
  try {
4411
- const result = spawnSync3("ps", ["-p", String(pid), "-o", "rss="], { encoding: "utf8", timeout: 5e3 });
4393
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "rss="], { encoding: "utf8", timeout: 5e3 });
4412
4394
  const psOutput = (result.stdout || "").trim();
4413
4395
  if (!psOutput) return null;
4414
4396
  const rss = parseInt(psOutput, 10);
@@ -4442,11 +4424,11 @@ async function start(mode = "mixed") {
4442
4424
  rotateAndCleanupLogs();
4443
4425
  const binary = PATHS.mihomoBinary;
4444
4426
  if (!fs5.existsSync(binary)) {
4445
- throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4427
+ throw new CliError("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4446
4428
  }
4447
4429
  const configFile = PATHS.configFile;
4448
4430
  if (!fs5.existsSync(configFile)) {
4449
- throw new Error("\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605\u5E76\u542F\u52A8");
4431
+ throw new CliError("\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605\u5E76\u542F\u52A8");
4450
4432
  }
4451
4433
  const staleState = checkStaleState();
4452
4434
  if (isTunMode) {
@@ -4523,7 +4505,7 @@ async function startTunMode(staleState) {
4523
4505
  }
4524
4506
  console.log("TUN \u6A21\u5F0F\u9700\u8981 sudo \u6743\u9650...");
4525
4507
  try {
4526
- const result = spawnSync3("sudo", [launchScript], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4508
+ const result = spawnSync2("sudo", [launchScript], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4527
4509
  if (result.error) throw result.error;
4528
4510
  if (result.status !== 0) {
4529
4511
  const err = new Error("TUN \u542F\u52A8\u811A\u672C\u6267\u884C\u5931\u8D25");
@@ -4763,7 +4745,7 @@ function runSudoScript(scriptBody, opts) {
4763
4745
  const scriptPath = path5.join(DIRS.runtime, opts.file);
4764
4746
  fs6.writeFileSync(scriptPath, scriptBody, { mode: 448 });
4765
4747
  try {
4766
- const result = spawnSync4("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4748
+ const result = spawnSync3("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4767
4749
  if (result.error) throw result.error;
4768
4750
  if (result.status !== 0) {
4769
4751
  if (result.status === 1) {
@@ -4797,10 +4779,10 @@ function isDaemonRunning(status) {
4797
4779
  }
4798
4780
  function enableDaemon() {
4799
4781
  if (!fs6.existsSync(PATHS.mihomoBinary)) {
4800
- throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4782
+ throw new CliError("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4801
4783
  }
4802
4784
  if (!fs6.existsSync(PATHS.configFile)) {
4803
- throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4785
+ throw new CliError("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4804
4786
  }
4805
4787
  ensureDirs();
4806
4788
  const stagePath = path5.join(DIRS.runtime, "daemon.plist.stage");
@@ -4905,25 +4887,86 @@ async function restartDaemon() {
4905
4887
  cleanupOldLogs();
4906
4888
  }
4907
4889
 
4908
- // src/subscription.ts
4909
- function isGithubUrl(url) {
4910
- const githubRe = /github\.com|raw\.githubusercontent\.com/i;
4911
- if (isMultiUrl(url)) {
4912
- return splitUrls(url).every((u) => githubRe.test(u));
4913
- }
4914
- return githubRe.test(url);
4915
- }
4916
- function getDefaultUpdateInterval(url) {
4917
- return isGithubUrl(url) ? DEFAULT_UPDATE_INTERVAL_HOURS_GITHUB : DEFAULT_UPDATE_INTERVAL_HOURS;
4918
- }
4919
- function resolveUpdateInterval(url, cachedInterval) {
4920
- return cachedInterval && cachedInterval > 0 ? cachedInterval : getDefaultUpdateInterval(url);
4921
- }
4922
- var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4923
- function isMultiUrl(url) {
4924
- return url.includes(",");
4890
+ // src/http.ts
4891
+ var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
4892
+ function createHttpClient(options = {}) {
4893
+ const { timeout = 6e4, secret } = options;
4894
+ const authHeaders = secret ? { Authorization: `Bearer ${secret}` } : {};
4895
+ return {
4896
+ async get(url, config) {
4897
+ const controller = new AbortController();
4898
+ const timer = setTimeout(() => controller.abort(), timeout);
4899
+ const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
4900
+ try {
4901
+ const response = await fetch(url, {
4902
+ signal,
4903
+ headers: { "User-Agent": `mihomo-cli/${VERSION}`, ...authHeaders }
4904
+ });
4905
+ if (!response.ok) {
4906
+ const error = new Error(`HTTP ${response.status}`);
4907
+ error.response = { status: response.status };
4908
+ try {
4909
+ error.response.data = await response.json();
4910
+ } catch {
4911
+ }
4912
+ throw error;
4913
+ }
4914
+ const declaredLen = Number(response.headers.get("content-length"));
4915
+ if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
4916
+ throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
4917
+ }
4918
+ const text = await readBodyWithLimit(response, controller);
4919
+ const data = config?.responseType === "json" ? JSON.parse(text) : text;
4920
+ return { data, headers: response.headers, status: response.status };
4921
+ } finally {
4922
+ clearTimeout(timer);
4923
+ }
4924
+ }
4925
+ };
4925
4926
  }
4926
- function isValidHttpUrl(url) {
4927
+ async function readBodyWithLimit(response, controller) {
4928
+ if (!response.body) return response.text();
4929
+ const reader = response.body.getReader();
4930
+ const chunks = [];
4931
+ let total = 0;
4932
+ try {
4933
+ while (true) {
4934
+ const { done, value } = await reader.read();
4935
+ if (done) break;
4936
+ if (value) {
4937
+ total += value.byteLength;
4938
+ if (total > MAX_RESPONSE_BYTES) {
4939
+ controller.abort();
4940
+ throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
4941
+ }
4942
+ chunks.push(value);
4943
+ }
4944
+ }
4945
+ } finally {
4946
+ reader.releaseLock();
4947
+ }
4948
+ return Buffer.concat(chunks).toString("utf8");
4949
+ }
4950
+
4951
+ // src/subscription.ts
4952
+ function isGithubUrl(url) {
4953
+ const githubRe = /github\.com|raw\.githubusercontent\.com/i;
4954
+ if (isMultiUrl(url)) {
4955
+ return splitUrls(url).every((u) => githubRe.test(u));
4956
+ }
4957
+ return githubRe.test(url);
4958
+ }
4959
+ function getDefaultUpdateInterval(url) {
4960
+ return isGithubUrl(url) ? DEFAULT_UPDATE_INTERVAL_HOURS_GITHUB : DEFAULT_UPDATE_INTERVAL_HOURS;
4961
+ }
4962
+ function resolveUpdateInterval(url, cachedInterval) {
4963
+ return cachedInterval && cachedInterval > 0 ? cachedInterval : getDefaultUpdateInterval(url);
4964
+ }
4965
+ var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4966
+ function isMultiUrl(url) {
4967
+ return url.includes(",");
4968
+ }
4969
+ function isValidHttpUrl(url) {
4927
4970
  try {
4928
4971
  const u = new URL(url.trim());
4929
4972
  return u.protocol === "http:" || u.protocol === "https:";
@@ -4937,7 +4980,7 @@ function splitUrls(url) {
4937
4980
  function loadSubscriptionConfig(subName) {
4938
4981
  const rawContent = readSubscriptionRawConfig(subName);
4939
4982
  if (!rawContent) {
4940
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4983
+ throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u66F4\u65B0\u8BA2\u9605\uFF08mihomo sub update ${subName}\uFF09`);
4941
4984
  }
4942
4985
  const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4943
4986
  return {
@@ -5016,6 +5059,13 @@ function getActiveSubscription() {
5016
5059
  }
5017
5060
  return subs[0];
5018
5061
  }
5062
+ function requireActiveSubscription(emptyMsg = "\u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605") {
5063
+ const sub = getActiveSubscription();
5064
+ if (!sub) {
5065
+ throw new CliError(emptyMsg);
5066
+ }
5067
+ return sub;
5068
+ }
5019
5069
  function findSubscriptionFuzzy(subs, pattern) {
5020
5070
  const lowerPattern = pattern.toLowerCase();
5021
5071
  const exact = [];
@@ -5037,14 +5087,15 @@ function findSubscriptionFuzzy(subs, pattern) {
5037
5087
  }
5038
5088
  function pickSingleSubscription(subs, pattern) {
5039
5089
  if (subs.length === 0) {
5040
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${pattern}" \u7684\u8BA2\u9605`);
5041
- process.exit(1);
5090
+ throw new CliError(`\u672A\u627E\u5230\u5339\u914D "${pattern}" \u7684\u8BA2\u9605`);
5042
5091
  }
5043
5092
  if (subs.length === 1) return subs[0];
5044
- console.error("\u9519\u8BEF: \u5339\u914D\u5230\u591A\u4E2A\u8BA2\u9605\uFF0C\u8BF7\u66F4\u7CBE\u786E\u6307\u5B9A");
5045
- console.log("\n\u5339\u914D\u7684\u8BA2\u9605:");
5046
- for (const s of subs) console.log(` ${s.name}`);
5047
- process.exit(1);
5093
+ throw new CliError("\u5339\u914D\u5230\u591A\u4E2A\u8BA2\u9605\uFF0C\u8BF7\u66F4\u7CBE\u786E\u6307\u5B9A", {
5094
+ hint: ["", "\u5339\u914D\u7684\u8BA2\u9605:", ...subs.map((s) => ` ${s.name}`)]
5095
+ });
5096
+ }
5097
+ function resolveSubscription(subs, pattern) {
5098
+ return pickSingleSubscription(findSubscriptionFuzzy(subs, pattern), pattern);
5048
5099
  }
5049
5100
  async function downloadSubscription(url, subName = "default", signal, persist = true) {
5050
5101
  let response;
@@ -5145,7 +5196,7 @@ async function downloadMergedSubscription(urls, subName, signal, persist = true)
5145
5196
  function prepareConfigForStart(mode, subName = "default") {
5146
5197
  const rawContent = readSubscriptionRawConfig(subName);
5147
5198
  if (!rawContent) {
5148
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
5199
+ throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
5149
5200
  }
5150
5201
  const subUrl = getSubscriptions().find((s) => s.name === subName)?.url;
5151
5202
  const buildResult = buildConfig(rawContent, mode, { subName, subUrl });
@@ -5414,144 +5465,461 @@ async function autoCleanSubscription(subName, options = {}) {
5414
5465
  return { summary, removedProxies, updatedGroups, removedGroups, skipped };
5415
5466
  }
5416
5467
 
5417
- // src/commands/daemon.ts
5418
- function printDaemonStatus() {
5419
- const status = getDaemonStatus();
5420
- const stateText = status.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
5421
- console.log(`${colors.gray("\u4FDD\u6D3B: ")}${stateText}`);
5422
- if (status.enabled) {
5423
- const runText = isDaemonRunning(status) ? colors.green(`\u8FD0\u884C\u4E2D (PID ${status.pid})`) : colors.yellow("\u672A\u8FD0\u884C");
5424
- console.log(`${colors.gray("\u5185\u6838: ")}${runText}`);
5425
- }
5426
- console.log("");
5427
- if (status.enabled) {
5428
- console.log("\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
5429
- } else {
5430
- console.log("\u5F00\u542F\u4FDD\u6D3B: mihomo daemon on");
5431
- console.log(colors.gray(" \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF08\u4EC5 Mixed \u6A21\u5F0F\uFF09"));
5468
+ // src/runtime.ts
5469
+ function getRuntimeMode() {
5470
+ if (isDaemonEnabled()) return "mixed";
5471
+ return getConfigInfo()?.tun ? "tun" : "mixed";
5472
+ }
5473
+ function getRunningState() {
5474
+ if (isDaemonEnabled()) {
5475
+ const daemon = getDaemonStatus();
5476
+ return { running: isDaemonRunning(daemon), pid: daemon.pid, daemon: true };
5432
5477
  }
5433
- console.log("");
5478
+ const status = getStatus();
5479
+ return { running: status.running, pid: status.pid, daemon: false };
5434
5480
  }
5435
- async function cmdDaemon(args) {
5436
- const action = args?.[1];
5437
- if (action === "on" || action === "enable") {
5438
- if (!hasKernel()) {
5439
- console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5440
- process.exit(1);
5441
- }
5442
- const sub = getActiveSubscription();
5443
- if (!sub) {
5444
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
5445
- process.exit(1);
5446
- }
5447
- let configInfo;
5448
- try {
5449
- configInfo = prepareConfigForStart("mixed", sub.name);
5450
- } catch (e) {
5451
- console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
5452
- process.exit(1);
5453
- }
5454
- console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u5B89\u88C5\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1\uFF08LaunchDaemon\uFF09"));
5455
- 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"));
5456
- try {
5457
- enableDaemon();
5458
- } catch (e) {
5459
- console.error(`${colors.red("\u542F\u7528\u4FDD\u6D3B\u5931\u8D25:")} ${e.message}`);
5460
- process.exit(1);
5461
- }
5462
- console.log(`${colors.green("\u5DF2\u542F\u7528\u4FDD\u6D3B")} \xB7 ${sub.name} \xB7 ${formatProxySummary(configInfo)}`);
5463
- console.log(colors.gray("\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF0C\u4EE3\u7406\u5C06\u5728\u540E\u53F0\u5E38\u9A7B"));
5464
- console.log("");
5481
+ function isRestartNeededOnChange() {
5482
+ return isDaemonEnabled() || getStatus().running;
5483
+ }
5484
+ async function launchOrRestart(mode) {
5485
+ if (isDaemonEnabled()) {
5486
+ await restartDaemon();
5465
5487
  await sleep(DAEMON_BOOT_WAIT_MS);
5466
- printDaemonStatus();
5467
- return;
5488
+ return getDaemonStatus().pid;
5489
+ }
5490
+ const result = await start(mode);
5491
+ return result.pid;
5492
+ }
5493
+
5494
+ // src/progress.ts
5495
+ var IS_TTY = process.stdout.isTTY === true;
5496
+ var BAR_WIDTH = 20;
5497
+ function createProgressPrinter(totalRounds = 1) {
5498
+ let alive = 0;
5499
+ let dead = 0;
5500
+ const resultMap = /* @__PURE__ */ new Map();
5501
+ function render(done, total) {
5502
+ if (!IS_TTY) return;
5503
+ const pct = Math.round(done / total * 100);
5504
+ const filled = Math.round(done / total * BAR_WIDTH);
5505
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
5506
+ process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
5468
5507
  }
5469
- if (action === "off" || action === "disable") {
5470
- if (!isDaemonEnabled()) {
5471
- console.log("\u4FDD\u6D3B\u5DF2\u662F\u5173\u95ED\u72B6\u6001");
5508
+ return {
5509
+ onResult(result, index, total, round = 1) {
5510
+ if (resultMap.size === 0 && totalRounds > 1) {
5511
+ console.log(`--- \u7B2C 1 \u8F6E\u6D4B\u8BD5 (${total} \u4E2A\u8282\u70B9) ---`);
5512
+ }
5513
+ const prev = resultMap.get(result.name);
5514
+ if (prev) {
5515
+ if (prev.result.delay !== null) alive--;
5516
+ else dead--;
5517
+ }
5518
+ if (result.delay !== null) alive++;
5519
+ else dead++;
5520
+ resultMap.set(result.name, { result, round });
5521
+ render(index + 1, total);
5522
+ },
5523
+ onRetryRound(round, count) {
5524
+ if (IS_TTY) {
5525
+ process.stdout.write("\n");
5526
+ }
5527
+ console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
5528
+ alive = 0;
5529
+ dead = 0;
5530
+ },
5531
+ finish() {
5532
+ if (IS_TTY) {
5533
+ process.stdout.write("\n");
5534
+ }
5535
+ console.log("");
5536
+ if (!IS_TTY) return;
5537
+ const entries = [...resultMap.values()];
5538
+ entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
5539
+ const total = entries.length;
5540
+ console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
5541
+ for (let i = 0; i < entries.length; i++) {
5542
+ const { result, round } = entries[i];
5543
+ const prefix = `[${i + 1}/${total}]`;
5544
+ if (result.delay !== null) {
5545
+ const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
5546
+ const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
5547
+ console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
5548
+ } else {
5549
+ console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
5550
+ }
5551
+ }
5472
5552
  console.log("");
5473
- printDaemonStatus();
5474
- return;
5475
5553
  }
5476
- console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u79FB\u9664\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1"));
5477
- try {
5478
- disableDaemon();
5479
- } catch (e) {
5480
- console.error(`${colors.red("\u5173\u95ED\u4FDD\u6D3B\u5931\u8D25:")} ${e.message}`);
5481
- process.exit(1);
5554
+ };
5555
+ }
5556
+ function formatCleanSummary(result) {
5557
+ const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
5558
+ if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
5559
+ if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
5560
+ return parts.join(", ");
5561
+ }
5562
+ function formatTestSummary(summary) {
5563
+ return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
5564
+ }
5565
+
5566
+ // src/commands/status.ts
5567
+ function printStatus() {
5568
+ const status = getStatus();
5569
+ const state = getRunningState();
5570
+ const info = getConfigInfo();
5571
+ const overwriteEnabled = isOverwriteEnabled();
5572
+ const overwriteFiles = listOverwriteFile().files;
5573
+ const activeSub = getActiveSubscription();
5574
+ const { running, pid, daemon: daemonManaged } = state;
5575
+ console.log("");
5576
+ let modeLabel = "";
5577
+ if (info) {
5578
+ modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
5579
+ }
5580
+ const statusText = running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
5581
+ console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}${modeLabel}`);
5582
+ console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
5583
+ if (pid) {
5584
+ console.log(`${colors.gray("PID: ")}${pid}`);
5585
+ if (!daemonManaged && status.processInfo) {
5586
+ console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
5482
5587
  }
5483
- console.log(`${colors.green("\u5DF2\u5173\u95ED\u4FDD\u6D3B")}\uFF0C\u4EE3\u7406\u5DF2\u505C\u6B62`);
5484
- console.log(colors.gray("\u91CD\u65B0\u542F\u7528: mihomo daemon on"));
5485
- console.log("");
5486
- return;
5487
5588
  }
5488
- if (action !== void 0 && action !== "status") {
5489
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684 daemon \u5B50\u547D\u4EE4: ${action}`);
5490
- console.log("");
5491
- console.log("\u53EF\u7528\u5B50\u547D\u4EE4: on, off, status");
5492
- process.exit(1);
5589
+ if (info) {
5590
+ if (info.tun) {
5591
+ const extra = info.mixedPort ? `\uFF0C\u53E6\u76D1\u542C ${info.mixedPort}` : "";
5592
+ console.log(`${colors.gray("\u7AEF\u53E3: ")}TUN \u63A5\u7BA1${extra}`);
5593
+ } else if (info.mixedPort) {
5594
+ console.log(`${colors.gray("\u7AEF\u53E3: ")}${info.mixedPort}`);
5595
+ } else {
5596
+ const ports = [];
5597
+ if (info.httpPort) ports.push(`HTTP:${info.httpPort}`);
5598
+ if (info.socksPort) ports.push(`SOCKS:${info.socksPort}`);
5599
+ console.log(`${colors.gray("\u7AEF\u53E3: ")}${ports.length > 0 ? ports.join(", ") : "\u672A\u77E5"}`);
5600
+ }
5493
5601
  }
5494
- console.log("");
5495
- printDaemonStatus();
5496
- }
5497
-
5498
- // src/commands/directory.ts
5499
- function cmdDirectory(args) {
5500
- const action = args?.[1];
5501
- if (action === "open") {
5502
- const target = args[2];
5503
- if (!target || target === "root") {
5504
- console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
5505
- const success = openUrl(USER_DATA_DIR);
5506
- if (!success) {
5507
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
5508
- }
5509
- return;
5602
+ if (activeSub) {
5603
+ let subLine = `${colors.gray("\u8BA2\u9605: ")}${activeSub.name}`;
5604
+ if (info) {
5605
+ subLine += ` (${formatProxySummary(info)})`;
5510
5606
  }
5511
- const key = target.toLowerCase();
5512
- const targetInfo = Object.hasOwn(DIRECTORY_TARGETS, key) ? DIRECTORY_TARGETS[key] : void 0;
5513
- if (targetInfo) {
5514
- const targetPath = targetInfo.path || USER_DATA_DIR;
5515
- console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
5516
- const success = openUrl(targetPath);
5517
- if (!success) {
5518
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${targetPath}`);
5607
+ console.log(subLine);
5608
+ const cached = getSubscriptionsWithCache().find((s) => s.name === activeSub.name);
5609
+ if (cached && (cached.download !== void 0 || cached.total !== void 0)) {
5610
+ const used = (cached.upload || 0) + (cached.download || 0);
5611
+ let trafficLine = `${colors.gray("\u6D41\u91CF: ")}${formatBytes(used)} / ${formatBytes(cached.total)}`;
5612
+ if (cached.total && cached.total > 0) {
5613
+ trafficLine += ` (${Math.min(used / cached.total * 100, 100).toFixed(1)}%)`;
5519
5614
  }
5520
- return;
5615
+ console.log(trafficLine);
5521
5616
  }
5522
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`);
5523
- console.log("");
5524
- console.log("\u53EF\u7528\u76EE\u6807:");
5525
- console.log(" root (\u9ED8\u8BA4) \u6839\u76EE\u5F55");
5526
- for (const [key2, val] of Object.entries(DIRECTORY_TARGETS)) {
5527
- if (key2 !== "root") {
5528
- console.log(` ${key2.padEnd(14)}${val.label}`);
5529
- }
5617
+ if (cached?.expire !== void 0) {
5618
+ console.log(`${colors.gray("\u5230\u671F: ")}${formatTimestamp(cached.expire)}`);
5530
5619
  }
5531
- console.log("");
5532
- process.exit(1);
5620
+ } else {
5621
+ console.log(`${colors.gray("\u8BA2\u9605: ")}\u672A\u914D\u7F6E`);
5622
+ }
5623
+ if (overwriteEnabled && overwriteFiles.length > 0) {
5624
+ const names = overwriteFiles.map((f) => f.name.replace(/^overwrite\.?/, "").replace(/\.ya?ml$/, "") || "\u4E3B\u6587\u4EF6").join(", ");
5625
+ console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (${names})`);
5626
+ } else if (overwriteEnabled) {
5627
+ console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (\u65E0\u6587\u4EF6)`);
5628
+ } else {
5629
+ console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
5630
+ }
5631
+ if (isDaemonEnabled()) {
5632
+ console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
5533
5633
  }
5534
5634
  console.log("");
5535
- console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
5536
- console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
5537
- console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
5538
- console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
5539
- console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
5540
- console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
5541
- console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
5542
- console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
5543
- console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
5544
- console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5545
- console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5546
- console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
5547
- console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
5548
- console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
5549
- console.log("");
5550
- console.log("\u6253\u5F00\u76EE\u5F55:");
5551
- console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
5552
- console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
5553
- console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
5554
- console.log(" mihomo dir open data \u6253\u5F00 mihomo \u6570\u636E\u76EE\u5F55");
5635
+ }
5636
+
5637
+ // src/commands/stop.ts
5638
+ function handleStopResult(result) {
5639
+ if (result.remaining && result.remaining.length > 0) {
5640
+ throw new CliError(result.remaining.join(", "), { label: "\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62", hint: "\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo" });
5641
+ }
5642
+ }
5643
+ async function cmdStop() {
5644
+ if (isDaemonEnabled()) {
5645
+ console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u4EE3\u7406\u7531 launchd \u6258\u7BA1"));
5646
+ console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
5647
+ return;
5648
+ }
5649
+ const pids = getMihomoPids();
5650
+ if (pids.length === 0) {
5651
+ console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
5652
+ return;
5653
+ }
5654
+ console.log(`\u505C\u6B62 ${pids.length} \u4E2A\u8FDB\u7A0B...`);
5655
+ handleStopResult(stop());
5656
+ console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
5657
+ }
5658
+
5659
+ // src/commands/start.ts
5660
+ async function cmdStart(args) {
5661
+ const modeToken = args[1] && !args[1].startsWith("-") ? args[1].toLowerCase() : void 0;
5662
+ if (modeToken !== void 0 && modeToken !== "tun" && modeToken !== "mixed") {
5663
+ throw new CliError(`\u672A\u77E5\u7684\u542F\u52A8\u6A21\u5F0F: ${args[1]}`, { hint: "\u7528\u6CD5: mihomo start [tun|mixed]\uFF08\u9ED8\u8BA4 mixed\uFF09" });
5664
+ }
5665
+ const targetMode = modeToken === "tun" ? "tun" : "mixed";
5666
+ if (!hasKernel()) {
5667
+ throw new CliError('\u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5668
+ }
5669
+ const daemonEnabled = isDaemonEnabled();
5670
+ if (targetMode === "tun" && daemonEnabled) {
5671
+ throw new CliError("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF08\u4EC5\u652F\u6301 Mixed \u6A21\u5F0F\uFF09\uFF0C\u65E0\u6CD5\u542F\u52A8 TUN", { hint: "\u8BF7\u5148\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off" });
5672
+ }
5673
+ const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
5674
+ const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
5675
+ const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
5676
+ const skipUpdate = hasFlag(args, "-s", "--no-update");
5677
+ const skipClean = hasFlag(args, "--no-clean");
5678
+ const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
5679
+ const sub = requireActiveSubscription("\u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
5680
+ if (!skipUpdate) {
5681
+ await autoUpdateStaleSubscription({ timeout: updateTimeout });
5682
+ }
5683
+ if (!daemonEnabled) {
5684
+ if (hasRootResidue()) {
5685
+ throw new CliError("\u5B58\u5728\u9700\u8981 root \u6743\u9650\u6E05\u7406\u7684\u6B8B\u7559\u8FDB\u7A0B/\u6587\u4EF6", {
5686
+ hint: [`\u8BF7\u5148\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo && sudo rm -f ${PATHS.pidFile}`, "\u6216\u5207\u6362\u5230 TUN \u6A21\u5F0F\u542F\u52A8\uFF08\u81EA\u52A8\u6E05\u7406\uFF09: mihomo start tun"]
5687
+ });
5688
+ }
5689
+ const status = getStatus();
5690
+ const hasProcess = status.running || status.allProcesses.length > 0;
5691
+ if (hasProcess) {
5692
+ const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
5693
+ console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
5694
+ }
5695
+ handleStopResult(stop());
5696
+ if (hasProcess) {
5697
+ console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
5698
+ `);
5699
+ }
5700
+ }
5701
+ let configInfo;
5702
+ try {
5703
+ configInfo = prepareConfigForStart(targetMode, sub.name);
5704
+ } catch (e) {
5705
+ if (e instanceof CliError) throw e;
5706
+ throw new CliError(e.message, { label: "\u914D\u7F6E\u9519\u8BEF" });
5707
+ }
5708
+ const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
5709
+ console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
5710
+ try {
5711
+ const pid = await launchOrRestart(targetMode);
5712
+ const label = daemonEnabled ? "\u5DF2\u542F\u52A8 (\u4FDD\u6D3B)" : "\u5DF2\u542F\u52A8";
5713
+ console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""}`);
5714
+ } catch (e) {
5715
+ if (e instanceof CliError) throw e;
5716
+ const lines = e.message.split("\n");
5717
+ throw new CliError(lines[0], { label: "\u542F\u52A8\u5931\u8D25", hint: lines.slice(1) });
5718
+ }
5719
+ const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
5720
+ if (!skipClean && configInfo.proxies > cleanThreshold) {
5721
+ const cache = readSubscriptionCache();
5722
+ const lastCleanAt = cache[sub.name]?.last_auto_clean_at;
5723
+ const withinCooldown = !!lastCleanAt && Date.now() - new Date(lastCleanAt).getTime() < AUTO_CLEAN_COOLDOWN_HOURS * 60 * 60 * 1e3;
5724
+ if (!withinCooldown) {
5725
+ console.log("");
5726
+ console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${cleanThreshold}\uFF0C\u81EA\u52A8\u6E05\u7406\uFF08${AUTO_CLEAN_COOLDOWN_HOURS}h \u5185\u4EC5\u4E00\u6B21\uFF0C--no-clean \u8DF3\u8FC7\uFF09...`);
5727
+ console.log("");
5728
+ await sleep(1e3);
5729
+ const progress = createProgressPrinter(rounds);
5730
+ const cleanResult = await autoCleanSubscription(sub.name, {
5731
+ timeout,
5732
+ concurrency,
5733
+ rounds,
5734
+ onResult: progress.onResult,
5735
+ onRetryRound: progress.onRetryRound
5736
+ });
5737
+ progress.finish();
5738
+ console.log(formatTestSummary(cleanResult.summary));
5739
+ if (cleanResult.skipped) {
5740
+ 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"));
5741
+ } else if (cleanResult.removedProxies > 0) {
5742
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
5743
+ console.log("");
5744
+ console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
5745
+ if (!daemonEnabled) handleStopResult(stop());
5746
+ try {
5747
+ configInfo = prepareConfigForStart(targetMode, sub.name);
5748
+ const pid = await launchOrRestart(targetMode);
5749
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
5750
+ } catch (e) {
5751
+ if (e instanceof CliError) throw e;
5752
+ throw new CliError(e.message.split("\n")[0], { label: "\u91CD\u542F\u5931\u8D25" });
5753
+ }
5754
+ }
5755
+ saveSubscriptionCache(sub.name, { last_auto_clean_at: (/* @__PURE__ */ new Date()).toISOString() });
5756
+ }
5757
+ }
5758
+ printStatus();
5759
+ }
5760
+
5761
+ // src/commands/shared.ts
5762
+ async function dispatchSubcommand(args, table, options) {
5763
+ const action = args[1];
5764
+ if (action) {
5765
+ const cmd = table.find((c) => c.name === action || c.aliases?.includes(action));
5766
+ if (cmd) return cmd.handler(args);
5767
+ if (options.onUnknown) return options.onUnknown(action);
5768
+ }
5769
+ return options.fallback(args);
5770
+ }
5771
+ function requireRunning() {
5772
+ const state = getRunningState();
5773
+ if (!state.running) {
5774
+ const hint = state.daemon ? "mihomo daemon on" : "mihomo start";
5775
+ throw new CliError(`mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (${hint})`);
5776
+ }
5777
+ }
5778
+ async function restartToApply(args) {
5779
+ if (!isRestartNeededOnChange()) return false;
5780
+ const currentMode = getRuntimeMode();
5781
+ console.log("");
5782
+ await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
5783
+ return true;
5784
+ }
5785
+
5786
+ // src/commands/daemon.ts
5787
+ function printDaemonStatus() {
5788
+ const status = getDaemonStatus();
5789
+ const stateText = status.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
5790
+ console.log(`${colors.gray("\u4FDD\u6D3B: ")}${stateText}`);
5791
+ if (status.enabled) {
5792
+ const runText = isDaemonRunning(status) ? colors.green(`\u8FD0\u884C\u4E2D (PID ${status.pid})`) : colors.yellow("\u672A\u8FD0\u884C");
5793
+ console.log(`${colors.gray("\u5185\u6838: ")}${runText}`);
5794
+ }
5795
+ console.log("");
5796
+ if (status.enabled) {
5797
+ console.log("\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
5798
+ } else {
5799
+ console.log("\u5F00\u542F\u4FDD\u6D3B: mihomo daemon on");
5800
+ console.log(colors.gray(" \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF08\u4EC5 Mixed \u6A21\u5F0F\uFF09"));
5801
+ }
5802
+ console.log("");
5803
+ }
5804
+ async function daemonOn() {
5805
+ if (!hasKernel()) {
5806
+ throw new CliError('\u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5807
+ }
5808
+ const sub = requireActiveSubscription("\u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
5809
+ let configInfo;
5810
+ try {
5811
+ configInfo = prepareConfigForStart("mixed", sub.name);
5812
+ } catch (e) {
5813
+ if (e instanceof CliError) throw e;
5814
+ throw new CliError(e.message, { label: "\u914D\u7F6E\u9519\u8BEF" });
5815
+ }
5816
+ console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u5B89\u88C5\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1\uFF08LaunchDaemon\uFF09"));
5817
+ 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"));
5818
+ try {
5819
+ enableDaemon();
5820
+ } catch (e) {
5821
+ if (e instanceof CliError) throw e;
5822
+ throw new CliError(e.message, { label: "\u542F\u7528\u4FDD\u6D3B\u5931\u8D25" });
5823
+ }
5824
+ console.log(`${colors.green("\u5DF2\u542F\u7528\u4FDD\u6D3B")} \xB7 ${sub.name} \xB7 ${formatProxySummary(configInfo)}`);
5825
+ console.log(colors.gray("\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF0C\u4EE3\u7406\u5C06\u5728\u540E\u53F0\u5E38\u9A7B"));
5826
+ console.log("");
5827
+ await sleep(DAEMON_BOOT_WAIT_MS);
5828
+ printDaemonStatus();
5829
+ }
5830
+ function daemonOff() {
5831
+ if (!isDaemonEnabled()) {
5832
+ console.log("\u4FDD\u6D3B\u5DF2\u662F\u5173\u95ED\u72B6\u6001");
5833
+ console.log("");
5834
+ printDaemonStatus();
5835
+ return;
5836
+ }
5837
+ console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u79FB\u9664\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1"));
5838
+ try {
5839
+ disableDaemon();
5840
+ } catch (e) {
5841
+ if (e instanceof CliError) throw e;
5842
+ throw new CliError(e.message, { label: "\u5173\u95ED\u4FDD\u6D3B\u5931\u8D25" });
5843
+ }
5844
+ console.log(`${colors.green("\u5DF2\u5173\u95ED\u4FDD\u6D3B")}\uFF0C\u4EE3\u7406\u5DF2\u505C\u6B62`);
5845
+ console.log(colors.gray("\u91CD\u65B0\u542F\u7528: mihomo daemon on"));
5846
+ console.log("");
5847
+ }
5848
+ function printStatusView() {
5849
+ console.log("");
5850
+ printDaemonStatus();
5851
+ }
5852
+ var SUBCOMMANDS = [
5853
+ { name: "on", aliases: ["enable"], handler: daemonOn },
5854
+ { name: "off", aliases: ["disable"], handler: daemonOff },
5855
+ { name: "status", handler: printStatusView }
5856
+ ];
5857
+ async function cmdDaemon(args) {
5858
+ await dispatchSubcommand(args, SUBCOMMANDS, {
5859
+ // 无 action → 显示状态;未知 action → 报错
5860
+ fallback: printStatusView,
5861
+ onUnknown: (action) => {
5862
+ const names = SUBCOMMANDS.flatMap((c) => [c.name, ...c.aliases ?? []]);
5863
+ const suggestion = suggestSimilar(action, names);
5864
+ throw new CliError(`\u672A\u77E5\u7684 daemon \u5B50\u547D\u4EE4: ${action}`, {
5865
+ hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "", "\u53EF\u7528\u5B50\u547D\u4EE4: on, off, status"]
5866
+ });
5867
+ }
5868
+ });
5869
+ }
5870
+
5871
+ // src/commands/directory.ts
5872
+ function openDirectory(args) {
5873
+ const target = args[2];
5874
+ if (!target || target === "root") {
5875
+ console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
5876
+ const success = openUrl(USER_DATA_DIR);
5877
+ if (!success) {
5878
+ console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
5879
+ }
5880
+ return;
5881
+ }
5882
+ const key = target.toLowerCase();
5883
+ const targetInfo = Object.hasOwn(DIRECTORY_TARGETS, key) ? DIRECTORY_TARGETS[key] : void 0;
5884
+ if (targetInfo) {
5885
+ const targetPath = targetInfo.path || USER_DATA_DIR;
5886
+ console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
5887
+ const success = openUrl(targetPath);
5888
+ if (!success) {
5889
+ console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${targetPath}`);
5890
+ }
5891
+ return;
5892
+ }
5893
+ const hint = ["", "\u53EF\u7528\u76EE\u6807:", " root (\u9ED8\u8BA4) \u6839\u76EE\u5F55"];
5894
+ for (const [k, val] of Object.entries(DIRECTORY_TARGETS)) {
5895
+ if (k !== "root") {
5896
+ hint.push(` ${k.padEnd(14)}${val.label}`);
5897
+ }
5898
+ }
5899
+ throw new CliError(`\u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`, { hint });
5900
+ }
5901
+ function printDirectoryInfo() {
5902
+ console.log("");
5903
+ console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
5904
+ console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
5905
+ console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
5906
+ console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
5907
+ console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
5908
+ console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
5909
+ console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
5910
+ console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
5911
+ console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
5912
+ console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5913
+ console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
5914
+ console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
5915
+ console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
5916
+ console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
5917
+ console.log("");
5918
+ console.log("\u6253\u5F00\u76EE\u5F55:");
5919
+ console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
5920
+ console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
5921
+ console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
5922
+ console.log(" mihomo dir open data \u6253\u5F00 mihomo \u6570\u636E\u76EE\u5F55");
5555
5923
  console.log(" mihomo dir open runtime \u6253\u5F00\u8FD0\u884C\u65F6\u76EE\u5F55");
5556
5924
  console.log(" mihomo dir open kernel \u6253\u5F00\u5185\u6838\u76EE\u5F55");
5557
5925
  console.log("");
@@ -5559,9 +5927,13 @@ function cmdDirectory(args) {
5559
5927
  console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
5560
5928
  console.log("");
5561
5929
  }
5930
+ var SUBCOMMANDS2 = [{ name: "open", handler: openDirectory }];
5931
+ function cmdDirectory(args) {
5932
+ void dispatchSubcommand(args, SUBCOMMANDS2, { fallback: printDirectoryInfo });
5933
+ }
5562
5934
 
5563
5935
  // src/kernel.ts
5564
- import { spawnSync as spawnSync5 } from "child_process";
5936
+ import { spawnSync as spawnSync4 } from "child_process";
5565
5937
  import fs7 from "fs";
5566
5938
  import path6 from "path";
5567
5939
 
@@ -5726,7 +6098,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5726
6098
  if (progressCallback) {
5727
6099
  progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
5728
6100
  }
5729
- const curlResult = spawnSync5(
6101
+ const curlResult = spawnSync4(
5730
6102
  "curl",
5731
6103
  ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
5732
6104
  { stdio: "inherit" }
@@ -5754,7 +6126,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5754
6126
  let extractedBinary = null;
5755
6127
  try {
5756
6128
  if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5757
- const listResult = spawnSync5("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
6129
+ const listResult = spawnSync4("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
5758
6130
  if (listResult.error) throw listResult.error;
5759
6131
  if (listResult.status !== 0) throw new Error(`tar \u5217\u8868\u9000\u51FA\u7801 ${listResult.status}`);
5760
6132
  const entries = (listResult.stdout || "").split("\n").filter(Boolean);
@@ -5763,13 +6135,13 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5763
6135
  throw new Error(`\u5F52\u6863\u542B\u975E\u6CD5\u8DEF\u5F84\u6761\u76EE: ${entry}`);
5764
6136
  }
5765
6137
  }
5766
- const tarResult = spawnSync5("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
6138
+ const tarResult = spawnSync4("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
5767
6139
  if (tarResult.error) throw tarResult.error;
5768
6140
  if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
5769
6141
  } else if (tempPath.endsWith(".gz")) {
5770
6142
  const baseName = path6.basename(tempPath, ".gz");
5771
6143
  const outputPath = path6.join(extractPath, baseName);
5772
- const gzipResult = spawnSync5("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
6144
+ const gzipResult = spawnSync4("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
5773
6145
  if (gzipResult.error) throw gzipResult.error;
5774
6146
  if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
5775
6147
  fs7.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
@@ -5805,7 +6177,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5805
6177
  if (progressCallback) {
5806
6178
  progressCallback("\u6821\u9A8C\u5185\u6838...");
5807
6179
  }
5808
- const check = spawnSync5(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
6180
+ const check = spawnSync4(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
5809
6181
  const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
5810
6182
  if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
5811
6183
  try {
@@ -5850,27 +6222,30 @@ async function cmdKernel(args) {
5850
6222
  const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
5851
6223
  console.log(`
5852
6224
  \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
6225
+ if (getRunningState().running) {
6226
+ console.log(colors.yellow("\u63D0\u793A: \u8FD0\u884C\u4E2D\u7684\u5185\u6838\u4ECD\u662F\u65E7\u7248\u672C\uFF0C\u6267\u884C mihomo start \u91CD\u542F\u540E\u751F\u6548"));
6227
+ }
5853
6228
  }
5854
6229
  } catch (e) {
5855
- console.error(`
5856
- \u66F4\u65B0\u5931\u8D25: ${e.message}`);
6230
+ if (e instanceof CliError) throw e;
5857
6231
  const err = e;
5858
- if (err.response?.data) {
5859
- if (err.response.data.message) {
5860
- console.error(`\u539F\u56E0: ${err.response.data.message}`);
5861
- }
5862
- if (err.response.data.documentation_url) {
5863
- console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
5864
- }
6232
+ const hint = [];
6233
+ if (err.response?.data?.message) {
6234
+ hint.push(`\u539F\u56E0: ${err.response.data.message}`);
6235
+ }
6236
+ if (err.response?.data?.documentation_url) {
6237
+ hint.push(`\u6587\u6863: ${err.response.data.documentation_url}`);
5865
6238
  }
5866
6239
  if (!effectiveMirror) {
5867
- console.error("");
5868
- console.error("\u63D0\u793A: \u76F4\u8FDE\u5931\u8D25\u6216\u4E0B\u8F7D\u8FC7\u6162\u65F6\u53EF\u4F7F\u7528\u955C\u50CF:");
5869
- console.error(" mihomo kernel --mirror [\u955C\u50CF] # \u4E0B\u8F7D\u8D70\u955C\u50CF\uFF08\u9ED8\u8BA4 v6.gh-proxy.org\uFF09");
5870
- console.error(" mihomo kernel --mirror-all [\u955C\u50CF] # API \u548C\u4E0B\u8F7D\u90FD\u8D70\u955C\u50CF");
5871
- console.error(` \u53EF\u7528\u955C\u50CF: ${AVAILABLE_MIRRORS.join(", ")}`);
6240
+ hint.push(
6241
+ "",
6242
+ "\u63D0\u793A: \u76F4\u8FDE\u5931\u8D25\u6216\u4E0B\u8F7D\u8FC7\u6162\u65F6\u53EF\u4F7F\u7528\u955C\u50CF:",
6243
+ " mihomo kernel --mirror [\u955C\u50CF] # \u4E0B\u8F7D\u8D70\u955C\u50CF\uFF08\u9ED8\u8BA4 v6.gh-proxy.org\uFF09",
6244
+ " mihomo kernel --mirror-all [\u955C\u50CF] # API \u548C\u4E0B\u8F7D\u90FD\u8D70\u955C\u50CF",
6245
+ ` \u53EF\u7528\u955C\u50CF: ${AVAILABLE_MIRRORS.join(", ")}`
6246
+ );
5872
6247
  }
5873
- process.exit(1);
6248
+ throw new CliError(err.message, { label: "\u66F4\u65B0\u5931\u8D25", hint });
5874
6249
  }
5875
6250
  }
5876
6251
 
@@ -5887,370 +6262,73 @@ function cmdLogs(args) {
5887
6262
  const targetName = getNonFlagArg(args, 1);
5888
6263
  const lines = parseIntArg(args, "-n", "--lines", 100);
5889
6264
  const openInViewer = hasFlag(args, "-o", "--open");
5890
- if (targetName) {
5891
- let logPath;
5892
- if (targetName === "current" || targetName === "0") {
5893
- logPath = getLogPath();
5894
- } else {
5895
- const parsedIdx = parseInt(targetName, 10);
5896
- if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
5897
- const archiveLogs = listLogs();
5898
- const archive = archiveLogs.archives[parsedIdx - 1];
5899
- if (!archive) {
5900
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
5901
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5902
- process.exit(1);
5903
- }
5904
- logPath = archive.path;
5905
- } else {
5906
- logPath = getLogPathByName(targetName);
5907
- }
5908
- }
5909
- if (!logPath) {
5910
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
5911
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5912
- process.exit(1);
5913
- }
5914
- if (openInViewer) {
5915
- openLogFile(logPath);
5916
- return;
5917
- }
5918
- viewLogWithTail(logPath, { follow: false, lines });
5919
- return;
5920
- }
5921
- const logs = listLogs();
5922
- const all = [];
5923
- if (logs.current) all.push(logs.current);
5924
- all.push(...logs.archives);
5925
- if (all.length === 0) {
5926
- console.log("\u6682\u65E0\u65E5\u5FD7");
5927
- return;
5928
- }
5929
- console.log("");
5930
- console.log("\u65E5\u5FD7\u5217\u8868:");
5931
- console.log("");
5932
- let archiveCounter = 0;
5933
- for (const log of all) {
5934
- let num;
5935
- if (log.isCurrent) {
5936
- num = " 0";
5937
- } else {
5938
- archiveCounter++;
5939
- num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
5940
- }
5941
- const time = formatDate(log.mtime);
5942
- const size = formatBytes(log.size);
5943
- const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
5944
- console.log(` ${num}. ${name}`);
5945
- console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
5946
- if (!log.isCurrent) {
5947
- console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
5948
- }
5949
- console.log("");
5950
- }
5951
- console.log("\u7528\u6CD5:");
5952
- console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
5953
- console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
5954
- console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
5955
- console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
5956
- console.log("");
5957
- }
5958
-
5959
- // src/commands/overwrite.ts
5960
- import path7 from "path";
5961
-
5962
- // src/runtime.ts
5963
- function getRuntimeMode() {
5964
- if (isDaemonEnabled()) return "mixed";
5965
- return getConfigInfo()?.tun ? "tun" : "mixed";
5966
- }
5967
- function getRunningState() {
5968
- if (isDaemonEnabled()) {
5969
- const daemon = getDaemonStatus();
5970
- return { running: isDaemonRunning(daemon), pid: daemon.pid, daemon: true };
5971
- }
5972
- const status = getStatus();
5973
- return { running: status.running, pid: status.pid, daemon: false };
5974
- }
5975
- function isRestartNeededOnChange() {
5976
- return isDaemonEnabled() || getStatus().running;
5977
- }
5978
- async function launchOrRestart(mode) {
5979
- if (isDaemonEnabled()) {
5980
- await restartDaemon();
5981
- await sleep(DAEMON_BOOT_WAIT_MS);
5982
- return getDaemonStatus().pid;
5983
- }
5984
- const result = await start(mode);
5985
- return result.pid;
5986
- }
5987
-
5988
- // src/progress.ts
5989
- var IS_TTY = process.stdout.isTTY === true;
5990
- var BAR_WIDTH = 20;
5991
- function createProgressPrinter(totalRounds = 1) {
5992
- let alive = 0;
5993
- let dead = 0;
5994
- const resultMap = /* @__PURE__ */ new Map();
5995
- function render(done, total) {
5996
- if (!IS_TTY) return;
5997
- const pct = Math.round(done / total * 100);
5998
- const filled = Math.round(done / total * BAR_WIDTH);
5999
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
6000
- process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
6001
- }
6002
- return {
6003
- onResult(result, index, total, round = 1) {
6004
- if (resultMap.size === 0 && totalRounds > 1) {
6005
- console.log(`--- \u7B2C 1 \u8F6E\u6D4B\u8BD5 (${total} \u4E2A\u8282\u70B9) ---`);
6006
- }
6007
- const prev = resultMap.get(result.name);
6008
- if (prev) {
6009
- if (prev.result.delay !== null) alive--;
6010
- else dead--;
6011
- }
6012
- if (result.delay !== null) alive++;
6013
- else dead++;
6014
- resultMap.set(result.name, { result, round });
6015
- render(index + 1, total);
6016
- },
6017
- onRetryRound(round, count) {
6018
- if (IS_TTY) {
6019
- process.stdout.write("\n");
6020
- }
6021
- console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
6022
- alive = 0;
6023
- dead = 0;
6024
- },
6025
- finish() {
6026
- if (IS_TTY) {
6027
- process.stdout.write("\n");
6028
- }
6029
- console.log("");
6030
- if (!IS_TTY) return;
6031
- const entries = [...resultMap.values()];
6032
- entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
6033
- const total = entries.length;
6034
- console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
6035
- for (let i = 0; i < entries.length; i++) {
6036
- const { result, round } = entries[i];
6037
- const prefix = `[${i + 1}/${total}]`;
6038
- if (result.delay !== null) {
6039
- const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
6040
- const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
6041
- console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
6042
- } else {
6043
- console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
6044
- }
6045
- }
6046
- console.log("");
6047
- }
6048
- };
6049
- }
6050
- function formatCleanSummary(result) {
6051
- const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
6052
- if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
6053
- if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
6054
- return parts.join(", ");
6055
- }
6056
- function formatTestSummary(summary) {
6057
- return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
6058
- }
6059
-
6060
- // src/commands/status.ts
6061
- function printStatus() {
6062
- const status = getStatus();
6063
- const state = getRunningState();
6064
- const info = getConfigInfo();
6065
- const overwriteEnabled = isOverwriteEnabled();
6066
- const overwriteFiles = listOverwriteFile().files;
6067
- const activeSub = getActiveSubscription();
6068
- const { running, pid, daemon: daemonManaged } = state;
6069
- console.log("");
6070
- let modeLabel = "";
6071
- if (info) {
6072
- modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
6073
- }
6074
- const statusText = running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
6075
- console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}${modeLabel}`);
6076
- console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
6077
- if (pid) {
6078
- console.log(`${colors.gray("PID: ")}${pid}`);
6079
- if (!daemonManaged && status.processInfo) {
6080
- console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
6081
- }
6082
- }
6083
- if (info) {
6084
- if (info.tun) {
6085
- const extra = info.mixedPort ? `\uFF0C\u53E6\u76D1\u542C ${info.mixedPort}` : "";
6086
- console.log(`${colors.gray("\u7AEF\u53E3: ")}TUN \u63A5\u7BA1${extra}`);
6087
- } else if (info.mixedPort) {
6088
- console.log(`${colors.gray("\u7AEF\u53E3: ")}${info.mixedPort}`);
6089
- } else {
6090
- const ports = [];
6091
- if (info.httpPort) ports.push(`HTTP:${info.httpPort}`);
6092
- if (info.socksPort) ports.push(`SOCKS:${info.socksPort}`);
6093
- console.log(`${colors.gray("\u7AEF\u53E3: ")}${ports.length > 0 ? ports.join(", ") : "\u672A\u77E5"}`);
6094
- }
6095
- }
6096
- if (activeSub) {
6097
- let subLine = `${colors.gray("\u8BA2\u9605: ")}${activeSub.name}`;
6098
- if (info) {
6099
- subLine += ` (${formatProxySummary(info)})`;
6100
- }
6101
- console.log(subLine);
6102
- } else {
6103
- console.log(`${colors.gray("\u8BA2\u9605: ")}\u672A\u914D\u7F6E`);
6104
- }
6105
- if (overwriteEnabled && overwriteFiles.length > 0) {
6106
- const names = overwriteFiles.map((f) => f.name.replace(/^overwrite\.?/, "").replace(/\.ya?ml$/, "") || "\u4E3B\u6587\u4EF6").join(", ");
6107
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (${names})`);
6108
- } else if (overwriteEnabled) {
6109
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.green("\u5DF2\u542F\u7528")} (\u65E0\u6587\u4EF6)`);
6110
- } else {
6111
- console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
6112
- }
6113
- if (isDaemonEnabled()) {
6114
- console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
6115
- }
6116
- console.log("");
6117
- }
6118
-
6119
- // src/commands/stop.ts
6120
- function handleStopResult(result) {
6121
- if (result.remaining && result.remaining.length > 0) {
6122
- console.error(`${colors.red("\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62:")} ${result.remaining.join(", ")}`);
6123
- console.error("\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo");
6124
- process.exit(1);
6125
- }
6126
- }
6127
- async function cmdStop() {
6128
- if (isDaemonEnabled()) {
6129
- console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u4EE3\u7406\u7531 launchd \u6258\u7BA1"));
6130
- console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
6131
- return;
6132
- }
6133
- const pids = getMihomoPids();
6134
- if (pids.length === 0) {
6135
- console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
6136
- return;
6137
- }
6138
- console.log(`\u505C\u6B62 ${pids.length} \u4E2A\u8FDB\u7A0B...`);
6139
- handleStopResult(stop());
6140
- console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
6141
- }
6142
-
6143
- // src/commands/start.ts
6144
- async function cmdStart(args) {
6145
- if (!hasKernel()) {
6146
- console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
6147
- process.exit(1);
6148
- }
6149
- const targetMode = args[1] === "tun" ? "tun" : "mixed";
6150
- const daemonEnabled = isDaemonEnabled();
6151
- if (targetMode === "tun" && daemonEnabled) {
6152
- 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`);
6153
- console.error("\u8BF7\u5148\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
6154
- process.exit(1);
6155
- }
6156
- const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6157
- const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6158
- const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
6159
- const skipUpdate = hasFlag(args, "-s", "--no-update");
6160
- const skipClean = hasFlag(args, "--no-clean");
6161
- const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
6162
- const sub = getActiveSubscription();
6163
- if (!sub) {
6164
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
6165
- process.exit(1);
6166
- }
6167
- if (!skipUpdate) {
6168
- await autoUpdateStaleSubscription({ timeout: updateTimeout });
6169
- }
6170
- if (!daemonEnabled) {
6171
- if (hasRootResidue()) {
6172
- console.error(`${colors.red("\u9519\u8BEF:")} \u5B58\u5728\u9700\u8981 root \u6743\u9650\u6E05\u7406\u7684\u6B8B\u7559\u8FDB\u7A0B/\u6587\u4EF6`);
6173
- console.error(`\u8BF7\u5148\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo && sudo rm -f ${PATHS.pidFile}`);
6174
- console.error("\u6216\u5207\u6362\u5230 TUN \u6A21\u5F0F\u542F\u52A8\uFF08\u81EA\u52A8\u6E05\u7406\uFF09: mihomo start tun");
6175
- process.exit(1);
6176
- }
6177
- const status = getStatus();
6178
- const hasProcess = status.running || status.allProcesses.length > 0;
6179
- if (hasProcess) {
6180
- const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
6181
- console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
6182
- }
6183
- handleStopResult(stop());
6184
- if (hasProcess) {
6185
- console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
6186
- `);
6187
- }
6188
- }
6189
- let configInfo;
6190
- try {
6191
- configInfo = prepareConfigForStart(targetMode, sub.name);
6192
- } catch (e) {
6193
- console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
6194
- process.exit(1);
6195
- }
6196
- const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
6197
- console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
6198
- try {
6199
- const pid = await launchOrRestart(targetMode);
6200
- const label = daemonEnabled ? "\u5DF2\u542F\u52A8 (\u4FDD\u6D3B)" : "\u5DF2\u542F\u52A8";
6201
- console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""}`);
6202
- } catch (e) {
6203
- const msg = e.message;
6204
- const lines = msg.split("\n");
6205
- console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${lines[0]}`);
6206
- if (lines.length > 1) {
6207
- for (const line of lines.slice(1)) console.error(line);
6208
- }
6209
- process.exit(1);
6210
- }
6211
- const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
6212
- if (!skipClean && configInfo.proxies > cleanThreshold) {
6213
- const cache = readSubscriptionCache();
6214
- const lastCleanAt = cache[sub.name]?.last_auto_clean_at;
6215
- const withinCooldown = !!lastCleanAt && Date.now() - new Date(lastCleanAt).getTime() < AUTO_CLEAN_COOLDOWN_HOURS * 60 * 60 * 1e3;
6216
- if (!withinCooldown) {
6217
- console.log("");
6218
- console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${cleanThreshold}\uFF0C\u81EA\u52A8\u6E05\u7406\uFF08${AUTO_CLEAN_COOLDOWN_HOURS}h \u5185\u4EC5\u4E00\u6B21\uFF0C--no-clean \u8DF3\u8FC7\uFF09...`);
6219
- console.log("");
6220
- await sleep(1e3);
6221
- const progress = createProgressPrinter(rounds);
6222
- const cleanResult = await autoCleanSubscription(sub.name, {
6223
- timeout,
6224
- concurrency,
6225
- rounds,
6226
- onResult: progress.onResult,
6227
- onRetryRound: progress.onRetryRound
6228
- });
6229
- progress.finish();
6230
- console.log(formatTestSummary(cleanResult.summary));
6231
- if (cleanResult.skipped) {
6232
- 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"));
6233
- } else if (cleanResult.removedProxies > 0) {
6234
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
6235
- console.log("");
6236
- console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
6237
- if (!daemonEnabled) handleStopResult(stop());
6238
- try {
6239
- configInfo = prepareConfigForStart(targetMode, sub.name);
6240
- const pid = await launchOrRestart(targetMode);
6241
- console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
6242
- } catch (e) {
6243
- console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6244
- process.exit(1);
6265
+ if (targetName) {
6266
+ let logPath;
6267
+ if (targetName === "current" || targetName === "0") {
6268
+ logPath = getLogPath();
6269
+ } else {
6270
+ const parsedIdx = parseInt(targetName, 10);
6271
+ if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
6272
+ const archiveLogs = listLogs();
6273
+ const archive = archiveLogs.archives[parsedIdx - 1];
6274
+ if (!archive) {
6275
+ throw new CliError(`\u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`, { hint: '\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868' });
6245
6276
  }
6277
+ logPath = archive.path;
6278
+ } else {
6279
+ logPath = getLogPathByName(targetName);
6246
6280
  }
6247
- saveSubscriptionCache(sub.name, { last_auto_clean_at: (/* @__PURE__ */ new Date()).toISOString() });
6248
6281
  }
6282
+ if (!logPath) {
6283
+ throw new CliError(`\u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`, { hint: '\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868' });
6284
+ }
6285
+ if (openInViewer) {
6286
+ openLogFile(logPath);
6287
+ return;
6288
+ }
6289
+ viewLogWithTail(logPath, { follow: false, lines });
6290
+ return;
6249
6291
  }
6250
- printStatus();
6292
+ const logs = listLogs();
6293
+ const all = [];
6294
+ if (logs.current) all.push(logs.current);
6295
+ all.push(...logs.archives);
6296
+ if (all.length === 0) {
6297
+ console.log("\u6682\u65E0\u65E5\u5FD7");
6298
+ return;
6299
+ }
6300
+ console.log("");
6301
+ console.log("\u65E5\u5FD7\u5217\u8868:");
6302
+ console.log("");
6303
+ let archiveCounter = 0;
6304
+ for (const log of all) {
6305
+ let num;
6306
+ if (log.isCurrent) {
6307
+ num = " 0";
6308
+ } else {
6309
+ archiveCounter++;
6310
+ num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
6311
+ }
6312
+ const time = formatDate(log.mtime);
6313
+ const size = formatBytes(log.size);
6314
+ const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
6315
+ console.log(` ${num}. ${name}`);
6316
+ console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
6317
+ if (!log.isCurrent) {
6318
+ console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
6319
+ }
6320
+ console.log("");
6321
+ }
6322
+ console.log("\u7528\u6CD5:");
6323
+ console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
6324
+ console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
6325
+ console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
6326
+ console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
6327
+ console.log("");
6251
6328
  }
6252
6329
 
6253
6330
  // src/commands/overwrite.ts
6331
+ import path7 from "path";
6254
6332
  function printOverwriteList() {
6255
6333
  const info = listOverwriteFile();
6256
6334
  const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
@@ -6282,48 +6360,30 @@ function printOverwriteList() {
6282
6360
  console.log("\u7981\u7528\u8986\u5199: mihomo ow off");
6283
6361
  console.log("");
6284
6362
  }
6285
- async function cmdOverwrite(args) {
6286
- const action = args?.[1];
6287
- const currentMode = getRuntimeMode();
6288
- const restartNeeded = isRestartNeededOnChange();
6289
- if (action === "on" || action === "enable") {
6290
- if (isOverwriteEnabled()) {
6291
- console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u542F\u7528\u72B6\u6001");
6292
- console.log("");
6293
- printOverwriteList();
6294
- return;
6295
- }
6296
- setOverwriteEnabled(true);
6297
- console.log("\u5DF2\u542F\u7528\u8986\u5199\u914D\u7F6E");
6298
- if (restartNeeded) {
6299
- console.log("");
6300
- await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
6301
- return;
6302
- }
6363
+ async function setOverwrite(enabled, args) {
6364
+ if (isOverwriteEnabled() === enabled) {
6365
+ console.log(`\u8986\u5199\u914D\u7F6E\u5DF2\u662F${enabled ? "\u542F\u7528" : "\u7981\u7528"}\u72B6\u6001`);
6303
6366
  console.log("");
6304
6367
  printOverwriteList();
6305
6368
  return;
6306
6369
  }
6307
- if (action === "off" || action === "disable") {
6308
- if (!isOverwriteEnabled()) {
6309
- console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u7981\u7528\u72B6\u6001");
6370
+ setOverwriteEnabled(enabled);
6371
+ console.log(`\u5DF2${enabled ? "\u542F\u7528" : "\u7981\u7528"}\u8986\u5199\u914D\u7F6E`);
6372
+ if (await restartToApply(args)) return;
6373
+ console.log("");
6374
+ printOverwriteList();
6375
+ }
6376
+ var SUBCOMMANDS3 = [
6377
+ { name: "on", aliases: ["enable"], handler: (args) => setOverwrite(true, args) },
6378
+ { name: "off", aliases: ["disable"], handler: (args) => setOverwrite(false, args) }
6379
+ ];
6380
+ async function cmdOverwrite(args) {
6381
+ await dispatchSubcommand(args, SUBCOMMANDS3, {
6382
+ fallback: () => {
6310
6383
  console.log("");
6311
6384
  printOverwriteList();
6312
- return;
6313
- }
6314
- setOverwriteEnabled(false);
6315
- console.log("\u5DF2\u7981\u7528\u8986\u5199\u914D\u7F6E");
6316
- if (restartNeeded) {
6317
- console.log("");
6318
- await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
6319
- return;
6320
6385
  }
6321
- console.log("");
6322
- printOverwriteList();
6323
- return;
6324
- }
6325
- console.log("");
6326
- printOverwriteList();
6386
+ });
6327
6387
  }
6328
6388
 
6329
6389
  // src/commands/reset.ts
@@ -6433,10 +6493,7 @@ async function cmdReset(args) {
6433
6493
  const KNOWN_FLAGS = /* @__PURE__ */ new Set(["--full", "--yes", "-y"]);
6434
6494
  const unknownFlags = flags.filter((f) => !KNOWN_FLAGS.has(f));
6435
6495
  if (unknownFlags.length > 0) {
6436
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u9009\u9879: ${unknownFlags.join(", ")}`);
6437
- console.log("");
6438
- console.log("\u53EF\u7528\u9009\u9879: --full\uFF08\u5220\u5168\u90E8\uFF09, -y/--yes\uFF08\u8DF3\u8FC7\u786E\u8BA4\uFF09");
6439
- process.exit(1);
6496
+ throw new CliError(`\u672A\u77E5\u7684\u9009\u9879: ${unknownFlags.join(", ")}`, { hint: ["", "\u53EF\u7528\u9009\u9879: --full\uFF08\u5220\u5168\u90E8\uFF09, -y/--yes\uFF08\u8DF3\u8FC7\u786E\u8BA4\uFF09"] });
6440
6497
  }
6441
6498
  const fullReset = flags.includes("--full");
6442
6499
  const skipConfirm = flags.includes("--yes") || flags.includes("-y");
@@ -6446,16 +6503,18 @@ async function cmdReset(args) {
6446
6503
  } else if (names.length > 0) {
6447
6504
  const { matched, unmatched } = resolveResetTargets(names);
6448
6505
  if (unmatched.length > 0) {
6449
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u91CD\u7F6E\u76EE\u6807: ${unmatched.join(", ")}`);
6450
- console.log("");
6451
- console.log(`\u53EF\u7528\u76EE\u6807: ${RESET_TARGETS.map((t) => t.aliases[0]).join(", ")}`);
6452
- console.log("");
6453
- console.log("\u793A\u4F8B:");
6454
- console.log(" mihomo reset sub log # \u5220\u9664\u8BA2\u9605\u548C\u65E5\u5FD7");
6455
- console.log(" mihomo reset kernel # \u53EA\u5220\u5185\u6838");
6456
- console.log(" mihomo reset --full # \u5220\u9664\u5168\u90E8");
6457
- console.log(" mihomo reset # \u5220\u9664\u5168\u90E8\uFF08\u4FDD\u7559\u8BBE\u7F6E\u3001\u5185\u6838\u3001\u8986\u5199\uFF09");
6458
- process.exit(1);
6506
+ throw new CliError(`\u672A\u77E5\u7684\u91CD\u7F6E\u76EE\u6807: ${unmatched.join(", ")}`, {
6507
+ hint: [
6508
+ "",
6509
+ `\u53EF\u7528\u76EE\u6807: ${RESET_TARGETS.map((t) => t.aliases[0]).join(", ")}`,
6510
+ "",
6511
+ "\u793A\u4F8B:",
6512
+ " mihomo reset sub log # \u5220\u9664\u8BA2\u9605\u548C\u65E5\u5FD7",
6513
+ " mihomo reset kernel # \u53EA\u5220\u5185\u6838",
6514
+ " mihomo reset --full # \u5220\u9664\u5168\u90E8",
6515
+ " mihomo reset # \u5220\u9664\u5168\u90E8\uFF08\u4FDD\u7559\u8BBE\u7F6E\u3001\u5185\u6838\u3001\u8986\u5199\uFF09"
6516
+ ]
6517
+ });
6459
6518
  }
6460
6519
  targets = matched;
6461
6520
  } else {
@@ -6525,6 +6584,15 @@ async function cmdReset(args) {
6525
6584
  import { spawn as spawn2 } from "child_process";
6526
6585
  import fs9 from "fs";
6527
6586
  import path8 from "path";
6587
+ function isProxyValid(proxy) {
6588
+ if (!proxy.name || !proxy.server || !proxy.port) return false;
6589
+ if (!proxy.type) return false;
6590
+ if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
6591
+ const pw = String(proxy.password || "");
6592
+ if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
6593
+ }
6594
+ return true;
6595
+ }
6528
6596
  var TEST_DIR = path8.join(USER_DATA_DIR, "test");
6529
6597
  var TEST_DIRS = {
6530
6598
  data: path8.join(TEST_DIR, "data"),
@@ -6548,12 +6616,12 @@ function buildTestConfig(subName) {
6548
6616
  ensureTestDirs();
6549
6617
  const rawContent = readSubscriptionRawConfig(subName);
6550
6618
  if (!rawContent) {
6551
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
6619
+ throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u66F4\u65B0\u8BA2\u9605\uFF08mihomo sub update ${subName}\uFF09`);
6552
6620
  }
6553
6621
  const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
6554
6622
  const proxies = (parsed.proxies || []).filter(isProxyValid);
6555
6623
  if (proxies.length === 0) {
6556
- throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
6624
+ throw new CliError(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
6557
6625
  }
6558
6626
  const nameCount = /* @__PURE__ */ new Map();
6559
6627
  for (const proxy of proxies) {
@@ -6580,7 +6648,7 @@ function buildTestConfig(subName) {
6580
6648
  }
6581
6649
  async function startTestInstance() {
6582
6650
  const binary = PATHS.mihomoBinary;
6583
- if (!fs9.existsSync(binary)) throw new Error('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
6651
+ if (!fs9.existsSync(binary)) throw new CliError('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
6584
6652
  stopTestInstance();
6585
6653
  const logFd = fs9.openSync(TEST_PATHS.logFile, "a");
6586
6654
  const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
@@ -6592,7 +6660,7 @@ async function startTestInstance() {
6592
6660
  fs9.closeSync(logFd);
6593
6661
  child.unref();
6594
6662
  const pid = child.pid;
6595
- if (!pid) throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u521B\u5EFA\u8FDB\u7A0B\uFF08\u5185\u6838\u4E8C\u8FDB\u5236\u53EF\u80FD\u4E0D\u53EF\u6267\u884C\uFF09");
6663
+ if (!pid) throw new CliError("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u521B\u5EFA\u8FDB\u7A0B\uFF08\u5185\u6838\u4E8C\u8FDB\u5236\u53EF\u80FD\u4E0D\u53EF\u6267\u884C\uFF09");
6596
6664
  fs9.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
6597
6665
  const client = createHttpClient({ timeout: 2e3 });
6598
6666
  let ready = false;
@@ -6612,11 +6680,11 @@ async function startTestInstance() {
6612
6680
  errorDetail = fs9.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
6613
6681
  } catch {
6614
6682
  }
6615
- throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
6683
+ throw new CliError(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
6616
6684
  ${errorDetail}` : ""}`);
6617
6685
  }
6618
6686
  if (!ready) {
6619
- throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
6687
+ throw new CliError("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
6620
6688
  }
6621
6689
  }
6622
6690
  function stopTestInstance() {
@@ -6664,21 +6732,18 @@ function githubRepoUrl(rawUrl) {
6664
6732
  function resolveTestTarget(args) {
6665
6733
  const subs = getSubscriptions();
6666
6734
  if (subs.length === 0) {
6667
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6668
- process.exit(1);
6735
+ throw new CliError("\u6CA1\u6709\u8BA2\u9605");
6669
6736
  }
6670
6737
  const nameArg = getNonFlagArg(args, 2);
6671
6738
  const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6672
6739
  const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
6673
6740
  let target;
6674
6741
  if (nameArg) {
6675
- const matches = findSubscriptionFuzzy(subs, nameArg);
6676
- target = pickSingleSubscription(matches, nameArg);
6742
+ target = resolveSubscription(subs, nameArg);
6677
6743
  } else {
6678
6744
  const activeSub = getActiveSubscription();
6679
6745
  if (!activeSub) {
6680
- console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
6681
- process.exit(1);
6746
+ throw new CliError("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
6682
6747
  }
6683
6748
  target = activeSub;
6684
6749
  }
@@ -6739,274 +6804,235 @@ function printSubscriptionList() {
6739
6804
  console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
6740
6805
  console.log("");
6741
6806
  }
6742
- async function cmdSubscription(args) {
6743
- const action = args[1];
6744
- if (!action || action === "list") {
6745
- printSubscriptionList();
6746
- return;
6807
+ async function subAdd(args) {
6808
+ const url = args[2]?.trim();
6809
+ const name = args[3] || "default";
6810
+ if (!url) {
6811
+ throw new CliError("\u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6747
6812
  }
6748
- if (action === "add") {
6749
- const url = args[2]?.trim();
6750
- const name = args[3] || "default";
6751
- if (!url) {
6752
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6753
- process.exit(1);
6754
- }
6755
- if (isMultiUrl(url)) {
6756
- const urls = splitUrls(url);
6757
- if (urls.length === 0) {
6758
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6759
- process.exit(1);
6760
- }
6761
- for (const u of urls) {
6762
- if (!isValidHttpUrl(u)) {
6763
- console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
6764
- process.exit(1);
6765
- }
6766
- }
6767
- const normalizedUrl = urls.join(",");
6768
- console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
6769
- try {
6770
- addSubscription(normalizedUrl, name);
6771
- setDefaultSubscription(name);
6772
- const info = await downloadMergedSubscription(urls, name);
6773
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
6774
- } catch (e) {
6775
- removeSubscription(name);
6776
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
6777
- process.exit(1);
6778
- }
6779
- } else {
6780
- if (!isValidHttpUrl(url)) {
6781
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL\uFF08\u9700\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF09");
6782
- process.exit(1);
6783
- }
6784
- console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
6785
- try {
6786
- addSubscription(url, name);
6787
- setDefaultSubscription(name);
6788
- const info = await downloadSubscription(url, name);
6789
- const repoUrl = githubRepoUrl(url);
6790
- if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
6791
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
6792
- } catch (e) {
6793
- removeSubscription(name);
6794
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
6795
- process.exit(1);
6796
- }
6813
+ if (isMultiUrl(url)) {
6814
+ const urls = splitUrls(url);
6815
+ if (urls.length === 0) {
6816
+ throw new CliError("\u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6797
6817
  }
6798
- console.log("");
6799
- printSubscriptionList();
6800
- return;
6801
- }
6802
- if (action === "update") {
6803
- const name = args[2];
6804
- const subs = getSubscriptions();
6805
- if (subs.length === 0) {
6806
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6807
- process.exit(1);
6808
- }
6809
- if (!name) {
6810
- console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
6811
- const results = await Promise.all(subs.map((sub) => tryUpdateOne(sub)));
6812
- let ok = 0;
6813
- for (const r of results) {
6814
- if (r.success) ok++;
6815
- printUpdateResult(r);
6818
+ for (const u of urls) {
6819
+ if (!isValidHttpUrl(u)) {
6820
+ throw new CliError(`\u65E0\u6548\u7684 URL: ${u}`);
6816
6821
  }
6817
- if (ok === 0) process.exit(1);
6818
- console.log("");
6819
- printRestartHintIfRunning();
6820
- printSubscriptionList();
6821
- return;
6822
6822
  }
6823
- const matches = findSubscriptionFuzzy(subs, name);
6824
- const target = pickSingleSubscription(matches, name);
6825
- console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
6826
- const result = await tryUpdateOne(target);
6827
- if (!result.success) {
6828
- console.error(`\u66F4\u65B0\u5931\u8D25: ${(result.error || "").split("\n")[0]}`);
6829
- process.exit(1);
6823
+ const normalizedUrl = urls.join(",");
6824
+ console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
6825
+ addSubscription(normalizedUrl, name);
6826
+ try {
6827
+ setDefaultSubscription(name);
6828
+ const info = await downloadMergedSubscription(urls, name);
6829
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
6830
+ } catch (e) {
6831
+ removeSubscription(name);
6832
+ throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25" });
6830
6833
  }
6831
- console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(result)})`);
6832
- console.log("");
6833
- printRestartHintIfRunning();
6834
- printSubscriptionList();
6835
- return;
6836
- }
6837
- if (action === "use") {
6838
- const name = args[2];
6839
- const subs = getSubscriptions();
6840
- if (!name) {
6841
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
6842
- if (subs.length > 0) {
6843
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
6844
- for (const s of subs) console.log(` ${s.name}`);
6845
- }
6846
- process.exit(1);
6847
- }
6848
- const matches = findSubscriptionFuzzy(subs, name);
6849
- const target = pickSingleSubscription(matches, name);
6850
- const currentDefault = getActiveSubscription();
6851
- const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
6852
- if (isAlreadyDefault) {
6853
- console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
6854
- console.log("");
6855
- printSubscriptionList();
6856
- return;
6834
+ } else {
6835
+ if (!isValidHttpUrl(url)) {
6836
+ throw new CliError("\u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL\uFF08\u9700\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF09");
6857
6837
  }
6858
- const currentMode = getRuntimeMode();
6859
- const restartNeeded = isRestartNeededOnChange();
6860
- const success = setDefaultSubscription(target.name);
6861
- if (success) {
6862
- console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
6863
- } else {
6864
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
6865
- process.exit(1);
6838
+ console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
6839
+ addSubscription(url, name);
6840
+ try {
6841
+ setDefaultSubscription(name);
6842
+ const info = await downloadSubscription(url, name);
6843
+ const repoUrl = githubRepoUrl(url);
6844
+ if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
6845
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
6846
+ } catch (e) {
6847
+ removeSubscription(name);
6848
+ throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25" });
6866
6849
  }
6867
- if (restartNeeded) {
6868
- console.log("");
6869
- await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
6870
- return;
6850
+ }
6851
+ console.log("");
6852
+ printSubscriptionList();
6853
+ }
6854
+ async function subUpdate(args) {
6855
+ const name = args[2];
6856
+ const subs = getSubscriptions();
6857
+ if (subs.length === 0) {
6858
+ throw new CliError("\u6CA1\u6709\u8BA2\u9605");
6859
+ }
6860
+ if (!name) {
6861
+ console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
6862
+ const results = await Promise.all(subs.map((sub) => tryUpdateOne(sub)));
6863
+ let ok = 0;
6864
+ for (const r of results) {
6865
+ if (r.success) ok++;
6866
+ printUpdateResult(r);
6871
6867
  }
6868
+ if (ok === 0) throw new CliError("\u5168\u90E8\u8BA2\u9605\u66F4\u65B0\u5931\u8D25");
6872
6869
  console.log("");
6870
+ printRestartHintIfRunning();
6873
6871
  printSubscriptionList();
6874
6872
  return;
6875
6873
  }
6876
- if (action === "web" || action === "open") {
6877
- const name = args[2];
6878
- const subs = getSubscriptionsWithCache();
6879
- if (subs.length === 0) {
6880
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6881
- process.exit(1);
6882
- }
6883
- let target;
6884
- if (name) {
6885
- const matches = findSubscriptionFuzzy(subs, name);
6886
- target = pickSingleSubscription(matches, name);
6887
- } else {
6888
- target = getActiveSubscription() || subs[0];
6889
- }
6890
- const cached = subs.find((s) => s.name === target.name);
6891
- let webPageUrl = cached?.web_page_url;
6892
- if (!webPageUrl) {
6893
- console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u67E5\u8BE2\u8BA2\u9605...");
6894
- try {
6895
- const info = isMultiUrl(target.url) ? await downloadMergedSubscription(splitUrls(target.url), target.name, void 0, false) : await downloadSubscription(target.url, target.name, void 0, false);
6896
- if (info.webPageUrl) {
6897
- webPageUrl = info.webPageUrl;
6898
- } else {
6899
- console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
6900
- process.exit(1);
6901
- }
6902
- } catch (e) {
6903
- console.error(`\u67E5\u8BE2\u5931\u8D25: ${e.message}`);
6904
- process.exit(1);
6905
- }
6906
- }
6907
- console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
6908
- const opened = openUrl(webPageUrl);
6909
- if (!opened) {
6910
- console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
6911
- }
6912
- return;
6874
+ const target = resolveSubscription(subs, name);
6875
+ console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
6876
+ const result = await tryUpdateOne(target);
6877
+ if (!result.success) {
6878
+ throw new CliError((result.error || "").split("\n")[0], { label: "\u66F4\u65B0\u5931\u8D25" });
6913
6879
  }
6914
- if (action === "remove" || action === "rm" || action === "delete") {
6915
- const name = args[2];
6916
- const subs = getSubscriptions();
6917
- if (!name) {
6918
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
6919
- if (subs.length > 0) {
6920
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
6921
- for (const s of subs) console.log(` ${s.name}`);
6922
- }
6923
- process.exit(1);
6924
- }
6925
- const matches = findSubscriptionFuzzy(subs, name);
6926
- const target = pickSingleSubscription(matches, name);
6927
- const switchedTo = removeSubscription(target.name);
6928
- console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
6929
- if (switchedTo) {
6930
- console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
6931
- }
6880
+ console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(result)})`);
6881
+ console.log("");
6882
+ printRestartHintIfRunning();
6883
+ printSubscriptionList();
6884
+ }
6885
+ async function subUse(args) {
6886
+ const name = args[2];
6887
+ const subs = getSubscriptions();
6888
+ if (!name) {
6889
+ throw new CliError("\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0", {
6890
+ hint: subs.length > 0 ? ["", "\u53EF\u7528\u8BA2\u9605:", ...subs.map((s) => ` ${s.name}`)] : void 0
6891
+ });
6892
+ }
6893
+ const target = resolveSubscription(subs, name);
6894
+ const currentDefault = getActiveSubscription();
6895
+ const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
6896
+ if (isAlreadyDefault) {
6897
+ console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
6932
6898
  console.log("");
6933
6899
  printSubscriptionList();
6934
6900
  return;
6935
6901
  }
6936
- if (action === "clean") {
6937
- const { target, timeout, concurrency } = resolveTestTarget(args);
6938
- const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6939
- console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
6940
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6941
- console.log("");
6942
- const progress = createProgressPrinter(rounds);
6943
- const result = await withTestInstance(target.name, async (apiBase) => {
6944
- return autoCleanSubscription(target.name, {
6945
- timeout,
6946
- concurrency,
6947
- rounds,
6948
- apiBase,
6949
- onResult: progress.onResult,
6950
- onRetryRound: progress.onRetryRound
6951
- });
6952
- });
6953
- progress.finish();
6954
- console.log(formatTestSummary(result.summary));
6955
- if (result.skipped) {
6956
- console.log("");
6957
- 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"));
6958
- } else if (result.removedProxies > 0) {
6959
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6960
- if (getRunningState().running) {
6961
- console.log("");
6962
- console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
6902
+ const success = setDefaultSubscription(target.name);
6903
+ if (!success) {
6904
+ throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
6905
+ }
6906
+ console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
6907
+ if (await restartToApply(args)) return;
6908
+ console.log("");
6909
+ printSubscriptionList();
6910
+ }
6911
+ async function subWeb(args) {
6912
+ const name = args[2];
6913
+ const subs = getSubscriptionsWithCache();
6914
+ if (subs.length === 0) {
6915
+ throw new CliError("\u6CA1\u6709\u8BA2\u9605");
6916
+ }
6917
+ let target;
6918
+ if (name) {
6919
+ target = resolveSubscription(subs, name);
6920
+ } else {
6921
+ target = getActiveSubscription() || subs[0];
6922
+ }
6923
+ const cached = subs.find((s) => s.name === target.name);
6924
+ let webPageUrl = cached?.web_page_url;
6925
+ if (!webPageUrl) {
6926
+ console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u67E5\u8BE2\u8BA2\u9605...");
6927
+ try {
6928
+ const info = isMultiUrl(target.url) ? await downloadMergedSubscription(splitUrls(target.url), target.name, void 0, false) : await downloadSubscription(target.url, target.name, void 0, false);
6929
+ if (!info.webPageUrl) {
6930
+ throw new CliError("\u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
6963
6931
  }
6932
+ webPageUrl = info.webPageUrl;
6933
+ } catch (e) {
6934
+ if (e instanceof CliError) throw e;
6935
+ throw new CliError(e.message, { label: "\u67E5\u8BE2\u5931\u8D25" });
6964
6936
  }
6965
- return;
6966
6937
  }
6967
- if (action === "test") {
6968
- const { target, timeout, concurrency } = resolveTestTarget(args);
6969
- console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
6970
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6971
- console.log("");
6972
- const progress = createProgressPrinter();
6973
- const summary = await withTestInstance(target.name, async (apiBase) => {
6974
- return testSubscriptionProxies(target.name, {
6975
- timeout,
6976
- concurrency,
6977
- apiBase,
6978
- onResult: progress.onResult
6979
- });
6980
- });
6981
- progress.finish();
6982
- console.log(formatTestSummary(summary));
6983
- return;
6938
+ console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
6939
+ const opened = openUrl(webPageUrl);
6940
+ if (!opened) {
6941
+ console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
6984
6942
  }
6985
- console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
6986
- console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
6987
- process.exit(1);
6988
6943
  }
6989
-
6990
- // src/commands/test.ts
6991
- function requireRunning() {
6992
- const state = getRunningState();
6993
- if (!state.running) {
6994
- const hint = state.daemon ? "mihomo daemon on" : "mihomo start";
6995
- console.error(`\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (${hint})`);
6996
- process.exit(1);
6944
+ function subRemove(args) {
6945
+ const name = args[2];
6946
+ const subs = getSubscriptions();
6947
+ if (!name) {
6948
+ throw new CliError("\u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0", {
6949
+ hint: subs.length > 0 ? ["", "\u53EF\u7528\u8BA2\u9605:", ...subs.map((s) => ` ${s.name}`)] : void 0
6950
+ });
6951
+ }
6952
+ const target = resolveSubscription(subs, name);
6953
+ const switchedTo = removeSubscription(target.name);
6954
+ console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
6955
+ if (switchedTo) {
6956
+ console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
6997
6957
  }
6958
+ console.log("");
6959
+ printSubscriptionList();
6998
6960
  }
6999
- function requireActiveSub() {
7000
- const activeSub = getActiveSubscription();
7001
- if (!activeSub) {
7002
- console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
7003
- process.exit(1);
6961
+ async function subClean(args) {
6962
+ const { target, timeout, concurrency } = resolveTestTarget(args);
6963
+ const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6964
+ console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
6965
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6966
+ console.log("");
6967
+ const progress = createProgressPrinter(rounds);
6968
+ const result = await withTestInstance(target.name, async (apiBase) => {
6969
+ return autoCleanSubscription(target.name, {
6970
+ timeout,
6971
+ concurrency,
6972
+ rounds,
6973
+ apiBase,
6974
+ onResult: progress.onResult,
6975
+ onRetryRound: progress.onRetryRound
6976
+ });
6977
+ });
6978
+ progress.finish();
6979
+ console.log(formatTestSummary(result.summary));
6980
+ if (result.skipped) {
6981
+ console.log("");
6982
+ 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"));
6983
+ } else if (result.removedProxies > 0) {
6984
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6985
+ if (getRunningState().running) {
6986
+ console.log("");
6987
+ console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
6988
+ }
7004
6989
  }
7005
- return activeSub;
7006
6990
  }
6991
+ async function subTest(args) {
6992
+ const { target, timeout, concurrency } = resolveTestTarget(args);
6993
+ console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
6994
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6995
+ console.log("");
6996
+ const progress = createProgressPrinter();
6997
+ const summary = await withTestInstance(target.name, async (apiBase) => {
6998
+ return testSubscriptionProxies(target.name, {
6999
+ timeout,
7000
+ concurrency,
7001
+ apiBase,
7002
+ onResult: progress.onResult
7003
+ });
7004
+ });
7005
+ progress.finish();
7006
+ console.log(formatTestSummary(summary));
7007
+ }
7008
+ var SUBCOMMANDS4 = [
7009
+ { name: "list", handler: printSubscriptionList },
7010
+ { name: "add", handler: subAdd },
7011
+ { name: "update", handler: subUpdate },
7012
+ { name: "use", handler: subUse },
7013
+ { name: "web", aliases: ["open"], handler: subWeb },
7014
+ { name: "remove", aliases: ["rm", "delete"], handler: subRemove },
7015
+ { name: "clean", handler: subClean },
7016
+ { name: "test", handler: subTest }
7017
+ ];
7018
+ async function cmdSubscription(args) {
7019
+ await dispatchSubcommand(args, SUBCOMMANDS4, {
7020
+ // 无子命令 → 列表;未知子命令 → 报错
7021
+ fallback: printSubscriptionList,
7022
+ onUnknown: (action) => {
7023
+ const names = SUBCOMMANDS4.flatMap((c) => [c.name, ...c.aliases ?? []]);
7024
+ const suggestion = suggestSimilar(action, names);
7025
+ throw new CliError(`\u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4: ${action}`, {
7026
+ hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]"]
7027
+ });
7028
+ }
7029
+ });
7030
+ }
7031
+
7032
+ // src/commands/test.ts
7007
7033
  async function cmdTest(args) {
7008
7034
  requireRunning();
7009
- const activeSub = requireActiveSub();
7035
+ const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
7010
7036
  const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
7011
7037
  const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
7012
7038
  console.log(`\u6D4B\u8BD5 "${activeSub.name}" \u8282\u70B9\u8FDE\u901A\u6027...`);
@@ -7023,7 +7049,7 @@ async function cmdTest(args) {
7023
7049
  }
7024
7050
  async function cmdClean(args) {
7025
7051
  requireRunning();
7026
- const activeSub = requireActiveSub();
7052
+ const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
7027
7053
  const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
7028
7054
  const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
7029
7055
  const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
@@ -7054,9 +7080,7 @@ async function cmdClean(args) {
7054
7080
  try {
7055
7081
  if (!daemonManaged) {
7056
7082
  if (hasRootResidue()) {
7057
- console.error(`${colors.red("\u9519\u8BEF:")} \u4E3B\u5B9E\u4F8B\u4EE5 root \u8FD0\u884C\uFF08TUN\uFF09\uFF0C\u505C\u6B62\u5B83\u9700\u8981 sudo`);
7058
- console.error("\u8BF7\u6539\u7528 mihomo sub clean\uFF08\u9694\u79BB\u5B9E\u4F8B\u6D4B\u901F\uFF0C\u65E0\u9700\u505C\u6B62\u4E3B\u5B9E\u4F8B\uFF09");
7059
- process.exit(1);
7083
+ throw new CliError("\u4E3B\u5B9E\u4F8B\u4EE5 root \u8FD0\u884C\uFF08TUN\uFF09\uFF0C\u505C\u6B62\u5B83\u9700\u8981 sudo", { hint: "\u8BF7\u6539\u7528 mihomo sub clean\uFF08\u9694\u79BB\u5B9E\u4F8B\u6D4B\u901F\uFF0C\u65E0\u9700\u505C\u6B62\u4E3B\u5B9E\u4F8B\uFF09" });
7060
7084
  }
7061
7085
  handleStopResult(stop());
7062
7086
  }
@@ -7065,8 +7089,8 @@ async function cmdClean(args) {
7065
7089
  const label = daemonManaged ? "\u5DF2\u91CD\u542F (\u4FDD\u6D3B)" : "\u5DF2\u91CD\u542F";
7066
7090
  console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
7067
7091
  } catch (e) {
7068
- console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
7069
- process.exit(1);
7092
+ if (e instanceof CliError) throw e;
7093
+ throw new CliError(e.message.split("\n")[0], { label: "\u91CD\u542F\u5931\u8D25" });
7070
7094
  }
7071
7095
  }
7072
7096
  }
@@ -7075,9 +7099,11 @@ async function cmdClean(args) {
7075
7099
  function cmdUI(args) {
7076
7100
  const uiName = args[1] || "zash";
7077
7101
  if (!Object.hasOwn(UI_URLS, uiName)) {
7078
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684 UI "${uiName}"`);
7079
- console.error("\u53EF\u7528 UI: zash (\u9ED8\u8BA4), dash, yacd");
7080
- process.exit(1);
7102
+ throw new CliError(`\u672A\u77E5\u7684 UI "${uiName}"`, { hint: "\u53EF\u7528 UI: zash (\u9ED8\u8BA4), dash, yacd" });
7103
+ }
7104
+ if (!getRunningState().running) {
7105
+ console.log(colors.yellow("\u63D0\u793A: mihomo \u672A\u8FD0\u884C\uFF0CUI \u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5\uFF08\u5148\u6267\u884C mihomo start \u542F\u52A8\uFF09"));
7106
+ console.log("");
7081
7107
  }
7082
7108
  const url = UI_URLS[uiName];
7083
7109
  console.log(`\u6253\u5F00 Web UI: ${uiName}`);
@@ -7093,37 +7119,54 @@ function cmdUI(args) {
7093
7119
  }
7094
7120
 
7095
7121
  // src/commands/update.ts
7096
- import { exec, spawn as spawn3 } from "child_process";
7122
+ import { execFile, spawn as spawn3 } from "child_process";
7097
7123
  import { promisify } from "util";
7098
- var execAsync = promisify(exec);
7124
+ var execFileAsync = promisify(execFile);
7125
+ var NPM_VIEW_TIMEOUT_MS = 15e3;
7126
+ async function getLatestNpmVersion() {
7127
+ try {
7128
+ const { stdout } = await execFileAsync("npm", ["view", PKG_NAME, "version"], { timeout: NPM_VIEW_TIMEOUT_MS });
7129
+ const version = stdout.trim().split("\n").pop()?.trim();
7130
+ return version || null;
7131
+ } catch {
7132
+ return null;
7133
+ }
7134
+ }
7099
7135
  async function cmdUpdate() {
7100
7136
  console.log(`\u5F53\u524D\u7248\u672C: ${colors.cyan(VERSION)}`);
7101
7137
  console.log("");
7138
+ console.log("\u6B63\u5728\u68C0\u67E5\u6700\u65B0\u7248\u672C...");
7139
+ const latest = await getLatestNpmVersion();
7140
+ if (latest && latest === VERSION) {
7141
+ console.log(`\u5DF2\u662F\u6700\u65B0\u7248\u672C (${colors.green(VERSION)})\uFF0C\u65E0\u9700\u66F4\u65B0`);
7142
+ return;
7143
+ }
7144
+ if (latest) {
7145
+ console.log(`\u6700\u65B0\u7248\u672C: ${colors.cyan(latest)}`);
7146
+ } else {
7147
+ console.log(colors.yellow("\u65E0\u6CD5\u67E5\u8BE2\u6700\u65B0\u7248\u672C\uFF08\u7F51\u7EDC\u95EE\u9898\uFF1F\uFF09\uFF0C\u5C06\u76F4\u63A5\u5C1D\u8BD5\u91CD\u65B0\u5B89\u88C5"));
7148
+ }
7149
+ console.log("");
7102
7150
  console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
7103
7151
  console.log("");
7104
- await new Promise((resolve) => {
7105
- const npm = spawn3("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
7152
+ await new Promise((resolve, reject) => {
7153
+ const npm = spawn3("npm", ["install", "-g", PKG_NAME], { stdio: "inherit" });
7106
7154
  npm.on("close", (code) => {
7107
7155
  if (code === 0) {
7108
7156
  resolve();
7109
7157
  } else {
7110
- console.error("\u66F4\u65B0\u5931\u8D25\u3002\u82E5\u4E3A\u6743\u9650\u95EE\u9898\uFF08EACCES\uFF09\uFF0C\u53EF\u5C1D\u8BD5: sudo npm install -g mihomo-cli");
7111
- process.exit(code || 1);
7158
+ reject(new CliError("\u66F4\u65B0\u5931\u8D25\u3002\u82E5\u4E3A\u6743\u9650\u95EE\u9898\uFF08EACCES\uFF09\uFF0C\u53EF\u5C1D\u8BD5: sudo npm install -g mihomo-cli", { exitCode: code || 1 }));
7112
7159
  }
7113
7160
  });
7114
7161
  npm.on("error", (e) => {
7115
- if (e.message.includes("EACCES") || e.message.includes("permission")) {
7116
- console.error("\u6743\u9650\u4E0D\u8DB3\uFF0C\u53EF\u5C1D\u8BD5: sudo npm install -g mihomo-cli");
7117
- } else {
7118
- console.error(`\u6267\u884C\u5931\u8D25: ${e.message}`);
7119
- }
7120
- process.exit(1);
7162
+ const perm = e.message.includes("EACCES") || e.message.includes("permission");
7163
+ reject(perm ? new CliError("\u6743\u9650\u4E0D\u8DB3\uFF0C\u53EF\u5C1D\u8BD5: sudo npm install -g mihomo-cli") : new CliError(`\u6267\u884C\u5931\u8D25: ${e.message}`));
7121
7164
  });
7122
7165
  });
7123
7166
  try {
7124
- const { stdout } = await execAsync("npm list -g mihomo-cli --json --depth=0");
7167
+ const { stdout } = await execFileAsync("npm", ["list", "-g", PKG_NAME, "--json", "--depth=0"]);
7125
7168
  const result = JSON.parse(stdout);
7126
- const newVersion = result.dependencies?.["mihomo-cli"]?.version;
7169
+ const newVersion = result.dependencies?.[PKG_NAME]?.version;
7127
7170
  console.log("");
7128
7171
  if (newVersion) {
7129
7172
  console.log(`\u66F4\u65B0\u5B8C\u6210\uFF0C\u6700\u65B0\u7248\u672C: ${colors.green(newVersion)}`);
@@ -7328,6 +7371,9 @@ var COMMAND_INDEX = (() => {
7328
7371
  function findCommand(token) {
7329
7372
  return COMMAND_INDEX.get(token);
7330
7373
  }
7374
+ function allCommandTokens() {
7375
+ return [...COMMAND_INDEX.keys()];
7376
+ }
7331
7377
 
7332
7378
  // src/index.ts
7333
7379
  process.on("SIGINT", () => {
@@ -7377,14 +7423,23 @@ async function main() {
7377
7423
  const token = args[0].toLowerCase();
7378
7424
  const command = findCommand(token);
7379
7425
  if (!command) {
7380
- console.error(`\u672A\u77E5\u547D\u4EE4: ${token}`);
7381
- console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');
7382
- process.exit(1);
7426
+ const suggestion = suggestSimilar(token, allCommandTokens());
7427
+ throw new CliError(`\u672A\u77E5\u547D\u4EE4: ${token}`, {
7428
+ hint: [suggestion.length > 0 ? `\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?` : '\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9']
7429
+ });
7383
7430
  }
7384
7431
  await command.handler(command.rewrite ? command.rewrite(args) : args);
7385
7432
  }
7386
7433
  main().catch((e) => {
7387
- console.error(`\u9519\u8BEF: ${e.message}`);
7434
+ if (e instanceof CliError) {
7435
+ console.error(`${colors.red(`${e.label}:`)} ${e.message}`);
7436
+ for (const line of e.hint) console.error(line);
7437
+ runCleanup();
7438
+ process.exit(e.exitCode);
7439
+ }
7440
+ const err = e;
7441
+ console.error(`${colors.red("\u9519\u8BEF:")} ${err.message}`);
7442
+ if (err.stack) console.error(err.stack.split("\n").slice(1).join("\n"));
7388
7443
  runCleanup();
7389
7444
  process.exit(1);
7390
7445
  });