mihomo-cli 3.5.0 → 3.7.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 = {
@@ -3047,7 +3067,14 @@ var UI_URLS = {
3047
3067
  dash: "https://metacubex.github.io/metacubexd",
3048
3068
  yacd: "https://yacd.metacubex.one"
3049
3069
  };
3050
- var LAUNCH_DAEMON_LABEL = process.env.MIHOMO_CLI_DAEMON_LABEL || "com.mihomo-cli.daemon";
3070
+ var DEFAULT_DAEMON_LABEL = "com.mihomo-cli.daemon";
3071
+ var DAEMON_LABEL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
3072
+ function isValidDaemonLabel(label) {
3073
+ return DAEMON_LABEL_RE.test(label) && !label.includes("..");
3074
+ }
3075
+ var RAW_DAEMON_LABEL = process.env.MIHOMO_CLI_DAEMON_LABEL;
3076
+ var RAW_DAEMON_LABEL_INPUT = RAW_DAEMON_LABEL;
3077
+ var LAUNCH_DAEMON_LABEL = RAW_DAEMON_LABEL && isValidDaemonLabel(RAW_DAEMON_LABEL) ? RAW_DAEMON_LABEL : DEFAULT_DAEMON_LABEL;
3051
3078
  var CONTROLLER_PORT = 9090;
3052
3079
  var CONTROLLER_ADDR = `127.0.0.1:${CONTROLLER_PORT}`;
3053
3080
  var CONTROLLER_BASE_URL = `http://${CONTROLLER_ADDR}`;
@@ -3100,6 +3127,41 @@ var AUTO_CLEAN_THRESHOLD = 100;
3100
3127
  var AUTO_CLEAN_THRESHOLD_GITHUB = 50;
3101
3128
  var AUTO_CLEAN_COOLDOWN_HOURS = 12;
3102
3129
 
3130
+ // src/errors.ts
3131
+ var TimeoutError = class extends Error {
3132
+ constructor() {
3133
+ super("timeout");
3134
+ this.name = "TimeoutError";
3135
+ }
3136
+ };
3137
+ var CliError = class extends Error {
3138
+ hint;
3139
+ label;
3140
+ exitCode;
3141
+ constructor(message, options = {}) {
3142
+ super(message);
3143
+ this.name = "CliError";
3144
+ this.label = options.label ?? "\u9519\u8BEF";
3145
+ this.hint = options.hint === void 0 ? [] : Array.isArray(options.hint) ? options.hint : [options.hint];
3146
+ this.exitCode = options.exitCode ?? 1;
3147
+ }
3148
+ };
3149
+ function withTimeout(promise, ms) {
3150
+ return new Promise((resolve, reject) => {
3151
+ const timer = setTimeout(() => reject(new TimeoutError()), ms);
3152
+ promise.then(
3153
+ (v) => {
3154
+ clearTimeout(timer);
3155
+ resolve(v);
3156
+ },
3157
+ (e) => {
3158
+ clearTimeout(timer);
3159
+ reject(e);
3160
+ }
3161
+ );
3162
+ });
3163
+ }
3164
+
3103
3165
  // src/overwrite.ts
3104
3166
  import fs3 from "fs";
3105
3167
  import path3 from "path";
@@ -3176,20 +3238,27 @@ function readSettings() {
3176
3238
  if (settingsCache !== null) return settingsCache;
3177
3239
  ensureDirs();
3178
3240
  if (fs2.existsSync(PATHS.settingsFile)) {
3241
+ let parsed;
3179
3242
  try {
3180
- const content = fs2.readFileSync(PATHS.settingsFile, "utf8");
3181
- settingsCache = JSON.parse(content);
3182
- return settingsCache;
3243
+ parsed = JSON.parse(fs2.readFileSync(PATHS.settingsFile, "utf8"));
3183
3244
  } catch {
3184
- try {
3185
- fs2.copyFileSync(PATHS.settingsFile, `${PATHS.settingsFile}.bak`);
3186
- console.warn(`\u8B66\u544A: settings.json \u683C\u5F0F\u635F\u574F\uFF0C\u5DF2\u5907\u4EFD\u5230 ${PATHS.settingsFile}.bak\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u8BBE\u7F6E`);
3187
- } catch {
3188
- console.warn("\u8B66\u544A: settings.json \u683C\u5F0F\u635F\u574F\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u8BBE\u7F6E");
3189
- }
3190
- settingsCache = {};
3191
- return settingsCache;
3245
+ return recoverCorruptedSettings();
3246
+ }
3247
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3248
+ return recoverCorruptedSettings();
3192
3249
  }
3250
+ settingsCache = parsed;
3251
+ return settingsCache;
3252
+ }
3253
+ settingsCache = {};
3254
+ return settingsCache;
3255
+ }
3256
+ function recoverCorruptedSettings() {
3257
+ try {
3258
+ fs2.copyFileSync(PATHS.settingsFile, `${PATHS.settingsFile}.bak`);
3259
+ console.warn(`\u8B66\u544A: settings.json \u683C\u5F0F\u635F\u574F\uFF0C\u5DF2\u5907\u4EFD\u5230 ${PATHS.settingsFile}.bak\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u8BBE\u7F6E`);
3260
+ } catch {
3261
+ console.warn("\u8B66\u544A: settings.json \u683C\u5F0F\u635F\u574F\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u8BBE\u7F6E");
3193
3262
  }
3194
3263
  settingsCache = {};
3195
3264
  return settingsCache;
@@ -3208,11 +3277,7 @@ function writeSettings(settings) {
3208
3277
  function invalidateSettingsCache() {
3209
3278
  settingsCache = null;
3210
3279
  }
3211
- function maskUrl(url) {
3212
- if (!url) return url;
3213
- if (url.includes(",")) {
3214
- return url.split(",").map((u) => maskUrl(u.trim())).join(", ");
3215
- }
3280
+ function maskSingleUrl(url) {
3216
3281
  try {
3217
3282
  const parsed = new URL(url);
3218
3283
  const tokenKeys = ["token", "key", "secret", "pass", "password", "auth", "access_token", "api_key"];
@@ -3232,6 +3297,26 @@ function maskUrl(url) {
3232
3297
  return url;
3233
3298
  }
3234
3299
  }
3300
+ function looksLikeMultiUrl(url) {
3301
+ if (!url.includes(",")) return false;
3302
+ const parts = url.split(",").map((u) => u.trim()).filter(Boolean);
3303
+ if (parts.length < 2) return false;
3304
+ return parts.every((p) => {
3305
+ try {
3306
+ const u = new URL(p);
3307
+ return u.protocol === "http:" || u.protocol === "https:";
3308
+ } catch {
3309
+ return false;
3310
+ }
3311
+ });
3312
+ }
3313
+ function maskUrl(url) {
3314
+ if (!url) return url;
3315
+ if (looksLikeMultiUrl(url)) {
3316
+ return url.split(",").map((u) => maskSingleUrl(u.trim())).join(", ");
3317
+ }
3318
+ return maskSingleUrl(url);
3319
+ }
3235
3320
  function readSubscriptionCache() {
3236
3321
  ensureDirs();
3237
3322
  if (fs2.existsSync(PATHS.subscriptionsCacheFile)) {
@@ -3261,7 +3346,14 @@ function saveSubscriptionCache(subName, data) {
3261
3346
  }
3262
3347
  function getSubscriptions() {
3263
3348
  const settings = readSettings();
3264
- return settings.subscriptions || [];
3349
+ const subs = settings.subscriptions;
3350
+ if (!Array.isArray(subs)) {
3351
+ if (subs !== void 0) {
3352
+ console.warn("\u8B66\u544A: settings.json \u7684 subscriptions \u4E0D\u662F\u5217\u8868\uFF0C\u5DF2\u5FFD\u7565\uFF08\u53EF\u7528 mihomo sub add \u91CD\u65B0\u6DFB\u52A0\uFF09");
3353
+ }
3354
+ return [];
3355
+ }
3356
+ return subs.filter((s) => s != null && typeof s === "object" && typeof s.name === "string" && typeof s.url === "string");
3265
3357
  }
3266
3358
  function getSubscriptionsWithCache() {
3267
3359
  const subs = getSubscriptions();
@@ -3274,15 +3366,15 @@ function getSubscriptionsWithCache() {
3274
3366
  var SAFE_NAME_RE = /^[\w\-\p{Unified_Ideograph}]{1,64}$/u;
3275
3367
  function validateSubscriptionName(name) {
3276
3368
  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`);
3369
+ 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
3370
  }
3279
3371
  }
3280
3372
  function addSubscription(url, name = "default") {
3281
3373
  validateSubscriptionName(name);
3282
3374
  const settings = readSettings();
3283
- const subs = [...settings.subscriptions || []];
3375
+ const subs = [...getSubscriptions()];
3284
3376
  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`);
3377
+ 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
3378
  }
3287
3379
  subs.push({ name, url });
3288
3380
  const updates = { subscriptions: subs };
@@ -3293,7 +3385,7 @@ function addSubscription(url, name = "default") {
3293
3385
  }
3294
3386
  function removeSubscription(name) {
3295
3387
  const settings = readSettings();
3296
- const subs = [...settings.subscriptions || []];
3388
+ const subs = [...getSubscriptions()];
3297
3389
  const idx = subs.findIndex((s) => s.name === name);
3298
3390
  if (idx < 0) return null;
3299
3391
  subs.splice(idx, 1);
@@ -3317,7 +3409,7 @@ function removeSubscription(name) {
3317
3409
  }
3318
3410
  function setDefaultSubscription(name) {
3319
3411
  const settings = readSettings();
3320
- const subs = settings.subscriptions || [];
3412
+ const subs = getSubscriptions();
3321
3413
  const idx = subs.findIndex((s) => s.name === name);
3322
3414
  if (idx < 0) return false;
3323
3415
  if (settings.active_subscription === name) return true;
@@ -3326,7 +3418,7 @@ function setDefaultSubscription(name) {
3326
3418
  }
3327
3419
  function getSubscriptionRawConfigPath(subName) {
3328
3420
  if (!SAFE_NAME_RE.test(subName)) {
3329
- throw new Error(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${subName}"`);
3421
+ throw new CliError(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${subName}"`);
3330
3422
  }
3331
3423
  return path2.join(DIRS.subscriptions, `${subName}.yaml`);
3332
3424
  }
@@ -3402,6 +3494,18 @@ function deepMergeWithOverrides(target, override) {
3402
3494
  const { key, forceOverwrite, arrayPrepend, arrayAppend, arrayMergeByName } = parseOverrideKey(rawKey);
3403
3495
  const existingValue = result[key];
3404
3496
  if (arrayMergeByName) {
3497
+ if (existingValue !== void 0 && !Array.isArray(existingValue)) {
3498
+ throw new CliError(
3499
+ `\u8986\u5199\u952E "${rawKey}" \u7684 ~ \u8BED\u4E49\u53EA\u9002\u7528\u4E8E\u6570\u7EC4\uFF0C\u4F46 "${key}" \u5F53\u524D\u662F${existingValue === null ? " null" : typeof existingValue === "object" ? "\u6620\u5C04" : `\u6807\u91CF\uFF08${typeof existingValue}\uFF09`}`,
3500
+ {
3501
+ label: "\u8986\u5199\u914D\u7F6E\u9519\u8BEF",
3502
+ hint: [
3503
+ `~${key} \u7528\u4E8E\u6309 name \u5C31\u5730\u5408\u5E76\u6570\u7EC4\u5143\u7D20\uFF08\u5982 ~proxy-groups\uFF09\u3002`,
3504
+ `\u82E5\u8981\u8986\u76D6\u975E\u6570\u7EC4\u7684 ${key}\uFF0C\u8BF7\u7528 ${key}!\uFF08\u5F3A\u5236\u8986\u76D6\uFF09\u6216\u76F4\u63A5\u5199 ${key}\uFF08\u6DF1\u5EA6\u5408\u5E76\uFF09\u3002`
3505
+ ]
3506
+ }
3507
+ );
3508
+ }
3405
3509
  const existingArr = Array.isArray(existingValue) ? existingValue : [];
3406
3510
  const overrideArr = Array.isArray(value) ? value : [value];
3407
3511
  const merged = [...existingArr];
@@ -3418,6 +3522,15 @@ function deepMergeWithOverrides(target, override) {
3418
3522
  continue;
3419
3523
  }
3420
3524
  if (arrayPrepend || arrayAppend) {
3525
+ if (existingValue !== void 0 && !Array.isArray(existingValue)) {
3526
+ throw new CliError(
3527
+ `\u8986\u5199\u952E "${rawKey}" \u7684\u6570\u7EC4\u62FC\u63A5\u8BED\u4E49\u53EA\u9002\u7528\u4E8E\u6570\u7EC4\uFF0C\u4F46 "${key}" \u5F53\u524D\u662F${existingValue === null ? " null" : typeof existingValue === "object" ? "\u6620\u5C04" : `\u6807\u91CF\uFF08${typeof existingValue}\uFF09`}`,
3528
+ {
3529
+ label: "\u8986\u5199\u914D\u7F6E\u9519\u8BEF",
3530
+ hint: [`+${key} / ${key}+ \u7528\u4E8E\u5411\u6570\u7EC4\u524D\u7F6E/\u8FFD\u52A0\u5143\u7D20\uFF08\u5982 rules+\uFF09\u3002`, `\u82E5\u8981\u66FF\u6362\u975E\u6570\u7EC4\u7684 ${key}\uFF0C\u8BF7\u76F4\u63A5\u5199 ${key}: <\u503C>\u3002`]
3531
+ }
3532
+ );
3533
+ }
3421
3534
  const existingArr = Array.isArray(existingValue) ? existingValue : [];
3422
3535
  const overrideArr = Array.isArray(value) ? value : [value];
3423
3536
  if (arrayPrepend) {
@@ -3480,7 +3593,18 @@ function summarizeMatch(match) {
3480
3593
  return parts.length > 0 ? parts.join(", ") : void 0;
3481
3594
  }
3482
3595
  function splitUrlsLocal(url) {
3483
- return url.split(",").map((u) => u.trim()).filter(Boolean);
3596
+ const isValidHttp = (u) => {
3597
+ try {
3598
+ const parsed = new URL(u.trim());
3599
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
3600
+ } catch {
3601
+ return false;
3602
+ }
3603
+ };
3604
+ if (!url.includes(",")) return [url.trim()];
3605
+ const parts = url.split(",").map((u) => u.trim()).filter(Boolean);
3606
+ if (parts.length > 1 && parts.every(isValidHttp)) return parts;
3607
+ return [url.trim()];
3484
3608
  }
3485
3609
  function hostMatchesDomain(host, domain) {
3486
3610
  const h = host.toLowerCase();
@@ -3491,7 +3615,9 @@ function matchesScope(match, scope) {
3491
3615
  if (!match) return true;
3492
3616
  if (match.subscription) {
3493
3617
  const names = Array.isArray(match.subscription) ? match.subscription : [match.subscription];
3494
- if (!scope?.subName || !names.includes(scope.subName)) return false;
3618
+ if (!scope?.subName) return false;
3619
+ const subName = scope.subName.toLowerCase();
3620
+ if (!names.some((n) => n.toLowerCase() === subName)) return false;
3495
3621
  }
3496
3622
  if (match["url-domain"]) {
3497
3623
  const domains = Array.isArray(match["url-domain"]) ? match["url-domain"] : [match["url-domain"]];
@@ -3564,26 +3690,7 @@ function listOverwriteFile() {
3564
3690
  }
3565
3691
 
3566
3692
  // 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
3693
  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
3694
  function sleepSync(ms) {
3588
3695
  Atomics.wait(sleepBuf, 0, 0, ms);
3589
3696
  }
@@ -3596,39 +3703,6 @@ function escapeRegExp(s) {
3596
3703
  function shellQuote(s) {
3597
3704
  return `'${s.replace(/'/g, "'\\''")}'`;
3598
3705
  }
3599
- var TimeoutError = class extends Error {
3600
- constructor() {
3601
- super("timeout");
3602
- this.name = "TimeoutError";
3603
- }
3604
- };
3605
- var CliError = class extends Error {
3606
- hint;
3607
- label;
3608
- exitCode;
3609
- constructor(message, options = {}) {
3610
- super(message);
3611
- this.name = "CliError";
3612
- this.label = options.label ?? "\u9519\u8BEF";
3613
- this.hint = options.hint === void 0 ? [] : Array.isArray(options.hint) ? options.hint : [options.hint];
3614
- this.exitCode = options.exitCode ?? 1;
3615
- }
3616
- };
3617
- function withTimeout(promise, ms) {
3618
- return new Promise((resolve, reject) => {
3619
- const timer = setTimeout(() => reject(new TimeoutError()), ms);
3620
- promise.then(
3621
- (v) => {
3622
- clearTimeout(timer);
3623
- resolve(v);
3624
- },
3625
- (e) => {
3626
- clearTimeout(timer);
3627
- reject(e);
3628
- }
3629
- );
3630
- });
3631
- }
3632
3706
  function formatBytes(bytes) {
3633
3707
  if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
3634
3708
  const num = Number(bytes);
@@ -3667,15 +3741,25 @@ function hasFlag(args, short, long) {
3667
3741
  }
3668
3742
  function parseIntArg(args, short, long, defaultValue) {
3669
3743
  if (!args) return defaultValue;
3744
+ const parse = (raw, flag) => {
3745
+ if (!/^\d+$/.test(raw.trim())) {
3746
+ throw new CliError(`\u9009\u9879 ${flag} \u9700\u8981\u6B63\u6574\u6570\uFF0C\u6536\u5230 "${raw}"`, { hint: [`\u4F8B\u5982: ${flag} ${defaultValue}`] });
3747
+ }
3748
+ const val = Number(raw);
3749
+ if (!Number.isSafeInteger(val) || val < 1) {
3750
+ throw new CliError(`\u9009\u9879 ${flag} \u9700\u8981 >= 1 \u7684\u6574\u6570\uFF0C\u6536\u5230 "${raw}"`, { hint: [`\u4F8B\u5982: ${flag} ${defaultValue}`] });
3751
+ }
3752
+ return val;
3753
+ };
3670
3754
  for (let i = 0; i < args.length; i++) {
3671
3755
  if (args[i] === short || args[i] === long) {
3672
3756
  if (i + 1 < args.length) {
3673
- const val = parseInt(args[i + 1], 10);
3674
- return Number.isNaN(val) ? defaultValue : val;
3757
+ return parse(args[i + 1], args[i]);
3675
3758
  }
3676
- } else if (args[i].startsWith(`${long}=`)) {
3677
- const val = parseInt(args[i].slice(long.length + 1), 10);
3678
- if (!Number.isNaN(val)) return val;
3759
+ throw new CliError(`\u9009\u9879 ${args[i]} \u7F3A\u5C11\u503C`, { hint: [`\u4F8B\u5982: ${args[i]} ${defaultValue}`] });
3760
+ }
3761
+ if (args[i].startsWith(`${long}=`)) {
3762
+ return parse(args[i].slice(long.length + 1), long);
3679
3763
  }
3680
3764
  }
3681
3765
  return defaultValue;
@@ -3708,90 +3792,35 @@ function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
3708
3792
  }
3709
3793
  return null;
3710
3794
  }
3711
- function isProcessRunning(pid) {
3712
- if (!pid) return false;
3713
- try {
3714
- const result = spawnSync("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: 5e3 });
3715
- return (result.stdout || "").trim().length > 0;
3716
- } catch {
3717
- return false;
3718
- }
3719
- }
3720
- function isProcessCommandMatching(pid, needle) {
3721
- if (!pid) return false;
3722
- try {
3723
- const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 5e3 });
3724
- return (result.stdout || "").includes(needle);
3725
- } catch {
3726
- return false;
3727
- }
3728
- }
3729
- function isProcessRoot(pid) {
3730
- if (!pid) return false;
3731
- try {
3732
- const result = spawnSync("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: 5e3 });
3733
- return (result.stdout || "").trim() === "0";
3734
- } catch {
3735
- return false;
3736
- }
3737
- }
3738
- function createHttpClient(options = {}) {
3739
- const { timeout = 6e4, secret } = options;
3740
- const authHeaders = secret ? { Authorization: `Bearer ${secret}` } : {};
3741
- return {
3742
- async get(url, config) {
3743
- const controller = new AbortController();
3744
- const timer = setTimeout(() => controller.abort(), timeout);
3745
- const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
3746
- try {
3747
- const response = await fetch(url, {
3748
- signal,
3749
- headers: { "User-Agent": `mihomo-cli/${VERSION}`, ...authHeaders }
3750
- });
3751
- if (!response.ok) {
3752
- const error = new Error(`HTTP ${response.status}`);
3753
- error.response = { status: response.status };
3754
- try {
3755
- error.response.data = await response.json();
3756
- } catch {
3757
- }
3758
- throw error;
3759
- }
3760
- const declaredLen = Number(response.headers.get("content-length"));
3761
- if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
3762
- throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
3763
- }
3764
- const text = await readBodyWithLimit(response, controller);
3765
- const data = config?.responseType === "json" ? JSON.parse(text) : text;
3766
- return { data, headers: response.headers, status: response.status };
3767
- } finally {
3768
- clearTimeout(timer);
3769
- }
3795
+ function levenshtein(a, b) {
3796
+ if (a === b) return 0;
3797
+ if (a.length === 0) return b.length;
3798
+ if (b.length === 0) return a.length;
3799
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
3800
+ for (let i = 1; i <= a.length; i++) {
3801
+ const curr = [i];
3802
+ for (let j = 1; j <= b.length; j++) {
3803
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
3770
3804
  }
3771
- };
3805
+ prev = curr;
3806
+ }
3807
+ return prev[b.length];
3772
3808
  }
3773
- async function readBodyWithLimit(response, controller) {
3774
- if (!response.body) return response.text();
3775
- const reader = response.body.getReader();
3776
- const chunks = [];
3777
- let total = 0;
3778
- try {
3779
- while (true) {
3780
- const { done, value } = await reader.read();
3781
- if (done) break;
3782
- if (value) {
3783
- total += value.byteLength;
3784
- if (total > MAX_RESPONSE_BYTES) {
3785
- controller.abort();
3786
- throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
3787
- }
3788
- chunks.push(value);
3789
- }
3809
+ function suggestSimilar(input, candidates) {
3810
+ const lower = input.toLowerCase();
3811
+ const scored = [];
3812
+ for (const cand of candidates) {
3813
+ const c = cand.toLowerCase();
3814
+ if (cand === input) continue;
3815
+ if (c.startsWith(lower)) {
3816
+ scored.push({ name: cand, score: 0, lenDiff: Math.abs(cand.length - input.length) });
3817
+ } else if (lower.length >= 3) {
3818
+ const d = levenshtein(lower, c);
3819
+ if (d <= 2) scored.push({ name: cand, score: d, lenDiff: Math.abs(cand.length - input.length) });
3790
3820
  }
3791
- } finally {
3792
- reader.releaseLock();
3793
3821
  }
3794
- return Buffer.concat(chunks).toString("utf8");
3822
+ scored.sort((a, b) => a.score - b.score || a.lenDiff - b.lenDiff);
3823
+ return scored.slice(0, 3).map((s) => s.name);
3795
3824
  }
3796
3825
  function normalizeMirrorUrl(val) {
3797
3826
  if (!val) return null;
@@ -3834,15 +3863,6 @@ function parseMirrorArg(args) {
3834
3863
  }
3835
3864
  return { mirror: null, isOverride: false, type: "download" };
3836
3865
  }
3837
- function isProxyValid(proxy) {
3838
- if (!proxy.name || !proxy.server || !proxy.port) return false;
3839
- if (!proxy.type) return false;
3840
- if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3841
- const pw = String(proxy.password || "");
3842
- if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
3843
- }
3844
- return true;
3845
- }
3846
3866
 
3847
3867
  // src/config.ts
3848
3868
  var SAFE_YAML_LOAD_OPTIONS = { maxAliases: 200 };
@@ -3888,7 +3908,7 @@ function excludeOverwriteProxiesFromIncludeAll(config, overwriteFiles) {
3888
3908
  if (injectedNames.length === 0) return;
3889
3909
  const groups = config["proxy-groups"];
3890
3910
  if (!groups) return;
3891
- const excludePattern = injectedNames.map((n) => escapeRegExp(n)).join("|");
3911
+ const excludePattern = `^(?:${injectedNames.map((n) => escapeRegExp(n)).join("|")})$`;
3892
3912
  for (const group of groups) {
3893
3913
  if (!group["include-all"] && !group["include-all-proxies"]) continue;
3894
3914
  const existing = group["exclude-filter"];
@@ -3923,7 +3943,58 @@ function getRuleTarget(rule) {
3923
3943
  }
3924
3944
  return last;
3925
3945
  }
3946
+ function assertConfigShape(config) {
3947
+ const listSections = [
3948
+ { key: "proxies", label: "\u8282\u70B9", needsName: true },
3949
+ { key: "proxy-groups", label: "\u4EE3\u7406\u7EC4", needsName: true },
3950
+ { key: "rules", label: "\u89C4\u5219", needsName: false }
3951
+ ];
3952
+ for (const { key, label, needsName } of listSections) {
3953
+ const value = config[key];
3954
+ if (value === void 0 || value === null) continue;
3955
+ if (!Array.isArray(value)) {
3956
+ throw new CliError(`${key} \u5FC5\u987B\u662F\u5217\u8868\uFF0C\u5F53\u524D\u4E3A ${typeof value === "object" ? "\u6620\u5C04" : typeof value}`, {
3957
+ label: "\u914D\u7F6E\u9519\u8BEF",
3958
+ hint: [
3959
+ `${label}\u6BB5\uFF08${key}\uFF09\u9700\u5199\u6210 YAML \u5217\u8868\uFF0C\u6BCF\u9879\u4EE5 "- " \u5F00\u5934\u3002`,
3960
+ `\u4F8B\u5982: ${key}:`,
3961
+ key === "rules" ? " - MATCH,DIRECT" : " - {name: xxx, ...}"
3962
+ ]
3963
+ });
3964
+ }
3965
+ for (let i = 0; i < value.length; i++) {
3966
+ const item = value[i];
3967
+ if (item === null || item === void 0) {
3968
+ throw new CliError(`${key}[${i}] \u4E3A\u7A7A`, {
3969
+ label: "\u914D\u7F6E\u9519\u8BEF",
3970
+ hint: [`${label}\u6BB5\uFF08${key}\uFF09\u7B2C ${i + 1} \u9879\u662F\u7A7A\u503C\uFF0C\u901A\u5E38\u662F\u5217\u8868\u91CC\u7559\u4E86\u7A7A\u7684 "- " \u884C\u3002`]
3971
+ });
3972
+ }
3973
+ if (needsName) {
3974
+ if (typeof item !== "object" || Array.isArray(item)) {
3975
+ throw new CliError(`${key}[${i}] \u5FC5\u987B\u662F\u6620\u5C04`, {
3976
+ label: "\u914D\u7F6E\u9519\u8BEF",
3977
+ hint: [`${label}\u6BB5\uFF08${key}\uFF09\u7B2C ${i + 1} \u9879\u5E94\u4E3A {name: ..., ...} \u5F62\u5F0F\uFF0C\u5F53\u524D\u662F ${Array.isArray(item) ? "\u5217\u8868" : typeof item}\u3002`]
3978
+ });
3979
+ }
3980
+ const name = item.name;
3981
+ if (typeof name !== "string" || name === "") {
3982
+ throw new CliError(`${key}[${i}] \u7F3A\u5C11\u6709\u6548\u7684 name`, {
3983
+ label: "\u914D\u7F6E\u9519\u8BEF",
3984
+ hint: [`${label}\u6BB5\uFF08${key}\uFF09\u7B2C ${i + 1} \u9879\u6CA1\u6709 name \u5B57\u6BB5\uFF08\u6216\u4E3A\u7A7A\uFF09\uFF0Cmihomo \u4F1A\u62D2\u7EDD\u542F\u52A8\u3002`]
3985
+ });
3986
+ }
3987
+ } else if (typeof item !== "string") {
3988
+ throw new CliError(`${key}[${i}] \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`, {
3989
+ label: "\u914D\u7F6E\u9519\u8BEF",
3990
+ hint: [`${label}\u6BB5\uFF08${key}\uFF09\u7B2C ${i + 1} \u9879\u5E94\u4E3A\u5F62\u5982 "MATCH,DIRECT" \u7684\u5B57\u7B26\u4E32\uFF0C\u5F53\u524D\u662F ${typeof item}\u3002`]
3991
+ });
3992
+ }
3993
+ }
3994
+ }
3995
+ }
3926
3996
  function validateConfig(config) {
3997
+ assertConfigShape(config);
3927
3998
  const warnings = [];
3928
3999
  const proxies = config.proxies || [];
3929
4000
  const groups = config["proxy-groups"] || [];
@@ -4097,7 +4168,7 @@ function getKernelVersion() {
4097
4168
  }
4098
4169
  if (kernelVersionCached) return kernelVersionCache;
4099
4170
  try {
4100
- const result = spawnSync2(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
4171
+ const result = spawnSync(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
4101
4172
  const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
4102
4173
  if (output) {
4103
4174
  const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
@@ -4191,12 +4262,12 @@ function printVersion() {
4191
4262
  }
4192
4263
 
4193
4264
  // src/daemon.ts
4194
- import { spawnSync as spawnSync4 } from "child_process";
4265
+ import { spawnSync as spawnSync3 } from "child_process";
4195
4266
  import fs6 from "fs";
4196
4267
  import path5 from "path";
4197
4268
 
4198
4269
  // src/process.ts
4199
- import { spawn, spawnSync as spawnSync3 } from "child_process";
4270
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
4200
4271
  import fs5 from "fs";
4201
4272
  import path4 from "path";
4202
4273
 
@@ -4233,6 +4304,34 @@ var SUDO_TIMEOUT_MS = 6e4;
4233
4304
  var TUN_MODE_POST_WAIT_MS = 500;
4234
4305
  var BATCH_KILL_THRESHOLD = 3;
4235
4306
  var DEFAULT_LOG_RETENTION_DAYS = 7;
4307
+ var PS_TIMEOUT_MS = 5e3;
4308
+ function isProcessRunning(pid) {
4309
+ if (!pid) return false;
4310
+ try {
4311
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
4312
+ return (result.stdout || "").trim().length > 0;
4313
+ } catch {
4314
+ return false;
4315
+ }
4316
+ }
4317
+ function isProcessCommandMatching(pid, needle) {
4318
+ if (!pid) return false;
4319
+ try {
4320
+ const result = spawnSync2("ps", ["-ww", "-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
4321
+ return (result.stdout || "").includes(needle);
4322
+ } catch {
4323
+ return false;
4324
+ }
4325
+ }
4326
+ function isProcessRoot(pid) {
4327
+ if (!pid) return false;
4328
+ try {
4329
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
4330
+ return (result.stdout || "").trim() === "0";
4331
+ } catch {
4332
+ return false;
4333
+ }
4334
+ }
4236
4335
  var MAIN_INSTANCE_PATTERN = `${escapeRegExp(PATHS.mihomoBinary)}.*${escapeRegExp(PATHS.configFile)}`;
4237
4336
  function clearRuntime() {
4238
4337
  if (fs5.existsSync(DIRS.runtime)) {
@@ -4251,11 +4350,12 @@ function getPid() {
4251
4350
  }
4252
4351
  function isRunning() {
4253
4352
  const pid = getPid();
4254
- return pid ? isProcessRunning(pid) : false;
4353
+ if (!pid) return false;
4354
+ return isProcessRunning(pid) && isProcessCommandMatching(pid, PATHS.mihomoBinary);
4255
4355
  }
4256
4356
  function getMihomoPids() {
4257
4357
  try {
4258
- const result = spawnSync3("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
4358
+ const result = spawnSync2("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
4259
4359
  const output = (result.stdout || "").trim();
4260
4360
  if (!output) return [];
4261
4361
  return output.split("\n").filter(Boolean).map((p) => parseInt(p, 10)).filter((p) => Number.isInteger(p) && p > 0);
@@ -4295,7 +4395,7 @@ function clearPid() {
4295
4395
  if (!fs5.existsSync(PATHS.pidFile)) return;
4296
4396
  if (isPidFileOwnedByRoot()) {
4297
4397
  try {
4298
- spawnSync3("sudo", ["rm", "-f", PATHS.pidFile], { stdio: "inherit", timeout: 1e4 });
4398
+ spawnSync2("sudo", ["rm", "-f", PATHS.pidFile], { stdio: "inherit", timeout: 1e4 });
4299
4399
  } catch {
4300
4400
  }
4301
4401
  } else {
@@ -4317,14 +4417,14 @@ function killAllMihomo(forceSudo = false) {
4317
4417
  const pattern = MAIN_INSTANCE_PATTERN;
4318
4418
  if (forceSudo) {
4319
4419
  try {
4320
- spawnSync3("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
4420
+ spawnSync2("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
4321
4421
  return true;
4322
4422
  } catch {
4323
4423
  return false;
4324
4424
  }
4325
4425
  } else {
4326
4426
  try {
4327
- spawnSync3("pkill", ["-9", "-f", pattern], { timeout: 1e4 });
4427
+ spawnSync2("pkill", ["-9", "-f", pattern], { timeout: 1e4 });
4328
4428
  return true;
4329
4429
  } catch {
4330
4430
  return false;
@@ -4420,7 +4520,7 @@ exit 2
4420
4520
  }
4421
4521
  function getProcessInfo(pid) {
4422
4522
  try {
4423
- const result = spawnSync3("ps", ["-p", String(pid), "-o", "rss="], { encoding: "utf8", timeout: 5e3 });
4523
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "rss="], { encoding: "utf8", timeout: 5e3 });
4424
4524
  const psOutput = (result.stdout || "").trim();
4425
4525
  if (!psOutput) return null;
4426
4526
  const rss = parseInt(psOutput, 10);
@@ -4454,11 +4554,11 @@ async function start(mode = "mixed") {
4454
4554
  rotateAndCleanupLogs();
4455
4555
  const binary = PATHS.mihomoBinary;
4456
4556
  if (!fs5.existsSync(binary)) {
4457
- throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4557
+ throw new CliError("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4458
4558
  }
4459
4559
  const configFile = PATHS.configFile;
4460
4560
  if (!fs5.existsSync(configFile)) {
4461
- throw new Error("\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605\u5E76\u542F\u52A8");
4561
+ throw new CliError("\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605\u5E76\u542F\u52A8");
4462
4562
  }
4463
4563
  const staleState = checkStaleState();
4464
4564
  if (isTunMode) {
@@ -4535,7 +4635,7 @@ async function startTunMode(staleState) {
4535
4635
  }
4536
4636
  console.log("TUN \u6A21\u5F0F\u9700\u8981 sudo \u6743\u9650...");
4537
4637
  try {
4538
- const result = spawnSync3("sudo", [launchScript], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4638
+ const result = spawnSync2("sudo", [launchScript], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4539
4639
  if (result.error) throw result.error;
4540
4640
  if (result.status !== 0) {
4541
4641
  const err = new Error("TUN \u542F\u52A8\u811A\u672C\u6267\u884C\u5931\u8D25");
@@ -4727,6 +4827,17 @@ function viewLogWithTail(logPath, options) {
4727
4827
 
4728
4828
  // src/daemon.ts
4729
4829
  var SERVICE_TARGET = `system/${LAUNCH_DAEMON_LABEL}`;
4830
+ function assertDaemonLabelSafe() {
4831
+ if (RAW_DAEMON_LABEL_INPUT !== void 0 && !isValidDaemonLabel(RAW_DAEMON_LABEL_INPUT)) {
4832
+ throw new CliError(`MIHOMO_CLI_DAEMON_LABEL \u65E0\u6548: "${RAW_DAEMON_LABEL_INPUT}"`, {
4833
+ label: "\u914D\u7F6E\u9519\u8BEF",
4834
+ hint: [
4835
+ '\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\uFF0C\u4E14\u4E0D\u80FD\u542B ".."\u3002',
4836
+ "\u8BE5\u503C\u4F1A\u6210\u4E3A /Library/LaunchDaemons/ \u4E0B\u7684 plist \u6587\u4EF6\u540D\uFF0C\u5E76\u4F5C\u4E3A root \u5199\u5165/\u5220\u9664\u7684\u76EE\u6807\u8DEF\u5F84\u3002"
4837
+ ]
4838
+ });
4839
+ }
4840
+ }
4730
4841
  var HOT_RELOAD_TIMEOUT_MS = 5e3;
4731
4842
  var DAEMON_BOOT_WAIT_MS = 500;
4732
4843
  var LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
@@ -4775,7 +4886,7 @@ function runSudoScript(scriptBody, opts) {
4775
4886
  const scriptPath = path5.join(DIRS.runtime, opts.file);
4776
4887
  fs6.writeFileSync(scriptPath, scriptBody, { mode: 448 });
4777
4888
  try {
4778
- const result = spawnSync4("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4889
+ const result = spawnSync3("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4779
4890
  if (result.error) throw result.error;
4780
4891
  if (result.status !== 0) {
4781
4892
  if (result.status === 1) {
@@ -4808,11 +4919,12 @@ function isDaemonRunning(status) {
4808
4919
  return status.loaded && status.pid !== null;
4809
4920
  }
4810
4921
  function enableDaemon() {
4922
+ assertDaemonLabelSafe();
4811
4923
  if (!fs6.existsSync(PATHS.mihomoBinary)) {
4812
- throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4924
+ throw new CliError("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4813
4925
  }
4814
4926
  if (!fs6.existsSync(PATHS.configFile)) {
4815
- throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4927
+ throw new CliError("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4816
4928
  }
4817
4929
  ensureDirs();
4818
4930
  const stagePath = path5.join(DIRS.runtime, "daemon.plist.stage");
@@ -4848,6 +4960,7 @@ function enableDaemon() {
4848
4960
  }
4849
4961
  }
4850
4962
  function disableDaemon() {
4963
+ assertDaemonLabelSafe();
4851
4964
  if (!isDaemonEnabled()) return;
4852
4965
  const target = shellQuote(SERVICE_TARGET);
4853
4966
  const plistDest = shellQuote(PATHS.launchDaemonPlist);
@@ -4892,7 +5005,7 @@ async function tryHotReload() {
4892
5005
  }
4893
5006
  async function restartDaemon() {
4894
5007
  if (!fs6.existsSync(PATHS.launchDaemonPlist)) {
4895
- throw new Error("\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u6CD5\u91CD\u542F");
5008
+ throw new CliError("\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u6CD5\u91CD\u542F", { hint: "\u542F\u7528\u4FDD\u6D3B: mihomo daemon on" });
4896
5009
  }
4897
5010
  if (!logOversized() && await tryHotReload()) return;
4898
5011
  const target = shellQuote(SERVICE_TARGET);
@@ -4917,6 +5030,69 @@ async function restartDaemon() {
4917
5030
  cleanupOldLogs();
4918
5031
  }
4919
5032
 
5033
+ // src/http.ts
5034
+ var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
5035
+ var MAX_ERROR_BODY_BYTES = 64 * 1024;
5036
+ function createHttpClient(options = {}) {
5037
+ const { timeout = 6e4, secret } = options;
5038
+ const authHeaders = secret ? { Authorization: `Bearer ${secret}` } : {};
5039
+ return {
5040
+ async get(url, config) {
5041
+ const controller = new AbortController();
5042
+ const timer = setTimeout(() => controller.abort(), timeout);
5043
+ const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
5044
+ try {
5045
+ const response = await fetch(url, {
5046
+ signal,
5047
+ headers: { "User-Agent": `mihomo-cli/${VERSION}`, ...authHeaders }
5048
+ });
5049
+ if (!response.ok) {
5050
+ const error = new Error(`HTTP ${response.status}`);
5051
+ error.response = { status: response.status };
5052
+ try {
5053
+ const text2 = await readBodyWithLimit(response, controller, MAX_ERROR_BODY_BYTES);
5054
+ error.response.data = JSON.parse(text2);
5055
+ } catch {
5056
+ }
5057
+ throw error;
5058
+ }
5059
+ const declaredLen = Number(response.headers.get("content-length"));
5060
+ if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
5061
+ throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
5062
+ }
5063
+ const text = await readBodyWithLimit(response, controller, MAX_RESPONSE_BYTES);
5064
+ const data = config?.responseType === "json" ? JSON.parse(text) : text;
5065
+ return { data, headers: response.headers, status: response.status };
5066
+ } finally {
5067
+ clearTimeout(timer);
5068
+ }
5069
+ }
5070
+ };
5071
+ }
5072
+ async function readBodyWithLimit(response, controller, limit) {
5073
+ if (!response.body) return response.text();
5074
+ const reader = response.body.getReader();
5075
+ const chunks = [];
5076
+ let total = 0;
5077
+ try {
5078
+ while (true) {
5079
+ const { done, value } = await reader.read();
5080
+ if (done) break;
5081
+ if (value) {
5082
+ total += value.byteLength;
5083
+ if (total > limit) {
5084
+ controller.abort();
5085
+ throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(limit)}\uFF09`);
5086
+ }
5087
+ chunks.push(value);
5088
+ }
5089
+ }
5090
+ } finally {
5091
+ reader.releaseLock();
5092
+ }
5093
+ return Buffer.concat(chunks).toString("utf8");
5094
+ }
5095
+
4920
5096
  // src/subscription.ts
4921
5097
  function isGithubUrl(url) {
4922
5098
  const githubRe = /github\.com|raw\.githubusercontent\.com/i;
@@ -4933,7 +5109,9 @@ function resolveUpdateInterval(url, cachedInterval) {
4933
5109
  }
4934
5110
  var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4935
5111
  function isMultiUrl(url) {
4936
- return url.includes(",");
5112
+ if (!url.includes(",")) return false;
5113
+ const parts = url.split(",").map((u) => u.trim()).filter(Boolean);
5114
+ return parts.length > 1 && parts.every(isValidHttpUrl);
4937
5115
  }
4938
5116
  function isValidHttpUrl(url) {
4939
5117
  try {
@@ -4944,12 +5122,13 @@ function isValidHttpUrl(url) {
4944
5122
  }
4945
5123
  }
4946
5124
  function splitUrls(url) {
5125
+ if (!isMultiUrl(url)) return [url.trim()];
4947
5126
  return url.split(",").map((u) => u.trim()).filter(Boolean);
4948
5127
  }
4949
5128
  function loadSubscriptionConfig(subName) {
4950
5129
  const rawContent = readSubscriptionRawConfig(subName);
4951
5130
  if (!rawContent) {
4952
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
5131
+ throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u66F4\u65B0\u8BA2\u9605\uFF08mihomo sub update ${subName}\uFF09`);
4953
5132
  }
4954
5133
  const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4955
5134
  return {
@@ -5066,6 +5245,23 @@ function pickSingleSubscription(subs, pattern) {
5066
5245
  function resolveSubscription(subs, pattern) {
5067
5246
  return pickSingleSubscription(findSubscriptionFuzzy(subs, pattern), pattern);
5068
5247
  }
5248
+ function assertLooksLikeSubscription(parsed, maskedUrl) {
5249
+ const hasProxies = Array.isArray(parsed.proxies) && parsed.proxies.length > 0;
5250
+ const hasGroups = Array.isArray(parsed["proxy-groups"]) && parsed["proxy-groups"].length > 0;
5251
+ const providers = parsed["proxy-providers"];
5252
+ const hasProviders = providers != null && typeof providers === "object" && Object.keys(providers).length > 0;
5253
+ if (hasProxies || hasGroups || hasProviders) return;
5254
+ const serverMsg = ["error", "message", "msg", "info"].map((k) => parsed[k]).find((v) => typeof v === "string" && v.length > 0);
5255
+ throw new CliError("\u8BA2\u9605\u5185\u5BB9\u4E0D\u542B\u4EFB\u4F55\u8282\u70B9\u6765\u6E90\uFF08proxies / proxy-groups / proxy-providers \u5747\u4E3A\u7A7A\uFF09", {
5256
+ label: "\u8BA2\u9605\u65E0\u6548",
5257
+ hint: [
5258
+ ...serverMsg ? [`\u670D\u52A1\u7AEF\u8FD4\u56DE: ${serverMsg}`] : [],
5259
+ `URL: ${maskedUrl}`,
5260
+ "\u5E38\u89C1\u539F\u56E0\uFF1A\u8BA2\u9605\u94FE\u63A5\u8FC7\u671F\u3001\u6D41\u91CF\u8017\u5C3D\u3001\u9700\u8981\u91CD\u65B0\u83B7\u53D6\u8BA2\u9605\u5730\u5740\u3002",
5261
+ "\u78C1\u76D8\u4E0A\u539F\u6709\u7684\u8BA2\u9605\u914D\u7F6E\u672A\u88AB\u8986\u76D6\u3002"
5262
+ ]
5263
+ });
5264
+ }
5069
5265
  async function downloadSubscription(url, subName = "default", signal, persist = true) {
5070
5266
  let response;
5071
5267
  try {
@@ -5087,6 +5283,7 @@ async function downloadSubscription(url, subName = "default", signal, persist =
5087
5283
  }
5088
5284
  const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
5089
5285
  if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
5286
+ assertLooksLikeSubscription(parsed, maskUrl(url));
5090
5287
  if (persist) {
5091
5288
  saveSubscriptionRawConfig(subName, content);
5092
5289
  }
@@ -5119,12 +5316,13 @@ async function downloadMergedSubscription(urls, subName, signal, persist = true)
5119
5316
  }
5120
5317
  })
5121
5318
  );
5122
- for (const r of responses) {
5123
- if (r.error) {
5124
- const maskedUrl = maskUrl(r.url);
5125
- throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${r.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${r.error.message}
5319
+ const failures = responses.filter((r) => r.error);
5320
+ if (failures.length > 0) {
5321
+ const isAbort = (e) => e.name === "AbortError" || /abort/i.test(e.message);
5322
+ const real = failures.find((r) => !isAbort(r.error)) ?? failures[0];
5323
+ const maskedUrl = maskUrl(real.url);
5324
+ throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${real.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${real.error.message}
5126
5325
  URL: ${maskedUrl}`);
5127
- }
5128
5326
  }
5129
5327
  const parsed = responses.map((r, i) => {
5130
5328
  const content = r.response?.data;
@@ -5145,6 +5343,7 @@ async function downloadMergedSubscription(urls, subName, signal, persist = true)
5145
5343
  }
5146
5344
  base.proxies = baseProxies;
5147
5345
  const mergedContent = dumpYaml(base);
5346
+ assertLooksLikeSubscription(base, urls.map((u) => maskUrl(u)).join(", "));
5148
5347
  if (persist) {
5149
5348
  saveSubscriptionRawConfig(subName, mergedContent);
5150
5349
  }
@@ -5165,7 +5364,7 @@ async function downloadMergedSubscription(urls, subName, signal, persist = true)
5165
5364
  function prepareConfigForStart(mode, subName = "default") {
5166
5365
  const rawContent = readSubscriptionRawConfig(subName);
5167
5366
  if (!rawContent) {
5168
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
5367
+ throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
5169
5368
  }
5170
5369
  const subUrl = getSubscriptions().find((s) => s.name === subName)?.url;
5171
5370
  const buildResult = buildConfig(rawContent, mode, { subName, subUrl });
@@ -5434,6 +5633,9 @@ async function autoCleanSubscription(subName, options = {}) {
5434
5633
  return { summary, removedProxies, updatedGroups, removedGroups, skipped };
5435
5634
  }
5436
5635
 
5636
+ // src/commands/shared.ts
5637
+ import readline from "readline";
5638
+
5437
5639
  // src/runtime.ts
5438
5640
  function getRuntimeMode() {
5439
5641
  if (isDaemonEnabled()) return "mixed";
@@ -5574,6 +5776,18 @@ function printStatus() {
5574
5776
  subLine += ` (${formatProxySummary(info)})`;
5575
5777
  }
5576
5778
  console.log(subLine);
5779
+ const cached = getSubscriptionsWithCache().find((s) => s.name === activeSub.name);
5780
+ if (cached && (cached.download !== void 0 || cached.total !== void 0)) {
5781
+ const used = (cached.upload || 0) + (cached.download || 0);
5782
+ let trafficLine = `${colors.gray("\u6D41\u91CF: ")}${formatBytes(used)} / ${formatBytes(cached.total)}`;
5783
+ if (cached.total && cached.total > 0) {
5784
+ trafficLine += ` (${Math.min(used / cached.total * 100, 100).toFixed(1)}%)`;
5785
+ }
5786
+ console.log(trafficLine);
5787
+ }
5788
+ if (cached?.expire !== void 0) {
5789
+ console.log(`${colors.gray("\u5230\u671F: ")}${formatTimestamp(cached.expire)}`);
5790
+ }
5577
5791
  } else {
5578
5792
  console.log(`${colors.gray("\u8BA2\u9605: ")}\u672A\u914D\u7F6E`);
5579
5793
  }
@@ -5615,10 +5829,14 @@ async function cmdStop() {
5615
5829
 
5616
5830
  // src/commands/start.ts
5617
5831
  async function cmdStart(args) {
5832
+ const modeToken = args[1] && !args[1].startsWith("-") ? args[1].toLowerCase() : void 0;
5833
+ if (modeToken !== void 0 && modeToken !== "tun" && modeToken !== "mixed") {
5834
+ throw new CliError(`\u672A\u77E5\u7684\u542F\u52A8\u6A21\u5F0F: ${args[1]}`, { hint: "\u7528\u6CD5: mihomo start [tun|mixed]\uFF08\u9ED8\u8BA4 mixed\uFF09" });
5835
+ }
5836
+ const targetMode = modeToken === "tun" ? "tun" : "mixed";
5618
5837
  if (!hasKernel()) {
5619
5838
  throw new CliError('\u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5620
5839
  }
5621
- const targetMode = args[1] === "tun" ? "tun" : "mixed";
5622
5840
  const daemonEnabled = isDaemonEnabled();
5623
5841
  if (targetMode === "tun" && daemonEnabled) {
5624
5842
  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" });
@@ -5721,6 +5939,17 @@ async function dispatchSubcommand(args, table, options) {
5721
5939
  }
5722
5940
  return options.fallback(args);
5723
5941
  }
5942
+ async function confirmPrompt(question) {
5943
+ if (!process.stdin.isTTY) return false;
5944
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5945
+ const answer = await new Promise((resolve) => {
5946
+ rl.question(`${question} (y/N) `, (a) => {
5947
+ rl.close();
5948
+ resolve(a);
5949
+ });
5950
+ });
5951
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
5952
+ }
5724
5953
  function requireRunning() {
5725
5954
  const state = getRunningState();
5726
5955
  if (!state.running) {
@@ -5812,7 +6041,11 @@ async function cmdDaemon(args) {
5812
6041
  // 无 action → 显示状态;未知 action → 报错
5813
6042
  fallback: printStatusView,
5814
6043
  onUnknown: (action) => {
5815
- throw new CliError(`\u672A\u77E5\u7684 daemon \u5B50\u547D\u4EE4: ${action}`, { hint: ["", "\u53EF\u7528\u5B50\u547D\u4EE4: on, off, status"] });
6044
+ const names = SUBCOMMANDS.flatMap((c) => [c.name, ...c.aliases ?? []]);
6045
+ const suggestion = suggestSimilar(action, names);
6046
+ throw new CliError(`\u672A\u77E5\u7684 daemon \u5B50\u547D\u4EE4: ${action}`, {
6047
+ hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "", "\u53EF\u7528\u5B50\u547D\u4EE4: on, off, status"]
6048
+ });
5816
6049
  }
5817
6050
  });
5818
6051
  }
@@ -5876,13 +6109,30 @@ function printDirectoryInfo() {
5876
6109
  console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
5877
6110
  console.log("");
5878
6111
  }
5879
- var SUBCOMMANDS2 = [{ name: "open", handler: openDirectory }];
5880
- function cmdDirectory(args) {
5881
- void dispatchSubcommand(args, SUBCOMMANDS2, { fallback: printDirectoryInfo });
6112
+ var SUBCOMMANDS2 = [
6113
+ { name: "open", handler: openDirectory },
6114
+ { name: "list", handler: printDirectoryInfo }
6115
+ ];
6116
+ async function cmdDirectory(args) {
6117
+ await dispatchSubcommand(args, SUBCOMMANDS2, {
6118
+ fallback: printDirectoryInfo,
6119
+ onUnknown: (action) => {
6120
+ const names = SUBCOMMANDS2.flatMap((c) => [c.name, ...c.aliases ?? []]);
6121
+ const suggestion = suggestSimilar(action, names);
6122
+ throw new CliError(`\u672A\u77E5\u7684\u76EE\u5F55\u5B50\u547D\u4EE4: ${action}`, {
6123
+ hint: [
6124
+ ...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [],
6125
+ "",
6126
+ "\u53EF\u7528\u5B50\u547D\u4EE4: open, list",
6127
+ "\u6253\u5F00\u6307\u5B9A\u76EE\u5F55: mihomo dir open <target>"
6128
+ ]
6129
+ });
6130
+ }
6131
+ });
5882
6132
  }
5883
6133
 
5884
6134
  // src/kernel.ts
5885
- import { spawnSync as spawnSync5 } from "child_process";
6135
+ import { spawnSync as spawnSync4 } from "child_process";
5886
6136
  import fs7 from "fs";
5887
6137
  import path6 from "path";
5888
6138
 
@@ -6047,7 +6297,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
6047
6297
  if (progressCallback) {
6048
6298
  progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
6049
6299
  }
6050
- const curlResult = spawnSync5(
6300
+ const curlResult = spawnSync4(
6051
6301
  "curl",
6052
6302
  ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
6053
6303
  { stdio: "inherit" }
@@ -6075,7 +6325,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
6075
6325
  let extractedBinary = null;
6076
6326
  try {
6077
6327
  if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
6078
- const listResult = spawnSync5("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
6328
+ const listResult = spawnSync4("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
6079
6329
  if (listResult.error) throw listResult.error;
6080
6330
  if (listResult.status !== 0) throw new Error(`tar \u5217\u8868\u9000\u51FA\u7801 ${listResult.status}`);
6081
6331
  const entries = (listResult.stdout || "").split("\n").filter(Boolean);
@@ -6084,13 +6334,13 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
6084
6334
  throw new Error(`\u5F52\u6863\u542B\u975E\u6CD5\u8DEF\u5F84\u6761\u76EE: ${entry}`);
6085
6335
  }
6086
6336
  }
6087
- const tarResult = spawnSync5("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
6337
+ const tarResult = spawnSync4("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
6088
6338
  if (tarResult.error) throw tarResult.error;
6089
6339
  if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
6090
6340
  } else if (tempPath.endsWith(".gz")) {
6091
6341
  const baseName = path6.basename(tempPath, ".gz");
6092
6342
  const outputPath = path6.join(extractPath, baseName);
6093
- const gzipResult = spawnSync5("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
6343
+ const gzipResult = spawnSync4("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
6094
6344
  if (gzipResult.error) throw gzipResult.error;
6095
6345
  if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
6096
6346
  fs7.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
@@ -6126,7 +6376,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
6126
6376
  if (progressCallback) {
6127
6377
  progressCallback("\u6821\u9A8C\u5185\u6838...");
6128
6378
  }
6129
- const check = spawnSync5(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
6379
+ const check = spawnSync4(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
6130
6380
  const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
6131
6381
  if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
6132
6382
  try {
@@ -6171,6 +6421,9 @@ async function cmdKernel(args) {
6171
6421
  const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
6172
6422
  console.log(`
6173
6423
  \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
6424
+ if (getRunningState().running) {
6425
+ 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"));
6426
+ }
6174
6427
  }
6175
6428
  } catch (e) {
6176
6429
  if (e instanceof CliError) throw e;
@@ -6321,20 +6574,35 @@ async function setOverwrite(enabled, args) {
6321
6574
  }
6322
6575
  var SUBCOMMANDS3 = [
6323
6576
  { name: "on", aliases: ["enable"], handler: (args) => setOverwrite(true, args) },
6324
- { name: "off", aliases: ["disable"], handler: (args) => setOverwrite(false, args) }
6577
+ { name: "off", aliases: ["disable"], handler: (args) => setOverwrite(false, args) },
6578
+ // list 显式注册:onUnknown 生效后,未注册的子命令会报错,不能再靠 fallback 兜住 `ow list`
6579
+ {
6580
+ name: "list",
6581
+ handler: () => {
6582
+ console.log("");
6583
+ printOverwriteList();
6584
+ }
6585
+ }
6325
6586
  ];
6326
6587
  async function cmdOverwrite(args) {
6327
6588
  await dispatchSubcommand(args, SUBCOMMANDS3, {
6589
+ // 无子命令 → 列表;未知子命令 → 报错(与 sub/daemon 同构,避免 `ow onn` 静默当成 list)
6328
6590
  fallback: () => {
6329
6591
  console.log("");
6330
6592
  printOverwriteList();
6593
+ },
6594
+ onUnknown: (action) => {
6595
+ const names = SUBCOMMANDS3.flatMap((c) => [c.name, ...c.aliases ?? []]);
6596
+ const suggestion = suggestSimilar(action, names);
6597
+ throw new CliError(`\u672A\u77E5\u7684\u8986\u5199\u5B50\u547D\u4EE4: ${action}`, {
6598
+ hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "", "\u53EF\u7528\u5B50\u547D\u4EE4: on, off, list"]
6599
+ });
6331
6600
  }
6332
6601
  });
6333
6602
  }
6334
6603
 
6335
6604
  // src/commands/reset.ts
6336
6605
  import fs8 from "fs";
6337
- import readline from "readline";
6338
6606
  var RESET_TARGETS = [
6339
6607
  {
6340
6608
  id: "subs",
@@ -6371,7 +6639,10 @@ var RESET_TARGETS = [
6371
6639
  id: "settings",
6372
6640
  aliases: ["setting", "settings", "config"],
6373
6641
  label: "\u8BBE\u7F6E",
6374
- paths: () => [PATHS.settingsFile],
6642
+ // 同时删 .bak:readSettings 遇格式损坏会备份原文件(settings.ts),里面含
6643
+ // controller_secret 与订阅 URL 的 token。只删主文件会让 "已重置: 设置" 名不副实,
6644
+ // 凭据仍明文留在数据目录(cache.json.bak 在 subscriptions/ 内,随整目录删除,无需单列)
6645
+ paths: () => [PATHS.settingsFile, `${PATHS.settingsFile}.bak`],
6375
6646
  needsStop: false
6376
6647
  },
6377
6648
  {
@@ -6421,18 +6692,9 @@ function resolveResetTargets(names) {
6421
6692
  unmatched.push(name);
6422
6693
  }
6423
6694
  }
6695
+ matched.sort((a, b) => RESET_TARGETS.indexOf(a) - RESET_TARGETS.indexOf(b));
6424
6696
  return { matched, unmatched };
6425
6697
  }
6426
- async function confirmPrompt(question) {
6427
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6428
- const answer = await new Promise((resolve) => {
6429
- rl.question(`${question} (y/N) `, (a) => {
6430
- rl.close();
6431
- resolve(a);
6432
- });
6433
- });
6434
- return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
6435
- }
6436
6698
  async function cmdReset(args) {
6437
6699
  const flags = (args || []).filter((a) => a.startsWith("-"));
6438
6700
  const names = (args || []).slice(1).filter((a) => !a.startsWith("-"));
@@ -6487,25 +6749,41 @@ async function cmdReset(args) {
6487
6749
  console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u91CD\u7F6E\u5C06\u4E00\u5E76\u5173\u95ED\u4FDD\u6D3B\uFF08\u79FB\u9664\u5F00\u673A\u81EA\u542F\uFF09"));
6488
6750
  }
6489
6751
  console.log(`\u5C06\u5220\u9664: ${targets.map((t) => t.label).join("\u3001")}`);
6490
- if (!skipConfirm && !await confirmPrompt("\u786E\u8BA4?")) {
6491
- console.log("\u5DF2\u53D6\u6D88");
6492
- return;
6752
+ if (!skipConfirm) {
6753
+ if (!process.stdin.isTTY) {
6754
+ throw new CliError("\u975E\u4EA4\u4E92\u73AF\u5883\u65E0\u6CD5\u786E\u8BA4", { label: "\u5DF2\u53D6\u6D88", hint: ["\u8DF3\u8FC7\u786E\u8BA4\u8BF7\u52A0 -y: mihomo reset ... -y"] });
6755
+ }
6756
+ if (!await confirmPrompt("\u786E\u8BA4?")) {
6757
+ console.log("\u5DF2\u53D6\u6D88");
6758
+ return;
6759
+ }
6493
6760
  }
6494
6761
  if (disablesDaemon && isDaemonEnabled()) {
6495
6762
  try {
6496
6763
  disableDaemon();
6497
6764
  } catch (e) {
6498
- console.error(`${colors.red("\u4FDD\u6D3B\u5173\u95ED\u5DF2\u53D6\u6D88\uFF0C\u91CD\u7F6E\u4E2D\u6B62:")} ${e.message.split("\n")[0]}`);
6499
- return;
6765
+ if (e instanceof CliError) throw e;
6766
+ throw new CliError(e.message.split("\n")[0], { label: "\u4FDD\u6D3B\u5173\u95ED\u5DF2\u53D6\u6D88\uFF0C\u91CD\u7F6E\u4E2D\u6B62" });
6500
6767
  }
6501
6768
  }
6502
6769
  if (needsStop && getMihomoPids().length > 0) {
6503
6770
  console.log("\u505C\u6B62\u8FDB\u7A0B...");
6504
- cleanupAll();
6771
+ const cleanup = cleanupAll();
6505
6772
  for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6506
6773
  if (getMihomoPids().length === 0) break;
6507
6774
  await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6508
6775
  }
6776
+ const remaining = getMihomoPids();
6777
+ if (remaining.length > 0) {
6778
+ throw new CliError(remaining.join(", "), {
6779
+ label: "\u8FDB\u7A0B\u672A\u80FD\u505C\u6B62\uFF0C\u91CD\u7F6E\u4E2D\u6B62",
6780
+ hint: [
6781
+ `\u672A\u7EC8\u6B62\u7684\u8FDB\u7A0B: ${remaining.join(", ")}${cleanup.failed > 0 ? `\uFF08${cleanup.failed} \u4E2A\u7EC8\u6B62\u5931\u8D25\uFF09` : ""}`,
6782
+ "\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo",
6783
+ "\u5426\u5219\u6B8B\u7559\u8FDB\u7A0B\u4F1A\u7EE7\u7EED\u4F7F\u7528\u5373\u5C06\u5220\u9664\u7684\u914D\u7F6E\u3002"
6784
+ ]
6785
+ });
6786
+ }
6509
6787
  }
6510
6788
  for (const t of targets) {
6511
6789
  for (const p of t.paths()) {
@@ -6530,6 +6808,16 @@ async function cmdReset(args) {
6530
6808
  import { spawn as spawn2 } from "child_process";
6531
6809
  import fs9 from "fs";
6532
6810
  import path8 from "path";
6811
+ function isProxyValid(proxy) {
6812
+ if (proxy === null || typeof proxy !== "object") return false;
6813
+ if (!proxy.name || !proxy.server || !proxy.port) return false;
6814
+ if (!proxy.type) return false;
6815
+ if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
6816
+ const pw = String(proxy.password || "");
6817
+ if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
6818
+ }
6819
+ return true;
6820
+ }
6533
6821
  var TEST_DIR = path8.join(USER_DATA_DIR, "test");
6534
6822
  var TEST_DIRS = {
6535
6823
  data: path8.join(TEST_DIR, "data"),
@@ -6553,12 +6841,12 @@ function buildTestConfig(subName) {
6553
6841
  ensureTestDirs();
6554
6842
  const rawContent = readSubscriptionRawConfig(subName);
6555
6843
  if (!rawContent) {
6556
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
6844
+ throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u66F4\u65B0\u8BA2\u9605\uFF08mihomo sub update ${subName}\uFF09`);
6557
6845
  }
6558
6846
  const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
6559
6847
  const proxies = (parsed.proxies || []).filter(isProxyValid);
6560
6848
  if (proxies.length === 0) {
6561
- throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
6849
+ throw new CliError(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
6562
6850
  }
6563
6851
  const nameCount = /* @__PURE__ */ new Map();
6564
6852
  for (const proxy of proxies) {
@@ -6585,7 +6873,7 @@ function buildTestConfig(subName) {
6585
6873
  }
6586
6874
  async function startTestInstance() {
6587
6875
  const binary = PATHS.mihomoBinary;
6588
- if (!fs9.existsSync(binary)) throw new Error('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
6876
+ if (!fs9.existsSync(binary)) throw new CliError('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
6589
6877
  stopTestInstance();
6590
6878
  const logFd = fs9.openSync(TEST_PATHS.logFile, "a");
6591
6879
  const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
@@ -6597,7 +6885,8 @@ async function startTestInstance() {
6597
6885
  fs9.closeSync(logFd);
6598
6886
  child.unref();
6599
6887
  const pid = child.pid;
6600
- 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");
6888
+ 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");
6889
+ spawnedTestPid = pid;
6601
6890
  fs9.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
6602
6891
  const client = createHttpClient({ timeout: 2e3 });
6603
6892
  let ready = false;
@@ -6617,20 +6906,23 @@ async function startTestInstance() {
6617
6906
  errorDetail = fs9.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
6618
6907
  } catch {
6619
6908
  }
6620
- throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
6909
+ throw new CliError(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
6621
6910
  ${errorDetail}` : ""}`);
6622
6911
  }
6623
6912
  if (!ready) {
6624
- throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
6913
+ throw new CliError("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
6625
6914
  }
6626
6915
  }
6916
+ var spawnedTestPid = null;
6627
6917
  function stopTestInstance() {
6628
- let pid;
6918
+ let pid = null;
6629
6919
  try {
6630
- pid = parseInt(fs9.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
6920
+ const fromFile = parseInt(fs9.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
6921
+ if (fromFile > 0) pid = fromFile;
6631
6922
  } catch {
6632
- return;
6633
6923
  }
6924
+ if (pid === null) pid = spawnedTestPid;
6925
+ if (pid === null) return;
6634
6926
  if (pid > 0 && isProcessRunning(pid) && isProcessCommandMatching(pid, TEST_PATHS.configFile)) {
6635
6927
  process.kill(pid, "SIGKILL");
6636
6928
  for (let i = 0; i < 20; i++) {
@@ -6638,6 +6930,7 @@ function stopTestInstance() {
6638
6930
  sleepSync(100);
6639
6931
  }
6640
6932
  }
6933
+ spawnedTestPid = null;
6641
6934
  try {
6642
6935
  fs9.unlinkSync(TEST_PATHS.pidFile);
6643
6936
  } catch {
@@ -6759,13 +7052,14 @@ async function subAdd(args) {
6759
7052
  }
6760
7053
  const normalizedUrl = urls.join(",");
6761
7054
  console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
7055
+ addSubscription(normalizedUrl, name);
6762
7056
  try {
6763
- addSubscription(normalizedUrl, name);
6764
- setDefaultSubscription(name);
6765
7057
  const info = await downloadMergedSubscription(urls, name);
7058
+ setDefaultSubscription(name);
6766
7059
  console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
6767
7060
  } catch (e) {
6768
7061
  removeSubscription(name);
7062
+ if (e instanceof CliError) throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25", hint: e.hint });
6769
7063
  throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25" });
6770
7064
  }
6771
7065
  } else {
@@ -6773,15 +7067,16 @@ async function subAdd(args) {
6773
7067
  throw new CliError("\u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL\uFF08\u9700\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF09");
6774
7068
  }
6775
7069
  console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
7070
+ addSubscription(url, name);
6776
7071
  try {
6777
- addSubscription(url, name);
6778
- setDefaultSubscription(name);
6779
7072
  const info = await downloadSubscription(url, name);
7073
+ setDefaultSubscription(name);
6780
7074
  const repoUrl = githubRepoUrl(url);
6781
7075
  if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
6782
7076
  console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
6783
7077
  } catch (e) {
6784
7078
  removeSubscription(name);
7079
+ if (e instanceof CliError) throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25", hint: e.hint });
6785
7080
  throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25" });
6786
7081
  }
6787
7082
  }
@@ -6878,8 +7173,8 @@ async function subWeb(args) {
6878
7173
  console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
6879
7174
  }
6880
7175
  }
6881
- function subRemove(args) {
6882
- const name = args[2];
7176
+ async function subRemove(args) {
7177
+ const name = getNonFlagArg(args, 2);
6883
7178
  const subs = getSubscriptions();
6884
7179
  if (!name) {
6885
7180
  throw new CliError("\u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0", {
@@ -6887,6 +7182,21 @@ function subRemove(args) {
6887
7182
  });
6888
7183
  }
6889
7184
  const target = resolveSubscription(subs, name);
7185
+ const isExact = target.name === name;
7186
+ const skipConfirm = hasFlag(args, "-y", "--yes");
7187
+ if (!isExact && !skipConfirm) {
7188
+ if (!process.stdin.isTTY) {
7189
+ throw new CliError(`\u6A21\u7CCA\u5339\u914D\u5230 "${target.name}"\uFF0C\u975E\u4EA4\u4E92\u73AF\u5883\u9700\u786E\u8BA4`, {
7190
+ label: "\u5DF2\u53D6\u6D88",
7191
+ hint: [`\u8BF7\u7528\u5B8C\u6574\u540D\u79F0: mihomo sub remove ${target.name}`, `\u6216\u8DF3\u8FC7\u786E\u8BA4: mihomo sub remove ${name} -y`]
7192
+ });
7193
+ }
7194
+ console.log(`\u5C06\u5220\u9664\u8BA2\u9605 "${target.name}" (\u6A21\u7CCA\u5339\u914D "${name}")`);
7195
+ if (!await confirmPrompt("\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\uFF0C\u786E\u8BA4?")) {
7196
+ console.log("\u5DF2\u53D6\u6D88");
7197
+ return;
7198
+ }
7199
+ }
6890
7200
  const switchedTo = removeSubscription(target.name);
6891
7201
  console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
6892
7202
  if (switchedTo) {
@@ -6956,18 +7266,22 @@ async function cmdSubscription(args) {
6956
7266
  await dispatchSubcommand(args, SUBCOMMANDS4, {
6957
7267
  // 无子命令 → 列表;未知子命令 → 报错
6958
7268
  fallback: printSubscriptionList,
6959
- onUnknown: () => {
6960
- throw new CliError("\u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4", { hint: "\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]" });
7269
+ onUnknown: (action) => {
7270
+ const names = SUBCOMMANDS4.flatMap((c) => [c.name, ...c.aliases ?? []]);
7271
+ const suggestion = suggestSimilar(action, names);
7272
+ throw new CliError(`\u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4: ${action}`, {
7273
+ hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]"]
7274
+ });
6961
7275
  }
6962
7276
  });
6963
7277
  }
6964
7278
 
6965
7279
  // src/commands/test.ts
6966
7280
  async function cmdTest(args) {
6967
- requireRunning();
6968
- const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
6969
7281
  const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6970
7282
  const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
7283
+ requireRunning();
7284
+ const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
6971
7285
  console.log(`\u6D4B\u8BD5 "${activeSub.name}" \u8282\u70B9\u8FDE\u901A\u6027...`);
6972
7286
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6973
7287
  console.log("");
@@ -6981,11 +7295,11 @@ async function cmdTest(args) {
6981
7295
  console.log(formatTestSummary(summary));
6982
7296
  }
6983
7297
  async function cmdClean(args) {
6984
- requireRunning();
6985
- const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
6986
7298
  const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6987
7299
  const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
6988
7300
  const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
7301
+ requireRunning();
7302
+ const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
6989
7303
  console.log(`\u6E05\u7406 "${activeSub.name}" \u5931\u8D25\u8282\u70B9...`);
6990
7304
  console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6991
7305
  console.log("");
@@ -7034,6 +7348,10 @@ function cmdUI(args) {
7034
7348
  if (!Object.hasOwn(UI_URLS, uiName)) {
7035
7349
  throw new CliError(`\u672A\u77E5\u7684 UI "${uiName}"`, { hint: "\u53EF\u7528 UI: zash (\u9ED8\u8BA4), dash, yacd" });
7036
7350
  }
7351
+ if (!getRunningState().running) {
7352
+ 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"));
7353
+ console.log("");
7354
+ }
7037
7355
  const url = UI_URLS[uiName];
7038
7356
  console.log(`\u6253\u5F00 Web UI: ${uiName}`);
7039
7357
  console.log(`\u5730\u5740: ${url}`);
@@ -7048,16 +7366,38 @@ function cmdUI(args) {
7048
7366
  }
7049
7367
 
7050
7368
  // src/commands/update.ts
7051
- import { exec, spawn as spawn3 } from "child_process";
7369
+ import { execFile, spawn as spawn3 } from "child_process";
7052
7370
  import { promisify } from "util";
7053
- var execAsync = promisify(exec);
7371
+ var execFileAsync = promisify(execFile);
7372
+ var NPM_VIEW_TIMEOUT_MS = 15e3;
7373
+ async function getLatestNpmVersion() {
7374
+ try {
7375
+ const { stdout } = await execFileAsync("npm", ["view", PKG_NAME, "version"], { timeout: NPM_VIEW_TIMEOUT_MS });
7376
+ const version = stdout.trim().split("\n").pop()?.trim();
7377
+ return version || null;
7378
+ } catch {
7379
+ return null;
7380
+ }
7381
+ }
7054
7382
  async function cmdUpdate() {
7055
7383
  console.log(`\u5F53\u524D\u7248\u672C: ${colors.cyan(VERSION)}`);
7056
7384
  console.log("");
7385
+ console.log("\u6B63\u5728\u68C0\u67E5\u6700\u65B0\u7248\u672C...");
7386
+ const latest = await getLatestNpmVersion();
7387
+ if (latest && latest === VERSION) {
7388
+ console.log(`\u5DF2\u662F\u6700\u65B0\u7248\u672C (${colors.green(VERSION)})\uFF0C\u65E0\u9700\u66F4\u65B0`);
7389
+ return;
7390
+ }
7391
+ if (latest) {
7392
+ console.log(`\u6700\u65B0\u7248\u672C: ${colors.cyan(latest)}`);
7393
+ } else {
7394
+ 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"));
7395
+ }
7396
+ console.log("");
7057
7397
  console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
7058
7398
  console.log("");
7059
7399
  await new Promise((resolve, reject) => {
7060
- const npm = spawn3("npm", ["install", "-g", "mihomo-cli"], { stdio: "inherit" });
7400
+ const npm = spawn3("npm", ["install", "-g", PKG_NAME], { stdio: "inherit" });
7061
7401
  npm.on("close", (code) => {
7062
7402
  if (code === 0) {
7063
7403
  resolve();
@@ -7071,9 +7411,9 @@ async function cmdUpdate() {
7071
7411
  });
7072
7412
  });
7073
7413
  try {
7074
- const { stdout } = await execAsync("npm list -g mihomo-cli --json --depth=0");
7414
+ const { stdout } = await execFileAsync("npm", ["list", "-g", PKG_NAME, "--json", "--depth=0"]);
7075
7415
  const result = JSON.parse(stdout);
7076
- const newVersion = result.dependencies?.["mihomo-cli"]?.version;
7416
+ const newVersion = result.dependencies?.[PKG_NAME]?.version;
7077
7417
  console.log("");
7078
7418
  if (newVersion) {
7079
7419
  console.log(`\u66F4\u65B0\u5B8C\u6210\uFF0C\u6700\u65B0\u7248\u672C: ${colors.green(newVersion)}`);
@@ -7143,18 +7483,18 @@ var COMMANDS = [
7143
7483
  // === 订阅 ===
7144
7484
  {
7145
7485
  name: "subscription",
7146
- aliases: ["sub", "subscriptions"],
7486
+ aliases: ["sub", "subs", "subscriptions"],
7147
7487
  handler: cmdSubscription,
7148
7488
  group: "subscription",
7149
7489
  usage: [
7150
- "subscription \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09",
7490
+ "subscription \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub/subs\uFF09",
7151
7491
  "subscription use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605",
7152
7492
  "subscription add <url> [name] \u6DFB\u52A0\u8BA2\u9605",
7153
7493
  "subscription update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09",
7154
- "subscription remove <name> \u5220\u9664\u8BA2\u9605",
7494
+ "subscription remove <name> \u5220\u9664\u8BA2\u9605\uFF08\u6A21\u7CCA\u5339\u914D\u9700\u786E\u8BA4\uFF0C-y \u8DF3\u8FC7\uFF09",
7155
7495
  "subscription web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762",
7156
7496
  "subscription test [name] \u6D4B\u8BD5\u8282\u70B9\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u65E0\u9700\u8FD0\u884C\uFF09",
7157
- "subscription clean [name] \u6D4B\u901F\u6E05\u7406\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u4E0D\u52A8\u4E3B\u5B9E\u4F8B\uFF09"
7497
+ "subscription clean [name] \u6D4B\u901F\u6E05\u7406\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u4E0D\u52A8\u4E3B\u5B9E\u4F8B\uFF09[-r N] [-t ms] [-j N]"
7158
7498
  ]
7159
7499
  },
7160
7500
  {
@@ -7191,7 +7531,7 @@ var COMMANDS = [
7191
7531
  name: "on",
7192
7532
  aliases: [],
7193
7533
  handler: cmdOverwrite,
7194
- rewrite: () => ["ow", "on"],
7534
+ rewrite: (args) => ["ow", "on", ...args.slice(1)],
7195
7535
  group: "config",
7196
7536
  usage: []
7197
7537
  },
@@ -7199,7 +7539,7 @@ var COMMANDS = [
7199
7539
  name: "off",
7200
7540
  aliases: [],
7201
7541
  handler: cmdOverwrite,
7202
- rewrite: () => ["ow", "off"],
7542
+ rewrite: (args) => ["ow", "off", ...args.slice(1)],
7203
7543
  group: "config",
7204
7544
  usage: []
7205
7545
  },
@@ -7278,6 +7618,9 @@ var COMMAND_INDEX = (() => {
7278
7618
  function findCommand(token) {
7279
7619
  return COMMAND_INDEX.get(token);
7280
7620
  }
7621
+ function allCommandTokens() {
7622
+ return [...COMMAND_INDEX.keys()];
7623
+ }
7281
7624
 
7282
7625
  // src/index.ts
7283
7626
  process.on("SIGINT", () => {
@@ -7315,11 +7658,26 @@ function clearProxyEnv() {
7315
7658
  delete process.env.all_proxy;
7316
7659
  delete process.env.ALL_PROXY;
7317
7660
  }
7661
+ var PLATFORM_FREE_COMMANDS = /* @__PURE__ */ new Set(["help", "version"]);
7662
+ function assertSupportedPlatform(commandName) {
7663
+ if (process.platform === "darwin") return;
7664
+ if (PLATFORM_FREE_COMMANDS.has(commandName)) return;
7665
+ if (process.env.MIHOMO_CLI_ALLOW_ANY_PLATFORM === "1") return;
7666
+ throw new CliError(`mihomo-cli \u76EE\u524D\u4EC5\u652F\u6301 macOS\uFF08\u5F53\u524D\u5E73\u53F0: ${process.platform}\uFF09`, {
7667
+ label: "\u5E73\u53F0\u4E0D\u652F\u6301",
7668
+ hint: [
7669
+ "\u8FDB\u7A0B\u4FDD\u6D3B\u4F9D\u8D56 launchd\u3001\u76EE\u5F55/UI \u6253\u5F00\u4F9D\u8D56 open\u3001\u63D0\u6743\u4F9D\u8D56 sudo\uFF0C\u5747\u65E0\u5176\u4ED6\u5E73\u53F0\u5B9E\u73B0\u3002",
7670
+ "Windows / Linux \u9002\u914D\u4ECD\u5728\u8FDB\u884C\u4E2D\u3002",
7671
+ "\u5982\u9700\u5728\u975E macOS \u4E0A\u5F00\u53D1\u8C03\u8BD5\uFF0C\u53EF\u8BBE MIHOMO_CLI_ALLOW_ANY_PLATFORM=1\uFF08\u529F\u80FD\u4E0D\u4FDD\u8BC1\u53EF\u7528\uFF09\u3002"
7672
+ ]
7673
+ });
7674
+ }
7318
7675
  async function main() {
7319
7676
  clearProxyEnv();
7320
- ensureDirs();
7321
7677
  const args = process.argv.slice(2);
7322
7678
  if (args.length === 0) {
7679
+ assertSupportedPlatform("status");
7680
+ ensureDirs();
7323
7681
  printStatus();
7324
7682
  printShortHelp();
7325
7683
  return;
@@ -7327,8 +7685,13 @@ async function main() {
7327
7685
  const token = args[0].toLowerCase();
7328
7686
  const command = findCommand(token);
7329
7687
  if (!command) {
7330
- throw new CliError(`\u672A\u77E5\u547D\u4EE4: ${token}`, { hint: '\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9' });
7688
+ const suggestion = suggestSimilar(token, allCommandTokens());
7689
+ throw new CliError(`\u672A\u77E5\u547D\u4EE4: ${token}`, {
7690
+ hint: [suggestion.length > 0 ? `\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?` : '\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9']
7691
+ });
7331
7692
  }
7693
+ assertSupportedPlatform(command.name);
7694
+ ensureDirs();
7332
7695
  await command.handler(command.rewrite ? command.rewrite(args) : args);
7333
7696
  }
7334
7697
  main().catch((e) => {