mihomo-cli 3.0.0 → 3.3.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +65 -25
  3. package/dist/index.js +1911 -1403
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/config.ts
4
- import { spawnSync } from "child_process";
4
+ import { spawnSync as spawnSync2 } from "child_process";
5
5
  import fs4 from "fs";
6
6
 
7
7
  // node_modules/js-yaml/dist/js-yaml.mjs
@@ -2917,13 +2917,18 @@ function dump(input, options = {}) {
2917
2917
  }
2918
2918
 
2919
2919
  // src/constants.ts
2920
- var AVAILABLE_MIRRORS = ["gh-proxy.org", "v6.gh-proxy.org", "hk.gh-proxy.org", "cdn.gh-proxy.org"];
2920
+ var AVAILABLE_MIRRORS = ["v6.gh-proxy.org", "gh-proxy.org", "hk.gh-proxy.org", "cdn.gh-proxy.org"];
2921
+ var DEFAULT_MIRROR = "https://v6.gh-proxy.org/";
2921
2922
  var UI_URLS = {
2922
2923
  zash: "https://board.zash.run.place",
2923
2924
  dash: "https://metacubex.github.io/metacubexd",
2924
2925
  yacd: "https://yacd.metacubex.one"
2925
2926
  };
2926
- var LAUNCH_AGENT_LABEL = "com.mihomo-cli.daemon";
2927
+ var LAUNCH_DAEMON_LABEL = process.env.MIHOMO_CLI_DAEMON_LABEL || "com.mihomo-cli.daemon";
2928
+ var CONTROLLER_PORT = 9090;
2929
+ var CONTROLLER_ADDR = `127.0.0.1:${CONTROLLER_PORT}`;
2930
+ var CONTROLLER_BASE_URL = `http://${CONTROLLER_ADDR}`;
2931
+ var TEST_CONTROLLER_ADDR = "127.0.0.1:29090";
2927
2932
  var TUN_CONFIG = {
2928
2933
  tun: {
2929
2934
  enable: true,
@@ -2937,14 +2942,14 @@ var TUN_CONFIG = {
2937
2942
  var TEST_CONFIG = {
2938
2943
  "mixed-port": 27890,
2939
2944
  "allow-lan": false,
2940
- "external-controller": "127.0.0.1:29090",
2945
+ "external-controller": TEST_CONTROLLER_ADDR,
2941
2946
  "log-level": "error",
2942
2947
  "geodata-mode": true
2943
2948
  };
2944
2949
  var BASE_CONFIG = {
2945
2950
  "mixed-port": 7890,
2946
2951
  "allow-lan": false,
2947
- "external-controller": "127.0.0.1:9090",
2952
+ "external-controller": CONTROLLER_ADDR,
2948
2953
  "unified-delay": true,
2949
2954
  "tcp-concurrent": true,
2950
2955
  "geo-auto-update": true,
@@ -2970,10 +2975,11 @@ var DEFAULT_TEST_URL = "http://www.gstatic.com/generate_204";
2970
2975
  var DEFAULT_CLEAN_ROUNDS = 2;
2971
2976
  var AUTO_CLEAN_THRESHOLD = 100;
2972
2977
  var AUTO_CLEAN_THRESHOLD_GITHUB = 50;
2978
+ var AUTO_CLEAN_COOLDOWN_HOURS = 12;
2973
2979
 
2974
2980
  // src/overwrite.ts
2975
2981
  import fs3 from "fs";
2976
- import path2 from "path";
2982
+ import path3 from "path";
2977
2983
 
2978
2984
  // src/paths.ts
2979
2985
  import fs from "fs";
@@ -3003,8 +3009,8 @@ var PATHS = {
3003
3009
  configStage1Subscription: path.join(DIRS.runtime, "1.subscription.yaml"),
3004
3010
  configStage2Overwrite: path.join(DIRS.runtime, "2.overwrite.yaml"),
3005
3011
  configStage3System: path.join(DIRS.runtime, "3.system.yaml"),
3006
- // launchd LaunchAgent plist 必须位于真实用户主目录,不受 MIHOMO_CLI_DIR 影响
3007
- launchAgentPlist: path.join(os.homedir(), "Library", "LaunchAgents", `${LAUNCH_AGENT_LABEL}.plist`)
3012
+ // launchd LaunchDaemon plist 位于系统级 /Library/LaunchDaemons/,root:wheel 拥有,与 homedir / MIHOMO_CLI_DIR 无关
3013
+ launchDaemonPlist: path.join("/Library/LaunchDaemons", `${LAUNCH_DAEMON_LABEL}.plist`)
3008
3014
  };
3009
3015
  var DIRECTORY_TARGETS = {
3010
3016
  root: { path: null, label: "\u6839\u76EE\u5F55" },
@@ -3040,6 +3046,7 @@ function rmrf(dir) {
3040
3046
 
3041
3047
  // src/settings.ts
3042
3048
  import fs2 from "fs";
3049
+ import path2 from "path";
3043
3050
  var settingsCache = null;
3044
3051
  function readSettings() {
3045
3052
  if (settingsCache !== null) return settingsCache;
@@ -3092,6 +3099,7 @@ function maskUrl(url) {
3092
3099
  }
3093
3100
  if (parsed.username) parsed.username = "***";
3094
3101
  if (parsed.password) parsed.password = "***";
3102
+ parsed.pathname = parsed.pathname.split("/").map((seg) => seg.length >= 16 ? `${seg.slice(0, 4)}***${seg.slice(-4)}` : seg).join("/");
3095
3103
  return parsed.toString();
3096
3104
  } catch {
3097
3105
  if (url.length > 30) {
@@ -3107,6 +3115,12 @@ function readSubscriptionCache() {
3107
3115
  const content = fs2.readFileSync(PATHS.subscriptionsCacheFile, "utf8");
3108
3116
  return JSON.parse(content);
3109
3117
  } catch {
3118
+ try {
3119
+ fs2.copyFileSync(PATHS.subscriptionsCacheFile, `${PATHS.subscriptionsCacheFile}.bak`);
3120
+ console.warn(`\u8B66\u544A: \u8BA2\u9605\u7F13\u5B58\u683C\u5F0F\u635F\u574F\uFF0C\u5DF2\u5907\u4EFD\u5230 ${PATHS.subscriptionsCacheFile}.bak`);
3121
+ } catch {
3122
+ console.warn("\u8B66\u544A: \u8BA2\u9605\u7F13\u5B58\u683C\u5F0F\u635F\u574F\uFF0C\u5DF2\u5FFD\u7565");
3123
+ }
3110
3124
  return {};
3111
3125
  }
3112
3126
  }
@@ -3143,12 +3157,10 @@ function addSubscription(url, name = "default") {
3143
3157
  validateSubscriptionName(name);
3144
3158
  const settings = readSettings();
3145
3159
  const subs = [...settings.subscriptions || []];
3146
- const existingIndex = subs.findIndex((s) => s.name === name);
3147
- if (existingIndex >= 0) {
3148
- subs[existingIndex] = { name, url };
3149
- } else {
3150
- subs.push({ name, url });
3160
+ if (subs.some((s) => s.name === name)) {
3161
+ throw new Error(`\u8BA2\u9605 "${name}" \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E2A\u540D\u79F0\uFF08mihomo sub add <url> <\u540D\u79F0>\uFF09\uFF0C\u6216\u5148\u5220\u9664\uFF08mihomo sub remove ${name}\uFF09`);
3151
3162
  }
3163
+ subs.push({ name, url });
3152
3164
  const updates = { subscriptions: subs };
3153
3165
  if (!settings.active_subscription && subs.length === 1) {
3154
3166
  updates.active_subscription = name;
@@ -3173,7 +3185,10 @@ function removeSubscription(name) {
3173
3185
  delete cache[name];
3174
3186
  writeSubscriptionCache(cache);
3175
3187
  }
3176
- fs2.rmSync(getSubscriptionRawConfigPath(name), { force: true });
3188
+ try {
3189
+ fs2.rmSync(getSubscriptionRawConfigPath(name), { force: true });
3190
+ } catch {
3191
+ }
3177
3192
  return switchedTo;
3178
3193
  }
3179
3194
  function setDefaultSubscription(name) {
@@ -3186,7 +3201,10 @@ function setDefaultSubscription(name) {
3186
3201
  return true;
3187
3202
  }
3188
3203
  function getSubscriptionRawConfigPath(subName) {
3189
- return `${DIRS.subscriptions}/${subName}.yaml`;
3204
+ if (!SAFE_NAME_RE.test(subName)) {
3205
+ throw new Error(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${subName}"`);
3206
+ }
3207
+ return path2.join(DIRS.subscriptions, `${subName}.yaml`);
3190
3208
  }
3191
3209
  function saveSubscriptionRawConfig(subName, content) {
3192
3210
  ensureDirs();
@@ -3205,6 +3223,7 @@ function parseOverrideKey(key) {
3205
3223
  let forceOverwrite = false;
3206
3224
  let arrayPrepend = false;
3207
3225
  let arrayAppend = false;
3226
+ let arrayMergeByName = false;
3208
3227
  const lastChar = key[key.length - 1];
3209
3228
  const openAngleCount = (key.match(/</g) || []).length;
3210
3229
  const closeAngleCount = (key.match(/>/g) || []).length;
@@ -3225,6 +3244,9 @@ function parseOverrideKey(key) {
3225
3244
  } else {
3226
3245
  actualKey = unwrapped;
3227
3246
  }
3247
+ } else if (actualKey.startsWith("~")) {
3248
+ arrayMergeByName = true;
3249
+ actualKey = actualKey.slice(1);
3228
3250
  } else {
3229
3251
  if (actualKey.startsWith("+")) {
3230
3252
  arrayPrepend = true;
@@ -3235,7 +3257,7 @@ function parseOverrideKey(key) {
3235
3257
  actualKey = actualKey.slice(0, -1);
3236
3258
  }
3237
3259
  }
3238
- return { key: actualKey, forceOverwrite, arrayPrepend, arrayAppend };
3260
+ return { key: actualKey, forceOverwrite, arrayPrepend, arrayAppend, arrayMergeByName };
3239
3261
  }
3240
3262
  function deepMergeWithOverrides(target, override) {
3241
3263
  let t = target;
@@ -3253,8 +3275,24 @@ function deepMergeWithOverrides(target, override) {
3253
3275
  }
3254
3276
  const result = { ...t };
3255
3277
  for (const [rawKey, value] of Object.entries(override)) {
3256
- const { key, forceOverwrite, arrayPrepend, arrayAppend } = parseOverrideKey(rawKey);
3278
+ const { key, forceOverwrite, arrayPrepend, arrayAppend, arrayMergeByName } = parseOverrideKey(rawKey);
3257
3279
  const existingValue = result[key];
3280
+ if (arrayMergeByName) {
3281
+ const existingArr = Array.isArray(existingValue) ? existingValue : [];
3282
+ const overrideArr = Array.isArray(value) ? value : [value];
3283
+ const merged = [...existingArr];
3284
+ for (const item of overrideArr) {
3285
+ const name = item && typeof item === "object" && !Array.isArray(item) ? item.name : void 0;
3286
+ const idx = name != null ? merged.findIndex((e) => e && typeof e === "object" && e.name === name) : -1;
3287
+ if (idx >= 0) {
3288
+ merged[idx] = deepMergeWithOverrides(merged[idx], item);
3289
+ } else {
3290
+ merged.push(item);
3291
+ }
3292
+ }
3293
+ result[key] = merged;
3294
+ continue;
3295
+ }
3258
3296
  if (arrayPrepend || arrayAppend) {
3259
3297
  const existingArr = Array.isArray(existingValue) ? existingValue : [];
3260
3298
  const overrideArr = Array.isArray(value) ? value : [value];
@@ -3284,22 +3322,91 @@ function isOverwriteEnabled() {
3284
3322
  function setOverwriteEnabled(enabled) {
3285
3323
  writeSettings({ overwrite_enabled: enabled });
3286
3324
  }
3325
+ function isOverwriteFilename(filename) {
3326
+ return filename === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(filename);
3327
+ }
3328
+ var MATCH_KEYS = /* @__PURE__ */ new Set(["subscription", "url-domain"]);
3329
+ function normalizeMatch(raw, fileName) {
3330
+ if (raw == null) return void 0;
3331
+ if (typeof raw !== "object" || Array.isArray(raw)) {
3332
+ console.warn(`\u8B66\u544A: \u8986\u5199\u6587\u4EF6 "${fileName}" \u7684 match \u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565\u4F5C\u7528\u57DF\u9650\u5B9A`);
3333
+ return void 0;
3334
+ }
3335
+ const result = {};
3336
+ let hasValid = false;
3337
+ for (const [key, value] of Object.entries(raw)) {
3338
+ if (!MATCH_KEYS.has(key)) {
3339
+ console.warn(`\u8B66\u544A: \u8986\u5199\u6587\u4EF6 "${fileName}" \u7684 match \u542B\u672A\u77E5\u952E "${key}"\uFF0C\u5DF2\u5FFD\u7565`);
3340
+ continue;
3341
+ }
3342
+ const arr = (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v.length > 0);
3343
+ if (arr.length === 0) continue;
3344
+ result[key] = arr;
3345
+ hasValid = true;
3346
+ }
3347
+ return hasValid ? result : void 0;
3348
+ }
3349
+ function summarizeMatch(match) {
3350
+ if (!match) return void 0;
3351
+ const parts = [];
3352
+ for (const [key, value] of Object.entries(match)) {
3353
+ const vals = Array.isArray(value) ? value : [value];
3354
+ parts.push(`${key}=${vals.join("/")}`);
3355
+ }
3356
+ return parts.length > 0 ? parts.join(", ") : void 0;
3357
+ }
3358
+ function splitUrlsLocal(url) {
3359
+ return url.split(",").map((u) => u.trim()).filter(Boolean);
3360
+ }
3361
+ function hostMatchesDomain(host, domain) {
3362
+ const h = host.toLowerCase();
3363
+ const d = domain.toLowerCase();
3364
+ return h === d || h.endsWith(`.${d}`);
3365
+ }
3366
+ function matchesScope(match, scope) {
3367
+ if (!match) return true;
3368
+ if (match.subscription) {
3369
+ const names = Array.isArray(match.subscription) ? match.subscription : [match.subscription];
3370
+ if (!scope?.subName || !names.includes(scope.subName)) return false;
3371
+ }
3372
+ if (match["url-domain"]) {
3373
+ const domains = Array.isArray(match["url-domain"]) ? match["url-domain"] : [match["url-domain"]];
3374
+ if (!scope?.subUrl) return false;
3375
+ const hosts = [];
3376
+ for (const u of splitUrlsLocal(scope.subUrl)) {
3377
+ try {
3378
+ hosts.push(new URL(u).hostname);
3379
+ } catch {
3380
+ }
3381
+ }
3382
+ if (hosts.length === 0) return false;
3383
+ const ok = domains.some((d) => hosts.some((h) => hostMatchesDomain(h, d)));
3384
+ if (!ok) return false;
3385
+ }
3386
+ return true;
3387
+ }
3388
+ function filterOverwriteFilesByScope(files, scope) {
3389
+ return files.filter((f) => matchesScope(f.match, scope));
3390
+ }
3287
3391
  function loadOverwriteFile() {
3288
3392
  const dir = USER_DATA_DIR;
3289
3393
  if (!fs3.existsSync(dir)) return [];
3290
- const files = fs3.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).sort((a, b) => {
3394
+ const files = fs3.readdirSync(dir).filter(isOverwriteFilename).sort((a, b) => {
3291
3395
  if (a === "overwrite.yaml") return -1;
3292
3396
  if (b === "overwrite.yaml") return 1;
3293
3397
  return a.localeCompare(b);
3294
3398
  });
3295
3399
  const results = [];
3296
3400
  for (const file of files) {
3297
- const filePath = path2.join(dir, file);
3401
+ const filePath = path3.join(dir, file);
3298
3402
  try {
3299
3403
  const content = fs3.readFileSync(filePath, "utf8");
3300
3404
  const parsed = load(content);
3301
- if (parsed && typeof parsed === "object") {
3302
- results.push({ name: file, path: filePath, config: parsed });
3405
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3406
+ const { match, ...config } = parsed;
3407
+ results.push({ name: file, path: filePath, config, match: normalizeMatch(match, file) });
3408
+ } else if (parsed !== null) {
3409
+ console.warn(`\u8B66\u544A: \u8986\u5199\u6587\u4EF6 "${file}" \u9876\u5C42\u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u5DF2\u8DF3\u8FC7`);
3303
3410
  }
3304
3411
  } catch (e) {
3305
3412
  console.warn(`\u8B66\u544A: \u8986\u5199\u6587\u4EF6 "${file}" \u89E3\u6790\u5931\u8D25: ${e.message}`);
@@ -3308,9 +3415,9 @@ function loadOverwriteFile() {
3308
3415
  return results;
3309
3416
  }
3310
3417
  function applyOverwrite(baseConfig, preloadedFiles) {
3311
- if (!isOverwriteEnabled()) return baseConfig;
3418
+ if (!isOverwriteEnabled()) return { ...baseConfig };
3312
3419
  const overwriteFiles = preloadedFiles || loadOverwriteFile();
3313
- if (overwriteFiles.length === 0) return baseConfig;
3420
+ if (overwriteFiles.length === 0) return { ...baseConfig };
3314
3421
  let result = { ...baseConfig };
3315
3422
  for (const file of overwriteFiles) {
3316
3423
  result = deepMergeWithOverrides(result, file.config);
@@ -3326,11 +3433,271 @@ function listOverwriteFile() {
3326
3433
  files: files.map((f) => ({
3327
3434
  name: f.name,
3328
3435
  path: f.path,
3329
- keys: Object.keys(f.config || {})
3436
+ keys: Object.keys(f.config || {}),
3437
+ scope: summarizeMatch(f.match)
3330
3438
  }))
3331
3439
  };
3332
3440
  }
3333
3441
 
3442
+ // src/utils.ts
3443
+ import { spawnSync } from "child_process";
3444
+ import { createRequire } from "module";
3445
+ var require2 = createRequire(import.meta.url);
3446
+ var pkg = require2("../package.json");
3447
+ var VERSION = pkg.version;
3448
+ var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
3449
+ var sleepBuf = new Int32Array(new SharedArrayBuffer(4));
3450
+ var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
3451
+ function colorize(code, str) {
3452
+ if (NO_COLOR) return String(str);
3453
+ return `${code + String(str)}\x1B[0m`;
3454
+ }
3455
+ var colors = {
3456
+ bold: (s) => colorize("\x1B[1m", s),
3457
+ red: (s) => colorize("\x1B[31m", s),
3458
+ green: (s) => colorize("\x1B[32m", s),
3459
+ yellow: (s) => colorize("\x1B[33m", s),
3460
+ cyan: (s) => colorize("\x1B[36m", s),
3461
+ gray: (s) => colorize("\x1B[90m", s)
3462
+ };
3463
+ function sleepSync(ms) {
3464
+ Atomics.wait(sleepBuf, 0, 0, ms);
3465
+ }
3466
+ function sleep(ms) {
3467
+ return new Promise((resolve) => setTimeout(resolve, ms));
3468
+ }
3469
+ function escapeRegExp(s) {
3470
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3471
+ }
3472
+ function shellQuote(s) {
3473
+ return `'${s.replace(/'/g, "'\\''")}'`;
3474
+ }
3475
+ var TimeoutError = class extends Error {
3476
+ constructor() {
3477
+ super("timeout");
3478
+ this.name = "TimeoutError";
3479
+ }
3480
+ };
3481
+ function withTimeout(promise, ms) {
3482
+ return new Promise((resolve, reject) => {
3483
+ const timer = setTimeout(() => reject(new TimeoutError()), ms);
3484
+ promise.then(
3485
+ (v) => {
3486
+ clearTimeout(timer);
3487
+ resolve(v);
3488
+ },
3489
+ (e) => {
3490
+ clearTimeout(timer);
3491
+ reject(e);
3492
+ }
3493
+ );
3494
+ });
3495
+ }
3496
+ function formatBytes(bytes) {
3497
+ if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
3498
+ const num = Number(bytes);
3499
+ if (!Number.isFinite(num) || num < 0) return "\u672A\u77E5";
3500
+ if (num === 0) return "0 B";
3501
+ const k = 1024;
3502
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
3503
+ const i = Math.min(Math.floor(Math.log(num) / Math.log(k)), sizes.length - 1);
3504
+ return `${parseFloat((num / k ** i).toFixed(2))} ${sizes[i]}`;
3505
+ }
3506
+ function formatTimestamp(ts) {
3507
+ if (ts === void 0 || ts === null) return "\u672A\u77E5";
3508
+ if (ts === 0) return "\u6C38\u4E45";
3509
+ try {
3510
+ return new Date(ts * 1e3).toLocaleString("zh-CN");
3511
+ } catch {
3512
+ return "\u672A\u77E5";
3513
+ }
3514
+ }
3515
+ function formatLocalTimestamp(d = /* @__PURE__ */ new Date()) {
3516
+ const p = (n) => String(n).padStart(2, "0");
3517
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}`;
3518
+ }
3519
+ function formatDate(dateOrIso) {
3520
+ if (dateOrIso === void 0 || dateOrIso === null) return "\u672A\u77E5";
3521
+ try {
3522
+ const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
3523
+ if (Number.isNaN(d.getTime())) return "\u672A\u77E5";
3524
+ return d.toLocaleString("zh-CN");
3525
+ } catch {
3526
+ return "\u672A\u77E5";
3527
+ }
3528
+ }
3529
+ function hasFlag(args, short, long) {
3530
+ return !!args && (args.includes(short) || long !== void 0 && args.includes(long));
3531
+ }
3532
+ function parseIntArg(args, short, long, defaultValue) {
3533
+ if (!args) return defaultValue;
3534
+ for (let i = 0; i < args.length; i++) {
3535
+ if (args[i] === short || args[i] === long) {
3536
+ if (i + 1 < args.length) {
3537
+ const val = parseInt(args[i + 1], 10);
3538
+ return Number.isNaN(val) ? defaultValue : val;
3539
+ }
3540
+ } else if (args[i].startsWith(`${long}=`)) {
3541
+ const val = parseInt(args[i].slice(long.length + 1), 10);
3542
+ if (!Number.isNaN(val)) return val;
3543
+ }
3544
+ }
3545
+ return defaultValue;
3546
+ }
3547
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["-t", "--timeout", "-j", "--concurrency", "-r", "--rounds", "-n", "--lines", "-u", "--update-timeout"]);
3548
+ function extractStartOptions(args) {
3549
+ if (!args) return [];
3550
+ const BOOL_FLAGS = /* @__PURE__ */ new Set(["-s", "--no-update", "--no-clean"]);
3551
+ const out = [];
3552
+ for (let i = 0; i < args.length; i++) {
3553
+ const a = args[i];
3554
+ if (VALUE_FLAGS.has(a)) {
3555
+ out.push(a);
3556
+ if (i + 1 < args.length) out.push(args[++i]);
3557
+ } else if (BOOL_FLAGS.has(a) || /^--(timeout|concurrency|rounds|update-timeout)=/.test(a)) {
3558
+ out.push(a);
3559
+ }
3560
+ }
3561
+ return out;
3562
+ }
3563
+ function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
3564
+ if (!args) return null;
3565
+ for (let i = startIdx; i < args.length; i++) {
3566
+ const a = args[i];
3567
+ if (a.startsWith("-")) {
3568
+ if (valueFlags.has(a)) i++;
3569
+ continue;
3570
+ }
3571
+ return a;
3572
+ }
3573
+ return null;
3574
+ }
3575
+ function isProcessRunning(pid) {
3576
+ if (!pid) return false;
3577
+ try {
3578
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: 5e3 });
3579
+ return (result.stdout || "").trim().length > 0;
3580
+ } catch {
3581
+ return false;
3582
+ }
3583
+ }
3584
+ function isProcessRoot(pid) {
3585
+ if (!pid) return false;
3586
+ try {
3587
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: 5e3 });
3588
+ return (result.stdout || "").trim() === "0";
3589
+ } catch {
3590
+ return false;
3591
+ }
3592
+ }
3593
+ function createHttpClient(options = {}) {
3594
+ const { timeout = 6e4 } = options;
3595
+ return {
3596
+ async get(url, config) {
3597
+ const controller = new AbortController();
3598
+ const timer = setTimeout(() => controller.abort(), timeout);
3599
+ const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
3600
+ try {
3601
+ const response = await fetch(url, {
3602
+ signal,
3603
+ headers: { "User-Agent": `mihomo-cli/${VERSION}` }
3604
+ });
3605
+ if (!response.ok) {
3606
+ const error = new Error(`HTTP ${response.status}`);
3607
+ error.response = { status: response.status };
3608
+ try {
3609
+ error.response.data = await response.json();
3610
+ } catch {
3611
+ }
3612
+ throw error;
3613
+ }
3614
+ const declaredLen = Number(response.headers.get("content-length"));
3615
+ if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
3616
+ throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
3617
+ }
3618
+ const text = await readBodyWithLimit(response, controller);
3619
+ const data = config?.responseType === "json" ? JSON.parse(text) : text;
3620
+ return { data, headers: response.headers, status: response.status };
3621
+ } finally {
3622
+ clearTimeout(timer);
3623
+ }
3624
+ }
3625
+ };
3626
+ }
3627
+ async function readBodyWithLimit(response, controller) {
3628
+ if (!response.body) return response.text();
3629
+ const reader = response.body.getReader();
3630
+ const chunks = [];
3631
+ let total = 0;
3632
+ try {
3633
+ while (true) {
3634
+ const { done, value } = await reader.read();
3635
+ if (done) break;
3636
+ if (value) {
3637
+ total += value.byteLength;
3638
+ if (total > MAX_RESPONSE_BYTES) {
3639
+ controller.abort();
3640
+ throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
3641
+ }
3642
+ chunks.push(value);
3643
+ }
3644
+ }
3645
+ } finally {
3646
+ reader.releaseLock();
3647
+ }
3648
+ return Buffer.concat(chunks).toString("utf8");
3649
+ }
3650
+ function normalizeMirrorUrl(val) {
3651
+ if (!val) return null;
3652
+ if (val === "direct" || val === "no" || val === "none") return null;
3653
+ let url = val;
3654
+ if (!url.startsWith("http")) {
3655
+ url = `https://${url}`;
3656
+ }
3657
+ if (!url.endsWith("/")) {
3658
+ url += "/";
3659
+ }
3660
+ return url;
3661
+ }
3662
+ function parseMirrorArg(args) {
3663
+ if (!args || args.length < 2) {
3664
+ return { mirror: null, isOverride: false, type: "download" };
3665
+ }
3666
+ if (args.includes("--no-mirror") || args.includes("--direct")) {
3667
+ return { mirror: null, isOverride: true, type: "download" };
3668
+ }
3669
+ const mirrorAllEq = args.find((a) => a.startsWith("--mirror-all="));
3670
+ const mirrorAllIdx = args.indexOf("--mirror-all");
3671
+ if (mirrorAllIdx >= 0 || mirrorAllEq) {
3672
+ const inline = mirrorAllEq?.slice("--mirror-all=".length);
3673
+ const nextArg = inline ?? args[mirrorAllIdx + 1];
3674
+ if (!nextArg || nextArg.startsWith("-")) {
3675
+ return { mirror: DEFAULT_MIRROR, isOverride: true, type: "all" };
3676
+ }
3677
+ return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "all" };
3678
+ }
3679
+ const mirrorEq = args.find((a) => a.startsWith("--mirror="));
3680
+ const mirrorIdx = args.indexOf("--mirror");
3681
+ if (mirrorIdx >= 0 || mirrorEq) {
3682
+ const inline = mirrorEq?.slice("--mirror=".length);
3683
+ const nextArg = inline ?? args[mirrorIdx + 1];
3684
+ if (!nextArg || nextArg.startsWith("-")) {
3685
+ return { mirror: DEFAULT_MIRROR, isOverride: true, type: "download" };
3686
+ }
3687
+ return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "download" };
3688
+ }
3689
+ return { mirror: null, isOverride: false, type: "download" };
3690
+ }
3691
+ function isProxyValid(proxy) {
3692
+ if (!proxy.name || !proxy.server || !proxy.port) return false;
3693
+ if (!proxy.type) return false;
3694
+ if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3695
+ const pw = String(proxy.password || "");
3696
+ if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
3697
+ }
3698
+ return true;
3699
+ }
3700
+
3334
3701
  // src/config.ts
3335
3702
  function parseYamlOrJson(content, errorMsg) {
3336
3703
  if (!content?.trim()) {
@@ -3347,6 +3714,9 @@ function parseYamlOrJson(content, errorMsg) {
3347
3714
  throw new Error(`${errorMsg || "\u5185\u5BB9"}\u683C\u5F0F\u9519\u8BEF\uFF0C\u65E0\u6CD5\u89E3\u6790\u4E3A YAML \u6216 JSON`);
3348
3715
  }
3349
3716
  }
3717
+ function dumpYaml(obj) {
3718
+ return dump(obj, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
3719
+ }
3350
3720
  function collectOverwriteProxyNames(overwriteFiles) {
3351
3721
  const names = [];
3352
3722
  for (const file of overwriteFiles) {
@@ -3354,7 +3724,8 @@ function collectOverwriteProxyNames(overwriteFiles) {
3354
3724
  if ((key === "+proxies" || key === "proxies+") && Array.isArray(value)) {
3355
3725
  for (const proxy of value) {
3356
3726
  if (proxy && typeof proxy === "object" && "name" in proxy) {
3357
- names.push(proxy.name);
3727
+ const name = proxy.name;
3728
+ if (typeof name === "string" && name.length > 0) names.push(name);
3358
3729
  }
3359
3730
  }
3360
3731
  }
@@ -3367,7 +3738,7 @@ function excludeOverwriteProxiesFromIncludeAll(config, overwriteFiles) {
3367
3738
  if (injectedNames.length === 0) return;
3368
3739
  const groups = config["proxy-groups"];
3369
3740
  if (!groups) return;
3370
- const excludePattern = injectedNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
3741
+ const excludePattern = injectedNames.map((n) => escapeRegExp(n)).join("|");
3371
3742
  for (const group of groups) {
3372
3743
  if (!group["include-all"] && !group["include-all-proxies"]) continue;
3373
3744
  const existing = group["exclude-filter"];
@@ -3392,6 +3763,16 @@ function deduplicateByName(items) {
3392
3763
  });
3393
3764
  return { result, names, duplicates };
3394
3765
  }
3766
+ var NON_TARGET_RULE_TYPES = /* @__PURE__ */ new Set(["SUB-RULE"]);
3767
+ function getRuleTarget(rule) {
3768
+ const parts = rule.split(",");
3769
+ if (parts.length < 2) return "";
3770
+ const last = parts[parts.length - 1].trim();
3771
+ if (last.toLowerCase() === "no-resolve" && parts.length >= 3) {
3772
+ return parts[parts.length - 2].trim();
3773
+ }
3774
+ return last;
3775
+ }
3395
3776
  function validateConfig(config) {
3396
3777
  const warnings = [];
3397
3778
  const proxies = config.proxies || [];
@@ -3436,9 +3817,9 @@ function validateConfig(config) {
3436
3817
  if (rules.length > 0) {
3437
3818
  const removedRules = [];
3438
3819
  config.rules = rules.filter((rule) => {
3439
- const parts = rule.split(",");
3440
- if (parts.length < 2) return true;
3441
- const target = parts[parts.length - 1].trim();
3820
+ const ruleType = rule.split(",")[0]?.trim().toUpperCase();
3821
+ if (NON_TARGET_RULE_TYPES.has(ruleType)) return true;
3822
+ const target = getRuleTarget(rule);
3442
3823
  if (!target || validNames.has(target)) return true;
3443
3824
  removedRules.push(rule);
3444
3825
  return false;
@@ -3449,13 +3830,14 @@ function validateConfig(config) {
3449
3830
  }
3450
3831
  return warnings;
3451
3832
  }
3452
- function buildConfig(subRawContent, mode) {
3833
+ function buildConfig(subRawContent, mode, scope) {
3453
3834
  const subscriptionConfig = parseYamlOrJson(subRawContent, "\u8BA2\u9605\u5185\u5BB9");
3454
3835
  if (!subscriptionConfig) {
3455
3836
  throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
3456
3837
  }
3457
3838
  const overwriteEnabled = isOverwriteEnabled();
3458
- const overwriteFiles = overwriteEnabled ? loadOverwriteFile() : [];
3839
+ const allFiles = overwriteEnabled ? loadOverwriteFile() : [];
3840
+ const overwriteFiles = filterOverwriteFilesByScope(allFiles, scope);
3459
3841
  const withOverwrites = applyOverwrite(subscriptionConfig, overwriteFiles);
3460
3842
  if (overwriteFiles.length > 0) {
3461
3843
  excludeOverwriteProxiesFromIncludeAll(withOverwrites, overwriteFiles);
@@ -3466,7 +3848,6 @@ function buildConfig(subRawContent, mode) {
3466
3848
  systemConfig[key] = value;
3467
3849
  }
3468
3850
  }
3469
- systemConfig["allow-lan"] = false;
3470
3851
  systemConfig["external-controller"] = BASE_CONFIG["external-controller"];
3471
3852
  systemConfig["mixed-port"] = BASE_CONFIG["mixed-port"];
3472
3853
  delete withOverwrites["mixed-port"];
@@ -3475,6 +3856,11 @@ function buildConfig(subRawContent, mode) {
3475
3856
  delete withOverwrites["external-ui"];
3476
3857
  delete withOverwrites["external-ui-name"];
3477
3858
  delete withOverwrites["external-ui-url"];
3859
+ delete withOverwrites.secret;
3860
+ const controllerSecret = readSettings().controller_secret;
3861
+ if (controllerSecret) {
3862
+ systemConfig.secret = controllerSecret;
3863
+ }
3478
3864
  if (mode === "tun") {
3479
3865
  systemConfig.tun = TUN_CONFIG.tun;
3480
3866
  const subDns = withOverwrites.dns || {};
@@ -3485,6 +3871,8 @@ function buildConfig(subRawContent, mode) {
3485
3871
  if (Object.keys(dns).length > 0) {
3486
3872
  systemConfig.dns = dns;
3487
3873
  }
3874
+ } else {
3875
+ delete withOverwrites.tun;
3488
3876
  }
3489
3877
  const merged = { ...withOverwrites, ...systemConfig };
3490
3878
  if (systemConfig.dns) {
@@ -3507,20 +3895,19 @@ function buildConfig(subRawContent, mode) {
3507
3895
  }
3508
3896
  function writeMihomoConfig(configObj) {
3509
3897
  ensureDirs();
3510
- const content = dump(configObj, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
3898
+ const content = dumpYaml(configObj);
3511
3899
  atomicWriteFileSync(PATHS.configFile, content, { mode: 384 });
3512
3900
  }
3513
3901
  function writeDebugConfig(buildResult) {
3514
3902
  ensureDirs();
3515
- const dumpOpts = { indent: 2, lineWidth: -1, schema: CORE_SCHEMA };
3516
- fs4.writeFileSync(PATHS.configStage1Subscription, dump(buildResult.subscriptionConfig, dumpOpts), { mode: 384 });
3903
+ fs4.writeFileSync(PATHS.configStage1Subscription, dumpYaml(buildResult.subscriptionConfig), { mode: 384 });
3517
3904
  const overwriteMerged = {};
3518
3905
  for (const f of buildResult.overwriteFiles) {
3519
3906
  Object.assign(overwriteMerged, f.config);
3520
3907
  }
3521
- const overwriteContent = buildResult.overwriteFiles.length > 0 ? dump(overwriteMerged, dumpOpts) : "# overwrite \u5DF2\u7981\u7528\u6216\u65E0\u8986\u5199\u6587\u4EF6\n";
3908
+ const overwriteContent = buildResult.overwriteFiles.length > 0 ? dumpYaml(overwriteMerged) : "# overwrite \u5DF2\u7981\u7528\u6216\u65E0\u8986\u5199\u6587\u4EF6\n";
3522
3909
  fs4.writeFileSync(PATHS.configStage2Overwrite, overwriteContent, { mode: 384 });
3523
- fs4.writeFileSync(PATHS.configStage3System, dump(buildResult.systemConfig, dumpOpts), { mode: 384 });
3910
+ fs4.writeFileSync(PATHS.configStage3System, dumpYaml(buildResult.systemConfig), { mode: 384 });
3524
3911
  }
3525
3912
  function hasConfig() {
3526
3913
  return fs4.existsSync(PATHS.configFile);
@@ -3560,7 +3947,7 @@ function getKernelVersion() {
3560
3947
  }
3561
3948
  if (kernelVersionCached) return kernelVersionCache;
3562
3949
  try {
3563
- const result = spawnSync(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
3950
+ const result = spawnSync2(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
3564
3951
  const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
3565
3952
  if (output) {
3566
3953
  const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
@@ -3579,211 +3966,113 @@ function clearKernelVersionCache() {
3579
3966
  kernelVersionCached = false;
3580
3967
  }
3581
3968
 
3582
- // src/daemon.ts
3583
- import { spawnSync as spawnSync4 } from "child_process";
3584
- import fs6 from "fs";
3585
- import path4 from "path";
3586
-
3587
- // src/process.ts
3588
- import { spawn, spawnSync as spawnSync3 } from "child_process";
3589
- import fs5 from "fs";
3590
- import path3 from "path";
3591
-
3592
- // src/utils.ts
3593
- import { spawnSync as spawnSync2 } from "child_process";
3594
- import { createRequire } from "module";
3595
- var require2 = createRequire(import.meta.url);
3596
- var pkg = require2("../package.json");
3597
- var VERSION = pkg.version;
3598
- var sleepBuf = new Int32Array(new SharedArrayBuffer(4));
3599
- var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
3600
- function colorize(code, str) {
3601
- if (NO_COLOR) return String(str);
3602
- return `${code + String(str)}\x1B[0m`;
3603
- }
3604
- var colors = {
3605
- bold: (s) => colorize("\x1B[1m", s),
3606
- red: (s) => colorize("\x1B[31m", s),
3607
- green: (s) => colorize("\x1B[32m", s),
3608
- yellow: (s) => colorize("\x1B[33m", s),
3609
- cyan: (s) => colorize("\x1B[36m", s),
3610
- gray: (s) => colorize("\x1B[90m", s)
3611
- };
3612
- function sleepSync(ms) {
3613
- Atomics.wait(sleepBuf, 0, 0, ms);
3614
- }
3615
- function sleep(ms) {
3616
- return new Promise((resolve) => setTimeout(resolve, ms));
3617
- }
3618
- var TimeoutError = class extends Error {
3619
- constructor() {
3620
- super("timeout");
3621
- this.name = "TimeoutError";
3622
- }
3623
- };
3624
- function withTimeout(promise, ms) {
3625
- return new Promise((resolve, reject) => {
3626
- const timer = setTimeout(() => reject(new TimeoutError()), ms);
3627
- promise.then(
3628
- (v) => {
3629
- clearTimeout(timer);
3630
- resolve(v);
3631
- },
3632
- (e) => {
3633
- clearTimeout(timer);
3634
- reject(e);
3635
- }
3636
- );
3637
- });
3638
- }
3639
- function formatBytes(bytes) {
3640
- if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
3641
- const num = Number(bytes);
3642
- if (!Number.isFinite(num) || num < 0) return "\u672A\u77E5";
3643
- if (num === 0) return "0 B";
3644
- const k = 1024;
3645
- const sizes = ["B", "KB", "MB", "GB", "TB"];
3646
- const i = Math.min(Math.floor(Math.log(num) / Math.log(k)), sizes.length - 1);
3647
- return `${parseFloat((num / k ** i).toFixed(2))} ${sizes[i]}`;
3648
- }
3649
- function formatTimestamp(ts) {
3650
- if (ts === void 0 || ts === null) return "\u672A\u77E5";
3651
- try {
3652
- return new Date(ts * 1e3).toLocaleString("zh-CN");
3653
- } catch {
3654
- return "\u672A\u77E5";
3655
- }
3656
- }
3657
- function formatDate(dateOrIso) {
3658
- if (dateOrIso === void 0 || dateOrIso === null) return "\u672A\u77E5";
3659
- try {
3660
- const d = dateOrIso instanceof Date ? dateOrIso : new Date(dateOrIso);
3661
- if (Number.isNaN(d.getTime())) return "\u672A\u77E5";
3662
- return d.toLocaleString("zh-CN");
3663
- } catch {
3664
- return "\u672A\u77E5";
3665
- }
3969
+ // src/commands/help.ts
3970
+ function printShortHelp() {
3971
+ console.log(`
3972
+ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
3973
+ `);
3974
+ console.log(
3975
+ `\u5E38\u7528\u547D\u4EE4:
3976
+ ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
3977
+ ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
3978
+ ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
3979
+ ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
3980
+ `
3981
+ );
3666
3982
  }
3667
- function hasFlag(args, short, long) {
3668
- return !!args && (args.includes(short) || args.includes(long));
3983
+ var GROUP_TITLES = [
3984
+ ["control", "\u63A7\u5236:"],
3985
+ ["interface", "\u754C\u9762:"],
3986
+ ["subscription", "\u8BA2\u9605:"],
3987
+ ["config", "\u914D\u7F6E:"],
3988
+ ["system", "\u7CFB\u7EDF:"]
3989
+ ];
3990
+ function printHelp(commands) {
3991
+ const lines = [`
3992
+ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}`, "", "\u547D\u4EE4\u522B\u540D: mihomo, mhm, mh", "", "\u7528\u6CD5:", " mihomo <\u547D\u4EE4> [\u9009\u9879]"];
3993
+ for (const [group, title] of GROUP_TITLES) {
3994
+ const usageLines = commands.filter((c) => c.group === group).flatMap((c) => c.usage);
3995
+ if (usageLines.length === 0) continue;
3996
+ lines.push("", colors.cyan(title));
3997
+ for (const u of usageLines) {
3998
+ lines.push(u.startsWith(" ") ? ` ${u}` : ` ${boldFirstToken(u)}`);
3999
+ }
4000
+ }
4001
+ const meta = commands.filter((c) => c.group === "meta").flatMap((c) => c.usage);
4002
+ if (meta.length > 0) {
4003
+ lines.push("", colors.cyan("\u5143:"));
4004
+ for (const u of meta) lines.push(` ${boldFirstToken(u)}`);
4005
+ }
4006
+ lines.push(
4007
+ "",
4008
+ `${colors.cyan("\u793A\u4F8B:")}`,
4009
+ " mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F",
4010
+ " mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F",
4011
+ " mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605",
4012
+ " mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)",
4013
+ " mihomo daemon on # \u5F00\u542F\u4FDD\u6D3B\uFF08\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF09",
4014
+ " mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)",
4015
+ " mihomo ui # \u6253\u5F00 Web UI",
4016
+ "",
4017
+ `${colors.cyan("\u5FEB\u6377\u547D\u4EE4:")}`,
4018
+ " tun = start tun use = sub use on/off = ow on/off open = dir open",
4019
+ " up = start down = stop upd/upgrade = update",
4020
+ "",
4021
+ `${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}`,
4022
+ " mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)",
4023
+ " tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo",
4024
+ "",
4025
+ `${colors.cyan("\u6570\u636E\u76EE\u5F55:")}`,
4026
+ " \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E",
4027
+ ` \u9ED8\u8BA4: ${USER_DATA_DIR}`
4028
+ );
4029
+ console.log(lines.join("\n"));
3669
4030
  }
3670
- function parseIntArg(args, short, long, defaultValue) {
3671
- if (!args) return defaultValue;
3672
- for (let i = 0; i < args.length; i++) {
3673
- if (args[i] === short || args[i] === long) {
3674
- if (i + 1 < args.length) {
3675
- const val = parseInt(args[i + 1], 10);
3676
- return Number.isNaN(val) ? defaultValue : val;
3677
- }
3678
- }
3679
- }
3680
- return defaultValue;
4031
+ function boldFirstToken(usage) {
4032
+ const spaceIdx = usage.indexOf(" ");
4033
+ if (spaceIdx < 0) return colors.bold(usage);
4034
+ return `${colors.bold(usage.slice(0, spaceIdx))}${usage.slice(spaceIdx)}`;
3681
4035
  }
3682
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["-t", "--timeout", "-j", "--concurrency", "-r", "--rounds", "-n", "--lines", "-u", "--update-timeout"]);
3683
- function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
3684
- if (!args) return null;
3685
- for (let i = startIdx; i < args.length; i++) {
3686
- const a = args[i];
3687
- if (a.startsWith("-")) {
3688
- if (valueFlags.has(a)) i++;
3689
- continue;
3690
- }
3691
- return a;
3692
- }
3693
- return null;
4036
+ function printVersion() {
4037
+ const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
4038
+ console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
4039
+ console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
4040
+ console.log(`${colors.gray("\u6570\u636E\u76EE\u5F55: ")}${USER_DATA_DIR}`);
3694
4041
  }
3695
- function isProcessRunning(pid) {
3696
- if (!pid) return false;
3697
- try {
3698
- const result = spawnSync2("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: 5e3 });
3699
- return (result.stdout || "").trim().length > 0;
3700
- } catch {
3701
- return false;
3702
- }
4042
+
4043
+ // src/daemon.ts
4044
+ import { spawnSync as spawnSync4 } from "child_process";
4045
+ import fs6 from "fs";
4046
+ import path5 from "path";
4047
+
4048
+ // src/process.ts
4049
+ import { spawn, spawnSync as spawnSync3 } from "child_process";
4050
+ import fs5 from "fs";
4051
+ import path4 from "path";
4052
+
4053
+ // src/lifecycle.ts
4054
+ var cleanupFns = /* @__PURE__ */ new Set();
4055
+ var silentSigint = false;
4056
+ function setSilentSigint(value) {
4057
+ silentSigint = value;
3703
4058
  }
3704
- function isProcessRoot(pid) {
3705
- if (!pid) return false;
3706
- try {
3707
- const result = spawnSync2("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: 5e3 });
3708
- return (result.stdout || "").trim() === "0";
3709
- } catch {
3710
- return false;
3711
- }
4059
+ function isSilentSigint() {
4060
+ return silentSigint;
3712
4061
  }
3713
- function createHttpClient(options = {}) {
3714
- const { timeout = 6e4 } = options;
3715
- return {
3716
- async get(url, config) {
3717
- const controller = new AbortController();
3718
- const timer = setTimeout(() => controller.abort(), timeout);
3719
- const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
3720
- try {
3721
- const response = await fetch(url, {
3722
- signal,
3723
- headers: { "User-Agent": `mihomo-cli/${VERSION}` }
3724
- });
3725
- if (!response.ok) {
3726
- const error = new Error(`HTTP ${response.status}`);
3727
- error.response = { status: response.status };
3728
- try {
3729
- error.response.data = await response.json();
3730
- } catch {
3731
- }
3732
- throw error;
3733
- }
3734
- const data = config?.responseType === "json" ? await response.json() : await response.text();
3735
- return { data, headers: response.headers, status: response.status };
3736
- } finally {
3737
- clearTimeout(timer);
3738
- }
3739
- }
4062
+ function registerCleanup(fn) {
4063
+ cleanupFns.add(fn);
4064
+ return () => {
4065
+ cleanupFns.delete(fn);
3740
4066
  };
3741
4067
  }
3742
- function normalizeMirrorUrl(val) {
3743
- if (!val) return null;
3744
- if (val === "direct" || val === "no" || val === "none") return null;
3745
- let url = val;
3746
- if (!url.startsWith("http")) {
3747
- url = `https://${url}`;
3748
- }
3749
- if (!url.endsWith("/")) {
3750
- url += "/";
3751
- }
3752
- return url;
3753
- }
3754
- function parseMirrorArg(args) {
3755
- if (!args || args.length < 2) {
3756
- return { mirror: null, isOverride: false, type: "download" };
3757
- }
3758
- if (args.includes("--no-mirror") || args.includes("--direct")) {
3759
- return { mirror: null, isOverride: true, type: "download" };
3760
- }
3761
- const mirrorAllIdx = args.indexOf("--mirror-all");
3762
- if (mirrorAllIdx >= 0) {
3763
- const nextArg = args[mirrorAllIdx + 1];
3764
- if (!nextArg || nextArg.startsWith("-")) {
3765
- return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "all" };
3766
- }
3767
- return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "all" };
3768
- }
3769
- const mirrorIdx = args.indexOf("--mirror");
3770
- if (mirrorIdx >= 0) {
3771
- const nextArg = args[mirrorIdx + 1];
3772
- if (!nextArg || nextArg.startsWith("-")) {
3773
- return { mirror: "https://v6.gh-proxy.org/", isOverride: true, type: "download" };
4068
+ function runCleanup() {
4069
+ for (const fn of cleanupFns) {
4070
+ try {
4071
+ fn();
4072
+ } catch {
3774
4073
  }
3775
- return { mirror: normalizeMirrorUrl(nextArg), isOverride: true, type: "download" };
3776
- }
3777
- return { mirror: null, isOverride: false, type: "download" };
3778
- }
3779
- function isProxyValid(proxy) {
3780
- if (!proxy.name || !proxy.server || !proxy.port) return false;
3781
- if (!proxy.type) return false;
3782
- if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
3783
- const pw = String(proxy.password || "");
3784
- if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
3785
4074
  }
3786
- return true;
4075
+ cleanupFns.clear();
3787
4076
  }
3788
4077
 
3789
4078
  // src/process.ts
@@ -3794,9 +4083,7 @@ var SUDO_TIMEOUT_MS = 6e4;
3794
4083
  var TUN_MODE_POST_WAIT_MS = 500;
3795
4084
  var BATCH_KILL_THRESHOLD = 3;
3796
4085
  var DEFAULT_LOG_RETENTION_DAYS = 7;
3797
- function escapeForPgrep(s) {
3798
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3799
- }
4086
+ var MAIN_INSTANCE_PATTERN = `${escapeRegExp(PATHS.mihomoBinary)}.*${escapeRegExp(PATHS.configFile)}`;
3800
4087
  function clearRuntime() {
3801
4088
  if (fs5.existsSync(DIRS.runtime)) {
3802
4089
  rmrf(DIRS.runtime);
@@ -3816,10 +4103,9 @@ function isRunning() {
3816
4103
  const pid = getPid();
3817
4104
  return pid ? isProcessRunning(pid) : false;
3818
4105
  }
3819
- function getAllMihomoPids() {
3820
- const binaryPath = PATHS.mihomoBinary;
4106
+ function getMihomoPids() {
3821
4107
  try {
3822
- const result = spawnSync3("pgrep", ["-f", escapeForPgrep(binaryPath)], { encoding: "utf8", timeout: 1e4 });
4108
+ const result = spawnSync3("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
3823
4109
  const output = (result.stdout || "").trim();
3824
4110
  if (!output) return [];
3825
4111
  return output.split("\n").filter(Boolean).map((p) => parseInt(p, 10)).filter((p) => Number.isInteger(p) && p > 0);
@@ -3837,7 +4123,7 @@ function isPidFileOwnedByRoot() {
3837
4123
  }
3838
4124
  }
3839
4125
  function checkStaleState() {
3840
- const allPids = getAllMihomoPids();
4126
+ const allPids = getMihomoPids();
3841
4127
  const hasRootProcess = allPids.some((p) => isProcessRoot(p));
3842
4128
  const hasRootPidFile = isPidFileOwnedByRoot();
3843
4129
  return {
@@ -3848,6 +4134,9 @@ function checkStaleState() {
3848
4134
  needsSudo: hasRootProcess || hasRootPidFile
3849
4135
  };
3850
4136
  }
4137
+ function hasRootResidue() {
4138
+ return checkStaleState().needsSudo;
4139
+ }
3851
4140
  function savePid(pid) {
3852
4141
  ensureDirs();
3853
4142
  fs5.writeFileSync(PATHS.pidFile, pid.toString(), { mode: 384 });
@@ -3866,29 +4155,16 @@ function clearPid() {
3866
4155
  }
3867
4156
  }
3868
4157
  }
3869
- function killProcess(pid, needsSudo = false) {
4158
+ function killProcess(pid) {
3870
4159
  try {
3871
- if (needsSudo) {
3872
- const result = spawnSync3("sudo", ["kill", "-9", String(pid)], { stdio: "inherit", timeout: 1e4 });
3873
- if (result.status === 0) {
3874
- return true;
3875
- }
3876
- try {
3877
- process.kill(pid, "SIGKILL");
3878
- return true;
3879
- } catch {
3880
- return false;
3881
- }
3882
- } else {
3883
- process.kill(pid, "SIGKILL");
3884
- return true;
3885
- }
4160
+ process.kill(pid, "SIGKILL");
4161
+ return true;
3886
4162
  } catch {
3887
4163
  return false;
3888
4164
  }
3889
4165
  }
3890
4166
  function killAllMihomo(forceSudo = false) {
3891
- const pattern = escapeForPgrep(PATHS.mihomoBinary);
4167
+ const pattern = MAIN_INSTANCE_PATTERN;
3892
4168
  if (forceSudo) {
3893
4169
  try {
3894
4170
  spawnSync3("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
@@ -3906,7 +4182,7 @@ function killAllMihomo(forceSudo = false) {
3906
4182
  }
3907
4183
  }
3908
4184
  function cleanupAll(forceSudo = false) {
3909
- const pids = getAllMihomoPids();
4185
+ const pids = getMihomoPids();
3910
4186
  if (pids.length === 0) {
3911
4187
  clearPid();
3912
4188
  return { killed: 0, failed: 0, remaining: [] };
@@ -3928,7 +4204,7 @@ function cleanupAll(forceSudo = false) {
3928
4204
  killedCount = pids.length;
3929
4205
  } else {
3930
4206
  for (const pid of pids) {
3931
- if (killProcess(pid, false)) {
4207
+ if (killProcess(pid)) {
3932
4208
  killedCount++;
3933
4209
  } else {
3934
4210
  failedPids.push(pid);
@@ -3937,26 +4213,26 @@ function cleanupAll(forceSudo = false) {
3937
4213
  }
3938
4214
  }
3939
4215
  for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
3940
- if (getAllMihomoPids().length === 0) break;
4216
+ if (getMihomoPids().length === 0) break;
3941
4217
  sleepSync(PROCESS_WAIT_INTERVAL);
3942
4218
  }
3943
4219
  clearPid();
3944
- return { killed: killedCount, failed: failedPids.length, remaining: getAllMihomoPids() };
4220
+ return { killed: killedCount, failed: failedPids.length, remaining: getMihomoPids() };
3945
4221
  }
3946
4222
  function createTunLaunchScript() {
3947
- const binary = PATHS.mihomoBinary;
3948
- const configFile = PATHS.configFile;
3949
- const logFile = PATHS.logFile;
3950
- const pidFile = PATHS.pidFile;
3951
- const dataDir = DIRS.data;
3952
- const killPattern = escapeForPgrep(binary);
4223
+ const binary = shellQuote(PATHS.mihomoBinary);
4224
+ const configFile = shellQuote(PATHS.configFile);
4225
+ const logFile = shellQuote(PATHS.logFile);
4226
+ const pidFile = shellQuote(PATHS.pidFile);
4227
+ const dataDir = shellQuote(DIRS.data);
4228
+ const killPattern = shellQuote(MAIN_INSTANCE_PATTERN);
3953
4229
  const scriptContent = `#!/bin/bash
3954
- BINARY="${binary}"
3955
- CONFIG_FILE="${configFile}"
3956
- LOG_FILE="${logFile}"
3957
- PID_FILE="${pidFile}"
3958
- DATA_DIR="${dataDir}"
3959
- KILL_PATTERN='${killPattern}'
4230
+ BINARY=${binary}
4231
+ CONFIG_FILE=${configFile}
4232
+ LOG_FILE=${logFile}
4233
+ PID_FILE=${pidFile}
4234
+ DATA_DIR=${dataDir}
4235
+ KILL_PATTERN=${killPattern}
3960
4236
 
3961
4237
  # \u7EC8\u6B62\u65E7\u8FDB\u7A0B
3962
4238
  pkill -9 -f "\${KILL_PATTERN}" 2>/dev/null || true
@@ -3980,14 +4256,15 @@ for i in 1 2 3 4 5; do
3980
4256
  fi
3981
4257
  done
3982
4258
 
3983
- # \u5931\u8D25\uFF0C\u663E\u793A\u65E5\u5FD7
4259
+ # \u5931\u8D25\uFF0C\u663E\u793A\u65E5\u5FD7\uFF08\u9000\u51FA\u7801 2\uFF1A\u907F\u5F00 sudo \u7684 1=\u9274\u6743\u5931\u8D25/\u53D6\u6D88\uFF0C\u4F9B\u8C03\u7528\u65B9\u533A\u5206\uFF09
4260
+ rm -f "\${PID_FILE}" 2>/dev/null || true
3984
4261
  echo "TUN \u542F\u52A8\u5931\u8D25"
3985
4262
  echo ""
3986
4263
  echo "--- \u65E5\u5FD7 ---"
3987
4264
  tail -25 "\${LOG_FILE}" 2>/dev/null
3988
- exit 1
4265
+ exit 2
3989
4266
  `;
3990
- const scriptPath = path3.join(DIRS.runtime, "launch-tun.sh");
4267
+ const scriptPath = path4.join(DIRS.runtime, "launch-tun.sh");
3991
4268
  fs5.writeFileSync(scriptPath, scriptContent, { mode: 448 });
3992
4269
  return scriptPath;
3993
4270
  }
@@ -4009,7 +4286,7 @@ function getProcessInfo(pid) {
4009
4286
  function getStatus() {
4010
4287
  const running = isRunning();
4011
4288
  const pid = getPid();
4012
- const allPids = getAllMihomoPids();
4289
+ const allPids = getMihomoPids();
4013
4290
  return {
4014
4291
  running,
4015
4292
  pid: running ? pid : null,
@@ -4043,7 +4320,7 @@ async function startMixedMode(staleState) {
4043
4320
  if (staleState.needsCleanup) {
4044
4321
  if (staleState.needsSudo) {
4045
4322
  console.log("\n\u53D1\u73B0\u9700\u8981 root \u6743\u9650\u6E05\u7406\u7684\u6B8B\u7559\u8FDB\u7A0B/\u6587\u4EF6");
4046
- console.log("\u8BF7\u5148\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo");
4323
+ console.log(`\u8BF7\u5148\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo && sudo rm -f ${PATHS.pidFile}`);
4047
4324
  console.log("\u6216\u8005\u5207\u6362\u5230 TUN \u6A21\u5F0F\uFF0C\u542F\u52A8\u65F6\u4F1A\u81EA\u52A8\u6E05\u7406");
4048
4325
  throw new Error("\u5B58\u5728\u9700\u8981 root \u6743\u9650\u6E05\u7406\u7684\u6B8B\u7559");
4049
4326
  }
@@ -4059,14 +4336,30 @@ async function startMixedMode(staleState) {
4059
4336
  const configFile = PATHS.configFile;
4060
4337
  const logFile = PATHS.logFile;
4061
4338
  const args = ["-d", DIRS.data, "-f", configFile];
4339
+ if (fs5.existsSync(logFile)) {
4340
+ try {
4341
+ fs5.accessSync(logFile, fs5.constants.W_OK);
4342
+ } catch {
4343
+ try {
4344
+ fs5.unlinkSync(logFile);
4345
+ } catch {
4346
+ }
4347
+ }
4348
+ }
4062
4349
  const logFd = fs5.openSync(logFile, "a");
4063
4350
  const child = spawn(PATHS.mihomoBinary, args, {
4064
4351
  detached: true,
4065
4352
  stdio: ["ignore", logFd, logFd]
4066
4353
  });
4354
+ child.on("error", () => {
4355
+ });
4067
4356
  fs5.closeSync(logFd);
4068
4357
  child.unref();
4069
4358
  const pid = child.pid;
4359
+ if (!pid) {
4360
+ clearPid();
4361
+ throw new Error("\u542F\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u521B\u5EFA\u5185\u6838\u8FDB\u7A0B\uFF08\u5185\u6838\u4E8C\u8FDB\u5236\u53EF\u80FD\u4E0D\u53EF\u6267\u884C\uFF09");
4362
+ }
4070
4363
  savePid(pid);
4071
4364
  await new Promise((resolve) => setTimeout(resolve, STARTUP_WAIT_MS));
4072
4365
  if (!isRunning()) {
@@ -4107,6 +4400,9 @@ async function startTunMode(staleState) {
4107
4400
  if (e.status === 1) {
4108
4401
  throw new Error("\u5BC6\u7801\u9519\u8BEF\u6216\u53D6\u6D88");
4109
4402
  }
4403
+ if (e.status === 2) {
4404
+ throw new Error("TUN \u542F\u52A8\u5931\u8D25\uFF08\u8BE6\u89C1\u4E0A\u65B9\u65E5\u5FD7\uFF09");
4405
+ }
4110
4406
  throw new Error(e.message);
4111
4407
  }
4112
4408
  try {
@@ -4121,14 +4417,14 @@ async function startTunMode(staleState) {
4121
4417
  return { success: true, pid: finalPid, mode: "tun" };
4122
4418
  }
4123
4419
  function stop(forceSudo = false) {
4124
- const allPids = getAllMihomoPids();
4420
+ const allPids = getMihomoPids();
4125
4421
  if (allPids.length === 0) {
4126
4422
  clearPid();
4127
4423
  clearRuntime();
4128
4424
  return { success: true, notRunning: true };
4129
4425
  }
4130
4426
  const result = cleanupAll(forceSudo);
4131
- const remaining = getAllMihomoPids();
4427
+ const remaining = getMihomoPids();
4132
4428
  if (remaining.length > 0) {
4133
4429
  console.log("");
4134
4430
  console.log("\u4ECD\u6709\u8FDB\u7A0B\u6B8B\u7559\uFF0C\u9700\u8981\u624B\u52A8\u6E05\u7406:");
@@ -4152,9 +4448,8 @@ function rotateLog() {
4152
4448
  if (!fs5.existsSync(logFile)) return null;
4153
4449
  const stat = fs5.statSync(logFile);
4154
4450
  if (stat.size === 0) return null;
4155
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/T/, "_").replace(/:/g, "-").replace(/\..+/, "");
4156
- const rotatedName = `mihomo.${timestamp}.log`;
4157
- const rotatedPath = path3.join(DIRS.logs, rotatedName);
4451
+ const rotatedName = `mihomo.${formatLocalTimestamp()}.log`;
4452
+ const rotatedPath = path4.join(DIRS.logs, rotatedName);
4158
4453
  fs5.renameSync(logFile, rotatedPath);
4159
4454
  return rotatedPath;
4160
4455
  }
@@ -4169,7 +4464,7 @@ function cleanupOldLogs(maxAgeDays = DEFAULT_LOG_RETENTION_DAYS) {
4169
4464
  for (const file of files) {
4170
4465
  if (!file.match(/^mihomo\.\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$/)) continue;
4171
4466
  try {
4172
- const filePath = path3.join(logsDir, file);
4467
+ const filePath = path4.join(logsDir, file);
4173
4468
  const stat = fs5.statSync(filePath);
4174
4469
  if (now - stat.mtimeMs > maxAgeMs) {
4175
4470
  fs5.unlinkSync(filePath);
@@ -4200,7 +4495,7 @@ function listLogs() {
4200
4495
  const match = file.match(/^mihomo\.(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.log$/);
4201
4496
  if (!match) continue;
4202
4497
  try {
4203
- const filePath = path3.join(logsDir, file);
4498
+ const filePath = path4.join(logsDir, file);
4204
4499
  const stat = fs5.statSync(filePath);
4205
4500
  result.archives.push({
4206
4501
  name: file,
@@ -4216,22 +4511,22 @@ function listLogs() {
4216
4511
  return result;
4217
4512
  }
4218
4513
  function isPathUnderDir(filePath, baseDir) {
4219
- const resolvedPath = path3.resolve(filePath);
4220
- const resolvedBase = path3.resolve(baseDir);
4221
- return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path3.sep);
4514
+ const resolvedPath = path4.resolve(filePath);
4515
+ const resolvedBase = path4.resolve(baseDir);
4516
+ return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path4.sep);
4222
4517
  }
4223
4518
  function getLogPathByName(name) {
4224
4519
  const logsDir = DIRS.logs;
4225
4520
  let targetName = name;
4226
4521
  if (!name.endsWith(".log")) targetName = `mihomo.${name}.log`;
4227
4522
  if (!targetName.startsWith("mihomo.")) targetName = `mihomo.${targetName}`;
4228
- const filePath = path3.join(logsDir, targetName);
4523
+ const filePath = path4.join(logsDir, targetName);
4229
4524
  if (fs5.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) return filePath;
4230
4525
  if (fs5.existsSync(logsDir)) {
4231
4526
  const files = fs5.readdirSync(logsDir);
4232
4527
  for (const file of files) {
4233
4528
  if (file.includes(name)) {
4234
- const candidatePath = path3.join(logsDir, file);
4529
+ const candidatePath = path4.join(logsDir, file);
4235
4530
  if (isPathUnderDir(candidatePath, logsDir)) return candidatePath;
4236
4531
  }
4237
4532
  }
@@ -4272,6 +4567,7 @@ function viewLogWithTail(logPath, options) {
4272
4567
  tailArgs.push("-n", lines.toString());
4273
4568
  tailArgs.push(logPath);
4274
4569
  const tail = spawn("tail", tailArgs, { stdio: "inherit" });
4570
+ if (follow) setSilentSigint(true);
4275
4571
  tail.on("close", () => process.exit(0));
4276
4572
  tail.on("error", (e) => {
4277
4573
  console.error(`\u65E0\u6CD5\u8BFB\u53D6\u65E5\u5FD7: ${e.message}`);
@@ -4280,31 +4576,16 @@ function viewLogWithTail(logPath, options) {
4280
4576
  }
4281
4577
 
4282
4578
  // src/daemon.ts
4283
- var LAUNCHCTL_TIMEOUT_MS = 1e4;
4284
- function launchctl(args) {
4285
- const result = spawnSync4("launchctl", args, { encoding: "utf8", timeout: LAUNCHCTL_TIMEOUT_MS });
4286
- return {
4287
- status: result.status,
4288
- stdout: result.stdout || "",
4289
- stderr: result.stderr || ""
4290
- };
4291
- }
4292
- function getUid() {
4293
- const uid = process.getuid?.();
4294
- if (uid === void 0) {
4295
- throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7528\u6237 UID\uFF0C\u4FDD\u6D3B\u529F\u80FD\u4EC5\u652F\u6301 macOS");
4579
+ var SERVICE_TARGET = `system/${LAUNCH_DAEMON_LABEL}`;
4580
+ var HOT_RELOAD_TIMEOUT_MS = 5e3;
4581
+ var DAEMON_BOOT_WAIT_MS = 500;
4582
+ var LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
4583
+ function logOversized() {
4584
+ try {
4585
+ return fs6.statSync(PATHS.logFile).size > LOG_ROTATE_MAX_BYTES;
4586
+ } catch {
4587
+ return false;
4296
4588
  }
4297
- return uid;
4298
- }
4299
- function guiDomain(uid) {
4300
- return `gui/${uid}`;
4301
- }
4302
- function serviceTarget(uid) {
4303
- return `gui/${uid}/${LAUNCH_AGENT_LABEL}`;
4304
- }
4305
- function launchctlError(action, result) {
4306
- const detail = (result.stderr || result.stdout || "").trim() || `\u9000\u51FA\u7801 ${result.status}`;
4307
- return new Error(`launchctl ${action} \u5931\u8D25: ${detail}`);
4308
4589
  }
4309
4590
  function escapeXml(s) {
4310
4591
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
@@ -4317,7 +4598,7 @@ function buildPlist() {
4317
4598
  <plist version="1.0">
4318
4599
  <dict>
4319
4600
  <key>Label</key>
4320
- <string>${escapeXml(LAUNCH_AGENT_LABEL)}</string>
4601
+ <string>${escapeXml(LAUNCH_DAEMON_LABEL)}</string>
4321
4602
  <key>ProgramArguments</key>
4322
4603
  <array>
4323
4604
  ${argsXml}
@@ -4336,25 +4617,42 @@ ${argsXml}
4336
4617
  </plist>
4337
4618
  `;
4338
4619
  }
4620
+ function runSudoScript(scriptBody, opts) {
4621
+ if (!process.stdin.isTTY) {
4622
+ throw new Error("\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u8F93\u5165\u7BA1\u7406\u5458\u5BC6\u7801\uFF08\u9700\u8981\u5728\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\u8FD0\u884C sudo\uFF09");
4623
+ }
4624
+ ensureDirs();
4625
+ const scriptPath = path5.join(DIRS.runtime, opts.file);
4626
+ fs6.writeFileSync(scriptPath, scriptBody, { mode: 448 });
4627
+ try {
4628
+ const result = spawnSync4("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
4629
+ if (result.error) throw result.error;
4630
+ if (result.status !== 0) {
4631
+ if (result.status === 1) {
4632
+ throw new Error("\u5DF2\u53D6\u6D88\u6216\u5BC6\u7801\u9519\u8BEF");
4633
+ }
4634
+ if (result.status == null) {
4635
+ throw new Error(`${opts.action}\u88AB\u4E2D\u65AD\uFF08sudo \u8FDB\u7A0B\u88AB\u4FE1\u53F7\u7EC8\u6B62\uFF09`);
4636
+ }
4637
+ const custom = opts.codeMessages?.[result.status];
4638
+ throw new Error(custom || `${opts.action}\u5931\u8D25\uFF08\u9000\u51FA\u7801 ${result.status}\uFF09`);
4639
+ }
4640
+ } finally {
4641
+ try {
4642
+ fs6.unlinkSync(scriptPath);
4643
+ } catch {
4644
+ }
4645
+ }
4646
+ }
4339
4647
  function isDaemonEnabled() {
4340
- return fs6.existsSync(PATHS.launchAgentPlist);
4648
+ return fs6.existsSync(PATHS.launchDaemonPlist);
4341
4649
  }
4342
4650
  function getDaemonStatus() {
4343
4651
  if (!isDaemonEnabled()) {
4344
4652
  return { enabled: false, loaded: false, pid: null };
4345
4653
  }
4346
- const uid = process.getuid?.();
4347
- if (uid === void 0) {
4348
- const pids = getAllMihomoPids();
4349
- return { enabled: true, loaded: pids.length > 0, pid: pids[0] ?? null };
4350
- }
4351
- const result = launchctl(["print", serviceTarget(uid)]);
4352
- if (result.status !== 0) {
4353
- return { enabled: true, loaded: false, pid: null };
4354
- }
4355
- const pidMatch = result.stdout.match(/\bpid = (\d+)/);
4356
- const pid = pidMatch ? parseInt(pidMatch[1], 10) : null;
4357
- return { enabled: true, loaded: true, pid };
4654
+ const rootPids = getMihomoPids().filter(isProcessRoot);
4655
+ return { enabled: true, loaded: rootPids.length > 0, pid: rootPids[0] ?? null };
4358
4656
  }
4359
4657
  function isDaemonRunning(status) {
4360
4658
  return status.loaded && status.pid !== null;
@@ -4366,52 +4664,113 @@ function enableDaemon() {
4366
4664
  if (!fs6.existsSync(PATHS.configFile)) {
4367
4665
  throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4368
4666
  }
4369
- const uid = getUid();
4370
- launchctl(["bootout", serviceTarget(uid)]);
4371
- cleanupAll();
4372
- fs6.mkdirSync(path4.dirname(PATHS.launchAgentPlist), { recursive: true });
4373
- atomicWriteFileSync(PATHS.launchAgentPlist, buildPlist(), { mode: 384 });
4374
- const result = launchctl(["bootstrap", guiDomain(uid), PATHS.launchAgentPlist]);
4375
- if (result.status !== 0) {
4376
- launchctl(["bootout", serviceTarget(uid)]);
4667
+ ensureDirs();
4668
+ const stagePath = path5.join(DIRS.runtime, "daemon.plist.stage");
4669
+ atomicWriteFileSync(stagePath, buildPlist(), { mode: 384 });
4670
+ const target = shellQuote(SERVICE_TARGET);
4671
+ const plistDest = shellQuote(PATHS.launchDaemonPlist);
4672
+ const stage = shellQuote(stagePath);
4673
+ const pattern = shellQuote(MAIN_INSTANCE_PATTERN);
4674
+ const script = [
4675
+ "#!/bin/bash",
4676
+ `launchctl bootout ${target} 2>/dev/null || true`,
4677
+ `pkill -9 -f ${pattern} 2>/dev/null || true`,
4678
+ "sleep 0.2",
4679
+ `install -m 644 -o root -g wheel ${stage} ${plistDest} || exit 2`,
4680
+ `launchctl bootstrap system ${plistDest} || { launchctl bootout ${target} 2>/dev/null; rm -f ${plistDest}; exit 3; }`,
4681
+ "exit 0",
4682
+ ""
4683
+ ].join("\n");
4684
+ try {
4685
+ runSudoScript(script, {
4686
+ action: "\u542F\u7528\u4FDD\u6D3B",
4687
+ file: "daemon-enable.sh",
4688
+ codeMessages: {
4689
+ 2: "\u5B89\u88C5 plist \u5230 /Library/LaunchDaemons \u5931\u8D25",
4690
+ 3: "\u88C5\u8F7D\u4FDD\u6D3B\u670D\u52A1\u5931\u8D25\uFF08launchctl bootstrap\uFF09"
4691
+ }
4692
+ });
4693
+ } finally {
4377
4694
  try {
4378
- fs6.unlinkSync(PATHS.launchAgentPlist);
4695
+ fs6.unlinkSync(stagePath);
4379
4696
  } catch {
4380
4697
  }
4381
- throw launchctlError("bootstrap", result);
4382
4698
  }
4383
4699
  }
4384
4700
  function disableDaemon() {
4385
- const uid = process.getuid?.();
4386
- if (uid !== void 0) {
4387
- launchctl(["bootout", serviceTarget(uid)]);
4701
+ if (!isDaemonEnabled()) return;
4702
+ const target = shellQuote(SERVICE_TARGET);
4703
+ const plistDest = shellQuote(PATHS.launchDaemonPlist);
4704
+ const logFile = shellQuote(PATHS.logFile);
4705
+ const dataDir = shellQuote(DIRS.data);
4706
+ const script = [
4707
+ "#!/bin/bash",
4708
+ `launchctl bootout ${target} 2>/dev/null || true`,
4709
+ `rm -f ${plistDest}`,
4710
+ `chown "$SUDO_UID:$SUDO_GID" ${logFile} 2>/dev/null || true`,
4711
+ `chown -R "$SUDO_UID:$SUDO_GID" ${dataDir} 2>/dev/null || true`,
4712
+ "exit 0",
4713
+ ""
4714
+ ].join("\n");
4715
+ runSudoScript(script, { action: "\u5173\u95ED\u4FDD\u6D3B", file: "daemon-disable.sh" });
4716
+ const rootPids = getMihomoPids().filter(isProcessRoot);
4717
+ if (rootPids.length > 0) {
4718
+ console.log("");
4719
+ console.log(`\u4ECD\u6709 root \u5185\u6838\u8FDB\u7A0B\u6B8B\u7559 (PID ${rootPids.join(", ")})`);
4720
+ console.log("\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo");
4388
4721
  }
4722
+ }
4723
+ async function tryHotReload() {
4724
+ const controller = new AbortController();
4725
+ const timer = setTimeout(() => controller.abort(), HOT_RELOAD_TIMEOUT_MS);
4389
4726
  try {
4390
- fs6.unlinkSync(PATHS.launchAgentPlist);
4727
+ const res = await fetch(`${CONTROLLER_BASE_URL}/configs?force=true`, {
4728
+ method: "PUT",
4729
+ headers: { "Content-Type": "application/json" },
4730
+ body: "{}",
4731
+ signal: controller.signal
4732
+ });
4733
+ return res.status === 204 || res.ok;
4391
4734
  } catch {
4735
+ return false;
4736
+ } finally {
4737
+ clearTimeout(timer);
4392
4738
  }
4393
4739
  }
4394
- function restartDaemon() {
4395
- const uid = getUid();
4396
- if (!fs6.existsSync(PATHS.launchAgentPlist)) {
4740
+ async function restartDaemon() {
4741
+ if (!fs6.existsSync(PATHS.launchDaemonPlist)) {
4397
4742
  throw new Error("\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u6CD5\u91CD\u542F");
4398
4743
  }
4399
- if (launchctl(["print", serviceTarget(uid)]).status === 0) {
4400
- const result2 = launchctl(["kickstart", "-k", serviceTarget(uid)]);
4401
- if (result2.status !== 0) {
4402
- throw launchctlError("kickstart", result2);
4403
- }
4404
- return;
4405
- }
4406
- const result = launchctl(["bootstrap", guiDomain(uid), PATHS.launchAgentPlist]);
4407
- if (result.status !== 0) {
4408
- throw launchctlError("bootstrap", result);
4409
- }
4744
+ if (!logOversized() && await tryHotReload()) return;
4745
+ const target = shellQuote(SERVICE_TARGET);
4746
+ const plistDest = shellQuote(PATHS.launchDaemonPlist);
4747
+ const logFile = shellQuote(PATHS.logFile);
4748
+ const archiveFile = shellQuote(path5.join(DIRS.logs, `mihomo.${formatLocalTimestamp()}.log`));
4749
+ const script = [
4750
+ "#!/bin/bash",
4751
+ `if [ -f ${logFile} ] && [ "$(stat -f%z ${logFile} 2>/dev/null || echo 0)" -gt ${LOG_ROTATE_MAX_BYTES} ]; then`,
4752
+ ` cp ${logFile} ${archiveFile} 2>/dev/null && : > ${logFile}`,
4753
+ "fi",
4754
+ `if launchctl kickstart -k ${target} 2>/dev/null; then exit 0; fi`,
4755
+ `launchctl bootstrap system ${plistDest} || exit 3`,
4756
+ "exit 0",
4757
+ ""
4758
+ ].join("\n");
4759
+ runSudoScript(script, {
4760
+ action: "\u91CD\u542F\u4FDD\u6D3B",
4761
+ file: "daemon-restart.sh",
4762
+ codeMessages: { 3: "\u91CD\u542F\u4FDD\u6D3B\u5931\u8D25\uFF08launchctl bootstrap\uFF09" }
4763
+ });
4764
+ cleanupOldLogs();
4410
4765
  }
4411
4766
 
4412
4767
  // src/subscription.ts
4413
4768
  function isGithubUrl(url) {
4414
- return /github\.com|raw\.githubusercontent\.com/i.test(url);
4769
+ const githubRe = /github\.com|raw\.githubusercontent\.com/i;
4770
+ if (isMultiUrl(url)) {
4771
+ return splitUrls(url).every((u) => githubRe.test(u));
4772
+ }
4773
+ return githubRe.test(url);
4415
4774
  }
4416
4775
  function getDefaultUpdateInterval(url) {
4417
4776
  return isGithubUrl(url) ? DEFAULT_UPDATE_INTERVAL_HOURS_GITHUB : DEFAULT_UPDATE_INTERVAL_HOURS;
@@ -4419,11 +4778,18 @@ function getDefaultUpdateInterval(url) {
4419
4778
  function resolveUpdateInterval(url, cachedInterval) {
4420
4779
  return cachedInterval && cachedInterval > 0 ? cachedInterval : getDefaultUpdateInterval(url);
4421
4780
  }
4422
- var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, schema: CORE_SCHEMA };
4423
4781
  var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4424
4782
  function isMultiUrl(url) {
4425
4783
  return url.includes(",");
4426
4784
  }
4785
+ function isValidHttpUrl(url) {
4786
+ try {
4787
+ const u = new URL(url.trim());
4788
+ return u.protocol === "http:" || u.protocol === "https:";
4789
+ } catch {
4790
+ return false;
4791
+ }
4792
+ }
4427
4793
  function splitUrls(url) {
4428
4794
  return url.split(",").map((u) => u.trim()).filter(Boolean);
4429
4795
  }
@@ -4443,7 +4809,7 @@ function saveSubscriptionConfig(subName, parsed) {
4443
4809
  normalizeProxyNamesBeforeSave(parsed);
4444
4810
  parsed.raw.proxies = parsed.proxies;
4445
4811
  parsed.raw["proxy-groups"] = parsed.proxyGroups;
4446
- saveSubscriptionRawConfig(subName, dump(parsed.raw, YAML_DUMP_OPTS));
4812
+ saveSubscriptionRawConfig(subName, dumpYaml(parsed.raw));
4447
4813
  }
4448
4814
  function parseUserInfo(header) {
4449
4815
  if (!header) return null;
@@ -4539,7 +4905,7 @@ function pickSingleSubscription(subs, pattern) {
4539
4905
  for (const s of subs) console.log(` ${s.name}`);
4540
4906
  process.exit(1);
4541
4907
  }
4542
- async function downloadSubscription(url, subName = "default", signal) {
4908
+ async function downloadSubscription(url, subName = "default", signal, persist = true) {
4543
4909
  let response;
4544
4910
  try {
4545
4911
  response = await HTTP_CLIENT.get(url, { responseType: "text", signal });
@@ -4560,9 +4926,13 @@ async function downloadSubscription(url, subName = "default", signal) {
4560
4926
  }
4561
4927
  const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
4562
4928
  if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4563
- saveSubscriptionRawConfig(subName, content);
4929
+ if (persist) {
4930
+ saveSubscriptionRawConfig(subName, content);
4931
+ }
4564
4932
  const meta = extractSubscriptionMeta(response.headers);
4565
- saveSubscriptionMeta(subName, meta);
4933
+ if (persist) {
4934
+ saveSubscriptionMeta(subName, meta);
4935
+ }
4566
4936
  const proxies = parsed.proxies;
4567
4937
  const proxyGroups = parsed["proxy-groups"];
4568
4938
  return {
@@ -4574,13 +4944,16 @@ async function downloadSubscription(url, subName = "default", signal) {
4574
4944
  username: meta.username
4575
4945
  };
4576
4946
  }
4577
- async function downloadMergedSubscription(urls, subName, signal) {
4947
+ async function downloadMergedSubscription(urls, subName, signal, persist = true) {
4948
+ const internal = new AbortController();
4949
+ const combinedSignal = signal ? AbortSignal.any([signal, internal.signal]) : internal.signal;
4578
4950
  const responses = await Promise.all(
4579
4951
  urls.map(async (url, index) => {
4580
4952
  try {
4581
- const response = await HTTP_CLIENT.get(url, { responseType: "text", signal });
4953
+ const response = await HTTP_CLIENT.get(url, { responseType: "text", signal: combinedSignal });
4582
4954
  return { url, index, response, error: null };
4583
4955
  } catch (e) {
4956
+ internal.abort();
4584
4957
  return { url, index, response: null, error: e };
4585
4958
  }
4586
4959
  })
@@ -4610,10 +4983,14 @@ async function downloadMergedSubscription(urls, subName, signal) {
4610
4983
  }
4611
4984
  }
4612
4985
  base.proxies = baseProxies;
4613
- const mergedContent = dump(base, YAML_DUMP_OPTS);
4614
- saveSubscriptionRawConfig(subName, mergedContent);
4986
+ const mergedContent = dumpYaml(base);
4987
+ if (persist) {
4988
+ saveSubscriptionRawConfig(subName, mergedContent);
4989
+ }
4615
4990
  const meta = extractSubscriptionMeta(responses[0].response?.headers);
4616
- saveSubscriptionMeta(subName, meta);
4991
+ if (persist) {
4992
+ saveSubscriptionMeta(subName, meta);
4993
+ }
4617
4994
  const proxyGroups = base["proxy-groups"];
4618
4995
  return {
4619
4996
  proxies: baseProxies.length,
@@ -4629,7 +5006,8 @@ function prepareConfigForStart(mode, subName = "default") {
4629
5006
  if (!rawContent) {
4630
5007
  throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
4631
5008
  }
4632
- const buildResult = buildConfig(rawContent, mode);
5009
+ const subUrl = getSubscriptions().find((s) => s.name === subName)?.url;
5010
+ const buildResult = buildConfig(rawContent, mode, { subName, subUrl });
4633
5011
  if (buildResult.warnings.length > 0) {
4634
5012
  for (const warning of buildResult.warnings) {
4635
5013
  console.log(`${colors.yellow("\u81EA\u52A8\u4FEE\u590D:")} ${warning}`);
@@ -4688,16 +5066,23 @@ async function autoUpdateStaleSubscription(options = {}) {
4688
5066
  }
4689
5067
  const timeoutMs = options.timeout ?? DEFAULT_AUTO_UPDATE_TIMEOUT;
4690
5068
  const controller = new AbortController();
4691
- let results;
5069
+ const results = [];
5070
+ const updatePromise = Promise.all(
5071
+ staleSubs.map(
5072
+ (sub) => tryUpdateOne(sub, controller.signal).then((r) => {
5073
+ results.push(r);
5074
+ return r;
5075
+ })
5076
+ )
5077
+ );
4692
5078
  try {
4693
- results = await withTimeout(Promise.all(staleSubs.map((sub) => tryUpdateOne(sub, controller.signal))), timeoutMs);
5079
+ await withTimeout(updatePromise, timeoutMs);
4694
5080
  } catch (e) {
4695
- if (e instanceof TimeoutError) {
4696
- controller.abort();
4697
- console.log(colors.yellow(`\u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 (${timeoutMs / 1e3}s)\uFF0C\u8DF3\u8FC7\u66F4\u65B0\uFF0C\u4F7F\u7528\u7F13\u5B58\u914D\u7F6E`));
4698
- return { total: staleSubs.length, updated: 0, failed: staleSubs.length };
4699
- }
4700
- throw e;
5081
+ if (!(e instanceof TimeoutError)) throw e;
5082
+ controller.abort();
5083
+ await updatePromise.catch(() => {
5084
+ });
5085
+ console.log(colors.yellow(`\u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 (${timeoutMs / 1e3}s)\uFF0C\u5DF2\u5B8C\u6210\u7684\u66F4\u65B0\u751F\u6548\uFF0C\u5176\u4F59\u4F7F\u7528\u7F13\u5B58\u914D\u7F6E`));
4701
5086
  }
4702
5087
  let updatedCount = 0;
4703
5088
  for (const r of results) {
@@ -4706,8 +5091,7 @@ async function autoUpdateStaleSubscription(options = {}) {
4706
5091
  }
4707
5092
  return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
4708
5093
  }
4709
- var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
4710
- async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API_BASE) {
5094
+ async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = CONTROLLER_BASE_URL) {
4711
5095
  const encodedName = encodeURIComponent(proxyName);
4712
5096
  const url = `${apiBase}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4713
5097
  try {
@@ -4729,7 +5113,13 @@ async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API
4729
5113
  }
4730
5114
  }
4731
5115
  async function testSubscriptionProxies(subName, options = {}) {
4732
- const { timeout = DEFAULT_TEST_TIMEOUT, concurrency = DEFAULT_TEST_CONCURRENCY, testUrl = DEFAULT_TEST_URL, apiBase = API_BASE, onResult } = options;
5116
+ const {
5117
+ timeout = DEFAULT_TEST_TIMEOUT,
5118
+ concurrency = DEFAULT_TEST_CONCURRENCY,
5119
+ testUrl = DEFAULT_TEST_URL,
5120
+ apiBase = CONTROLLER_BASE_URL,
5121
+ onResult
5122
+ } = options;
4733
5123
  const { proxies } = options.parsed || loadSubscriptionConfig(subName);
4734
5124
  if (proxies.length === 0) {
4735
5125
  return { total: 0, alive: 0, dead: 0, results: [] };
@@ -4775,6 +5165,19 @@ function normalizeProxyNamesBeforeSave(parsed) {
4775
5165
  group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
4776
5166
  }
4777
5167
  }
5168
+ const rules = parsed.raw.rules;
5169
+ if (Array.isArray(rules)) {
5170
+ parsed.raw.rules = rules.map((rule) => {
5171
+ if (typeof rule !== "string") return rule;
5172
+ const parts = rule.split(",");
5173
+ if (parts.length < 2) return rule;
5174
+ const targetIdx = parts[parts.length - 1].trim().toLowerCase() === "no-resolve" && parts.length >= 3 ? parts.length - 2 : parts.length - 1;
5175
+ const target = parts[targetIdx].trim();
5176
+ const renamed = renameMap.get(target);
5177
+ if (renamed) parts[targetIdx] = renamed;
5178
+ return parts.join(",");
5179
+ });
5180
+ }
4778
5181
  return renameMap.size;
4779
5182
  }
4780
5183
  function cleanDeadProxies(parsed, deadNames) {
@@ -4791,7 +5194,8 @@ function cleanDeadProxies(parsed, deadNames) {
4791
5194
  if (group.proxies.length < before) {
4792
5195
  updatedGroups++;
4793
5196
  }
4794
- if (group.proxies.length === 0) {
5197
+ const hasOtherSource = group.use || group["include-all"] || group["include-all-proxies"];
5198
+ if (group.proxies.length === 0 && !hasOtherSource) {
4795
5199
  removedGroupNames.add(group.name);
4796
5200
  }
4797
5201
  }
@@ -4803,13 +5207,14 @@ function cleanDeadProxies(parsed, deadNames) {
4803
5207
  group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
4804
5208
  }
4805
5209
  }
5210
+ }
5211
+ const removedTargets = /* @__PURE__ */ new Set([...removedGroupNames, ...deadNames]);
5212
+ if (removedTargets.size > 0) {
4806
5213
  const rules = parsed.raw.rules;
4807
5214
  if (Array.isArray(rules)) {
4808
5215
  parsed.raw.rules = rules.filter((rule) => {
4809
5216
  if (typeof rule !== "string") return true;
4810
- const parts = rule.split(",");
4811
- if (parts.length < 2) return true;
4812
- return !removedGroupNames.has(parts[parts.length - 1].trim());
5217
+ return !removedTargets.has(getRuleTarget(rule));
4813
5218
  });
4814
5219
  }
4815
5220
  }
@@ -4904,6 +5309,8 @@ async function cmdDaemon(args) {
4904
5309
  console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
4905
5310
  process.exit(1);
4906
5311
  }
5312
+ console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u5B89\u88C5\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1\uFF08LaunchDaemon\uFF09"));
5313
+ console.log(colors.gray("\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u9700\u8981 root\uFF0C\u4EE5\u89E3\u51B3\u5C40\u57DF\u7F51\u8BBF\u95EE\u53D7\u9650\u95EE\u9898"));
4907
5314
  try {
4908
5315
  enableDaemon();
4909
5316
  } catch (e) {
@@ -4913,6 +5320,7 @@ async function cmdDaemon(args) {
4913
5320
  console.log(`${colors.green("\u5DF2\u542F\u7528\u4FDD\u6D3B")} \xB7 ${sub.name} \xB7 ${formatProxySummary(configInfo)}`);
4914
5321
  console.log(colors.gray("\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF0C\u4EE3\u7406\u5C06\u5728\u540E\u53F0\u5E38\u9A7B"));
4915
5322
  console.log("");
5323
+ await sleep(DAEMON_BOOT_WAIT_MS);
4916
5324
  printDaemonStatus();
4917
5325
  return;
4918
5326
  }
@@ -4923,6 +5331,7 @@ async function cmdDaemon(args) {
4923
5331
  printDaemonStatus();
4924
5332
  return;
4925
5333
  }
5334
+ console.log(colors.gray("\u5C06\u8BF7\u6C42\u7BA1\u7406\u5458\u6743\u9650\u4EE5\u79FB\u9664\u7CFB\u7EDF\u7EA7\u4FDD\u6D3B\u670D\u52A1"));
4926
5335
  try {
4927
5336
  disableDaemon();
4928
5337
  } catch (e) {
@@ -5009,98 +5418,10 @@ function cmdDirectory(args) {
5009
5418
  console.log("");
5010
5419
  }
5011
5420
 
5012
- // src/commands/help.ts
5013
- function printShortHelp() {
5014
- console.log(`
5015
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
5016
- `);
5017
- console.log(
5018
- `\u5E38\u7528\u547D\u4EE4:
5019
- ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
5020
- ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
5021
- ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
5022
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
5023
- `
5024
- );
5025
- }
5026
- function printHelp() {
5027
- console.log(
5028
- `
5029
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}
5030
-
5031
- \u547D\u4EE4\u522B\u540D: mihomo, mhm, mh
5032
-
5033
- \u7528\u6CD5:
5034
- mihomo <\u547D\u4EE4> [\u9009\u9879]
5035
-
5036
- ${colors.cyan("\u63A7\u5236:")}
5037
- ${colors.bold("start")} [tun|mixed] [-s] [-u ms] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)
5038
- [-r N] [-t ms] [-j N]
5039
- ${colors.bold("stop")} \u505C\u6B62\u4EE3\u7406
5040
- ${colors.bold("status")} \u67E5\u770B\u72B6\u6001
5041
-
5042
- ${colors.cyan("\u754C\u9762:")}
5043
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)
5044
- ${colors.bold("log")} [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09
5045
- ${colors.bold("logs")} [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09
5046
-
5047
- ${colors.cyan("\u8BA2\u9605:")}
5048
- ${colors.bold("subscription")} \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09
5049
- ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
5050
- ${colors.bold("subscription")} add <url> [name] \u6DFB\u52A0\u8BA2\u9605
5051
- ${colors.bold("subscription")} update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09
5052
- ${colors.bold("subscription")} remove <name> \u5220\u9664\u8BA2\u9605
5053
- ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
5054
- ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
5055
- ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
5056
- ${colors.bold("test")} [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027
5057
- ${colors.bold("clean")} [-t ms] [-j N] [-r N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F
5058
-
5059
- ${colors.cyan("\u914D\u7F6E:")}
5060
- ${colors.bold("overwrite")} \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09
5061
- ${colors.bold("overwrite")} on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E
5062
- ${colors.bold("directory")} \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09
5063
- ${colors.bold("directory")} open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|...
5064
-
5065
- ${colors.cyan("\u7CFB\u7EDF:")}
5066
- ${colors.bold("kernel")} [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09
5067
- ${colors.bold("daemon")} on|off \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF08\u4EC5 Mixed\uFF09
5068
- ${colors.bold("daemon")} status \u67E5\u770B\u4FDD\u6D3B\u72B6\u6001
5069
- ${colors.bold("update")} \u66F4\u65B0 mihomo-cli (npm install -g)
5070
- ${colors.bold("reset")} [\u76EE\u6807...] [--full] \u91CD\u7F6E: \u7559\u7A7A\u4FDD\u7559\u8BBE\u7F6E/\u5185\u6838/\u8986\u5199, \u6307\u5B9A\u76EE\u6807\u5220\u5BF9\u5E94\u9879, --full \u5220\u5168\u90E8
5071
- ${colors.bold("help")}, -h \u663E\u793A\u5E2E\u52A9
5072
- ${colors.bold("version")}, -v \u663E\u793A\u7248\u672C
5073
-
5074
- ${colors.cyan("\u793A\u4F8B:")}
5075
- mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F
5076
- mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F
5077
- mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605
5078
- mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)
5079
- mihomo daemon on # \u5F00\u542F\u4FDD\u6D3B\uFF08\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF09
5080
- mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)
5081
- mihomo ui # \u6253\u5F00 Web UI
5082
-
5083
- ${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}
5084
- mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)
5085
- tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo
5086
-
5087
- ${colors.cyan("\u6570\u636E\u76EE\u5F55:")}
5088
- \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E
5089
- \u9ED8\u8BA4: ${USER_DATA_DIR}
5090
- `
5091
- );
5092
- }
5093
- function printVersion() {
5094
- const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
5095
- console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
5096
- console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
5097
- console.log(`${colors.gray("\u6570\u636E\u76EE\u5F55: ")}${USER_DATA_DIR}`);
5098
- }
5099
-
5100
5421
  // src/kernel.ts
5101
5422
  import { spawnSync as spawnSync5 } from "child_process";
5102
5423
  import fs7 from "fs";
5103
- import path5 from "path";
5424
+ import path6 from "path";
5104
5425
 
5105
5426
  // node_modules/compare-versions/lib/esm/utils.js
5106
5427
  var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
@@ -5185,7 +5506,7 @@ function findMatchingAsset(assets, platform, arch) {
5185
5506
  const nameWithoutGz = a.name.slice(0, -3);
5186
5507
  const parts = nameWithoutGz.split("-");
5187
5508
  const lastPart = parts[parts.length - 1];
5188
- return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go");
5509
+ return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go") && !nameWithoutGz.includes("-compatible");
5189
5510
  });
5190
5511
  return standardAsset || matchingAssets[0];
5191
5512
  }
@@ -5228,7 +5549,7 @@ function findBinaryInDir(dir, maxDepth = 4) {
5228
5549
  if (maxDepth <= 0) return null;
5229
5550
  const files = fs7.readdirSync(dir);
5230
5551
  for (const f of files) {
5231
- const fullPath = path5.join(dir, f);
5552
+ const fullPath = path6.join(dir, f);
5232
5553
  const stat = fs7.statSync(fullPath);
5233
5554
  if (stat.isDirectory()) {
5234
5555
  const found = findBinaryInDir(fullPath, maxDepth - 1);
@@ -5255,7 +5576,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5255
5576
  \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
5256
5577
  }
5257
5578
  const downloadUrl = withMirror(asset.browser_download_url, mirror);
5258
- const tempPath = path5.join(DIRS.kernel, asset.name);
5579
+ const tempPath = path6.join(DIRS.kernel, asset.name);
5259
5580
  const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
5260
5581
  if (mirror && progressCallback) {
5261
5582
  progressCallback("\u63D0\u793A: \u7ECF\u7B2C\u4E09\u65B9\u955C\u50CF\u4E2D\u8F6C\u4E0B\u8F7D\uFF0C\u65E0\u6CD5\u9A8C\u8BC1\u6765\u6E90\u5B8C\u6574\u6027\uFF0C\u5EFA\u8BAE\u76F4\u8FDE\u6216\u81EA\u884C\u6821\u9A8C\u4EA7\u7269");
@@ -5268,6 +5589,12 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5268
5589
  ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
5269
5590
  { stdio: "inherit" }
5270
5591
  );
5592
+ if (curlResult.error) {
5593
+ if (curlResult.error.code === "ENOENT") {
5594
+ throw new Error("\u672A\u627E\u5230 curl \u547D\u4EE4\uFF0C\u8BF7\u5148\u5B89\u88C5 curl \u540E\u91CD\u8BD5");
5595
+ }
5596
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25: ${curlResult.error.message}`);
5597
+ }
5271
5598
  if (curlResult.status !== 0) {
5272
5599
  try {
5273
5600
  fs7.unlinkSync(tempPath);
@@ -5285,12 +5612,21 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5285
5612
  let extractedBinary = null;
5286
5613
  try {
5287
5614
  if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5615
+ const listResult = spawnSync5("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
5616
+ if (listResult.error) throw listResult.error;
5617
+ if (listResult.status !== 0) throw new Error(`tar \u5217\u8868\u9000\u51FA\u7801 ${listResult.status}`);
5618
+ const entries = (listResult.stdout || "").split("\n").filter(Boolean);
5619
+ for (const entry of entries) {
5620
+ if (entry.startsWith("/") || entry.split("/").includes("..")) {
5621
+ throw new Error(`\u5F52\u6863\u542B\u975E\u6CD5\u8DEF\u5F84\u6761\u76EE: ${entry}`);
5622
+ }
5623
+ }
5288
5624
  const tarResult = spawnSync5("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
5289
5625
  if (tarResult.error) throw tarResult.error;
5290
5626
  if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
5291
5627
  } else if (tempPath.endsWith(".gz")) {
5292
- const baseName = path5.basename(tempPath, ".gz");
5293
- const outputPath = path5.join(extractPath, baseName);
5628
+ const baseName = path6.basename(tempPath, ".gz");
5629
+ const outputPath = path6.join(extractPath, baseName);
5294
5630
  const gzipResult = spawnSync5("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
5295
5631
  if (gzipResult.error) throw gzipResult.error;
5296
5632
  if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
@@ -5357,20 +5693,8 @@ async function cmdKernel(args) {
5357
5693
  if (effectiveMirror) {
5358
5694
  const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
5359
5695
  console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
5696
+ console.log("");
5360
5697
  }
5361
- console.log("\n\u63D0\u793A: \u5982\u679C\u4E0B\u8F7D\u901F\u5EA6\u8FC7\u6162\u6216\u76F4\u8FDE\u5931\u8D25\uFF0C\u53EF\u4F7F\u7528 --mirror \u53C2\u6570\u901A\u8FC7\u955C\u50CF\u4E0B\u8F7D");
5362
- console.log("\n\u7528\u6CD5:");
5363
- console.log(" mihomo kernel # \u76F4\u8FDE");
5364
- console.log(" mihomo kernel --mirror # \u4E0B\u8F7D\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF (v6.gh-proxy.org)");
5365
- console.log(" mihomo kernel --mirror hk.gh-proxy.org # \u4E0B\u8F7D\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
5366
- console.log(" mihomo kernel --mirror-all # API\u8BF7\u6C42\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF");
5367
- console.log(" mihomo kernel --mirror-all hk.gh-proxy.org # API\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
5368
- console.log("\n\u53EF\u7528\u955C\u50CF:");
5369
- for (const m of AVAILABLE_MIRRORS) {
5370
- const isCurrent = effectiveMirror && (effectiveMirror.includes(`//${m}/`) || effectiveMirror.includes(`//${m}:`) || effectiveMirror.endsWith(`//${m}`));
5371
- console.log(` ${m}${isCurrent ? " (\u5F53\u524D)" : ""}`);
5372
- }
5373
- console.log("");
5374
5698
  console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
5375
5699
  try {
5376
5700
  const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
@@ -5397,6 +5721,13 @@ async function cmdKernel(args) {
5397
5721
  console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
5398
5722
  }
5399
5723
  }
5724
+ if (!effectiveMirror) {
5725
+ console.error("");
5726
+ console.error("\u63D0\u793A: \u76F4\u8FDE\u5931\u8D25\u6216\u4E0B\u8F7D\u8FC7\u6162\u65F6\u53EF\u4F7F\u7528\u955C\u50CF:");
5727
+ console.error(" mihomo kernel --mirror [\u955C\u50CF] # \u4E0B\u8F7D\u8D70\u955C\u50CF\uFF08\u9ED8\u8BA4 v6.gh-proxy.org\uFF09");
5728
+ console.error(" mihomo kernel --mirror-all [\u955C\u50CF] # API \u548C\u4E0B\u8F7D\u90FD\u8D70\u955C\u50CF");
5729
+ console.error(` \u53EF\u7528\u955C\u50CF: ${AVAILABLE_MIRRORS.join(", ")}`);
5730
+ }
5400
5731
  process.exit(1);
5401
5732
  }
5402
5733
  }
@@ -5486,19 +5817,116 @@ function cmdLogs(args) {
5486
5817
  // src/commands/overwrite.ts
5487
5818
  import path7 from "path";
5488
5819
 
5820
+ // src/runtime.ts
5821
+ function getRuntimeMode() {
5822
+ if (isDaemonEnabled()) return "mixed";
5823
+ return getConfigInfo()?.tun ? "tun" : "mixed";
5824
+ }
5825
+ function getRunningState() {
5826
+ if (isDaemonEnabled()) {
5827
+ const daemon = getDaemonStatus();
5828
+ return { running: isDaemonRunning(daemon), pid: daemon.pid, daemon: true };
5829
+ }
5830
+ const status = getStatus();
5831
+ return { running: status.running, pid: status.pid, daemon: false };
5832
+ }
5833
+ function isRestartNeededOnChange() {
5834
+ return isDaemonEnabled() || getStatus().running;
5835
+ }
5836
+ async function launchOrRestart(mode) {
5837
+ if (isDaemonEnabled()) {
5838
+ await restartDaemon();
5839
+ await sleep(DAEMON_BOOT_WAIT_MS);
5840
+ return getDaemonStatus().pid;
5841
+ }
5842
+ const result = await start(mode);
5843
+ return result.pid;
5844
+ }
5845
+
5846
+ // src/progress.ts
5847
+ var IS_TTY = process.stdout.isTTY === true;
5848
+ var BAR_WIDTH = 20;
5849
+ function createProgressPrinter(totalRounds = 1) {
5850
+ let alive = 0;
5851
+ let dead = 0;
5852
+ const resultMap = /* @__PURE__ */ new Map();
5853
+ function render(done, total) {
5854
+ if (!IS_TTY) return;
5855
+ const pct = Math.round(done / total * 100);
5856
+ const filled = Math.round(done / total * BAR_WIDTH);
5857
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
5858
+ process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
5859
+ }
5860
+ return {
5861
+ onResult(result, index, total, round = 1) {
5862
+ if (resultMap.size === 0 && totalRounds > 1) {
5863
+ console.log(`--- \u7B2C 1 \u8F6E\u6D4B\u8BD5 (${total} \u4E2A\u8282\u70B9) ---`);
5864
+ }
5865
+ const prev = resultMap.get(result.name);
5866
+ if (prev) {
5867
+ if (prev.result.delay !== null) alive--;
5868
+ else dead--;
5869
+ }
5870
+ if (result.delay !== null) alive++;
5871
+ else dead++;
5872
+ resultMap.set(result.name, { result, round });
5873
+ render(index + 1, total);
5874
+ },
5875
+ onRetryRound(round, count) {
5876
+ if (IS_TTY) {
5877
+ process.stdout.write("\n");
5878
+ }
5879
+ console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
5880
+ alive = 0;
5881
+ dead = 0;
5882
+ },
5883
+ finish() {
5884
+ if (IS_TTY) {
5885
+ process.stdout.write("\n");
5886
+ }
5887
+ console.log("");
5888
+ if (!IS_TTY) return;
5889
+ const entries = [...resultMap.values()];
5890
+ entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
5891
+ const total = entries.length;
5892
+ console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
5893
+ for (let i = 0; i < entries.length; i++) {
5894
+ const { result, round } = entries[i];
5895
+ const prefix = `[${i + 1}/${total}]`;
5896
+ if (result.delay !== null) {
5897
+ const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
5898
+ const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
5899
+ console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
5900
+ } else {
5901
+ console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
5902
+ }
5903
+ }
5904
+ console.log("");
5905
+ }
5906
+ };
5907
+ }
5908
+ function formatCleanSummary(result) {
5909
+ const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
5910
+ if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
5911
+ if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
5912
+ return parts.join(", ");
5913
+ }
5914
+ function formatTestSummary(summary) {
5915
+ return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
5916
+ }
5917
+
5489
5918
  // src/commands/status.ts
5490
5919
  function printStatus() {
5491
5920
  const status = getStatus();
5492
- const daemon = getDaemonStatus();
5921
+ const state = getRunningState();
5493
5922
  const info = getConfigInfo();
5494
5923
  const overwriteEnabled = isOverwriteEnabled();
5495
5924
  const overwriteFiles = listOverwriteFile().files;
5496
5925
  const activeSub = getActiveSubscription();
5497
- const running = daemon.enabled ? isDaemonRunning(daemon) : status.running;
5498
- const pid = daemon.enabled ? daemon.pid : status.pid;
5926
+ const { running, pid, daemon: daemonManaged } = state;
5499
5927
  console.log("");
5500
5928
  let modeLabel = "";
5501
- if (info && running) {
5929
+ if (info) {
5502
5930
  modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
5503
5931
  }
5504
5932
  const statusText = running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
@@ -5506,12 +5934,15 @@ function printStatus() {
5506
5934
  console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
5507
5935
  if (pid) {
5508
5936
  console.log(`${colors.gray("PID: ")}${pid}`);
5509
- if (!daemon.enabled && status.processInfo) {
5937
+ if (!daemonManaged && status.processInfo) {
5510
5938
  console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
5511
5939
  }
5512
5940
  }
5513
5941
  if (info) {
5514
- if (info.mixedPort) {
5942
+ if (info.tun) {
5943
+ const extra = info.mixedPort ? `\uFF0C\u53E6\u76D1\u542C ${info.mixedPort}` : "";
5944
+ console.log(`${colors.gray("\u7AEF\u53E3: ")}TUN \u63A5\u7BA1${extra}`);
5945
+ } else if (info.mixedPort) {
5515
5946
  console.log(`${colors.gray("\u7AEF\u53E3: ")}${info.mixedPort}`);
5516
5947
  } else {
5517
5948
  const ports = [];
@@ -5537,7 +5968,7 @@ function printStatus() {
5537
5968
  } else {
5538
5969
  console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
5539
5970
  }
5540
- if (daemon.enabled) {
5971
+ if (isDaemonEnabled()) {
5541
5972
  console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
5542
5973
  }
5543
5974
  console.log("");
@@ -5557,7 +5988,7 @@ async function cmdStop() {
5557
5988
  console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
5558
5989
  return;
5559
5990
  }
5560
- const pids = getAllMihomoPids();
5991
+ const pids = getMihomoPids();
5561
5992
  if (pids.length === 0) {
5562
5993
  console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
5563
5994
  return;
@@ -5567,915 +5998,859 @@ async function cmdStop() {
5567
5998
  console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
5568
5999
  }
5569
6000
 
5570
- // src/test-instance.ts
5571
- import { spawn as spawn2 } from "child_process";
5572
- import fs8 from "fs";
5573
- import path6 from "path";
5574
-
5575
- // src/lifecycle.ts
5576
- var cleanupFns = /* @__PURE__ */ new Set();
5577
- function registerCleanup(fn) {
5578
- cleanupFns.add(fn);
5579
- return () => {
5580
- cleanupFns.delete(fn);
5581
- };
5582
- }
5583
- function runCleanup() {
5584
- for (const fn of cleanupFns) {
5585
- try {
5586
- fn();
5587
- } catch {
5588
- }
6001
+ // src/commands/start.ts
6002
+ async function cmdStart(args) {
6003
+ if (!hasKernel()) {
6004
+ console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
6005
+ process.exit(1);
5589
6006
  }
5590
- cleanupFns.clear();
5591
- }
5592
-
5593
- // src/test-instance.ts
5594
- var TEST_DIR = path6.join(USER_DATA_DIR, "test");
5595
- var TEST_DIRS = {
5596
- data: path6.join(TEST_DIR, "data"),
5597
- runtime: path6.join(TEST_DIR, "runtime")
5598
- };
5599
- var TEST_PATHS = {
5600
- configFile: path6.join(TEST_DIRS.runtime, "config.yaml"),
5601
- pidFile: path6.join(TEST_DIRS.runtime, "pid"),
5602
- logFile: path6.join(TEST_DIR, "test.log")
5603
- };
5604
- var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
5605
- function ensureTestDirs() {
5606
- for (const dir of Object.values(TEST_DIRS)) {
5607
- fs8.mkdirSync(dir, { recursive: true, mode: 448 });
6007
+ const targetMode = args[1] === "tun" ? "tun" : "mixed";
6008
+ const daemonEnabled = isDaemonEnabled();
6009
+ if (targetMode === "tun" && daemonEnabled) {
6010
+ 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`);
6011
+ console.error("\u8BF7\u5148\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
6012
+ process.exit(1);
5608
6013
  }
5609
- }
5610
- function cleanupTestDir() {
5611
- rmrf(TEST_DIR);
5612
- }
5613
- function buildTestConfig(subName) {
5614
- ensureTestDirs();
5615
- const rawContent = readSubscriptionRawConfig(subName);
5616
- if (!rawContent) {
5617
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
6014
+ const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6015
+ const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6016
+ const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
6017
+ const skipUpdate = hasFlag(args, "-s", "--no-update");
6018
+ const skipClean = hasFlag(args, "--no-clean");
6019
+ const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
6020
+ const sub = getActiveSubscription();
6021
+ if (!sub) {
6022
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
6023
+ process.exit(1);
5618
6024
  }
5619
- const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
5620
- const proxies = (parsed.proxies || []).filter(isProxyValid);
5621
- if (proxies.length === 0) {
5622
- throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
6025
+ if (!skipUpdate) {
6026
+ await autoUpdateStaleSubscription({ timeout: updateTimeout });
5623
6027
  }
5624
- const nameCount = /* @__PURE__ */ new Map();
5625
- for (const proxy of proxies) {
5626
- const count = (nameCount.get(proxy.name) || 0) + 1;
5627
- nameCount.set(proxy.name, count);
5628
- if (count > 1) {
5629
- proxy.name = `${proxy.name} #${count}`;
5630
- }
5631
- }
5632
- const config = {
5633
- ...TEST_CONFIG,
5634
- proxies,
5635
- "proxy-groups": [
5636
- {
5637
- name: "PROXY",
5638
- type: "select",
5639
- proxies: proxies.map((p) => p.name)
5640
- }
5641
- ],
5642
- rules: ["MATCH,PROXY"]
5643
- };
5644
- const content = dump(config, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
5645
- fs8.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
5646
- }
5647
- async function startTestInstance() {
5648
- const binary = PATHS.mihomoBinary;
5649
- if (!fs8.existsSync(binary)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
5650
- stopTestInstance();
5651
- const logFd = fs8.openSync(TEST_PATHS.logFile, "a");
5652
- const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
5653
- detached: true,
5654
- stdio: ["ignore", logFd, logFd]
5655
- });
5656
- fs8.closeSync(logFd);
5657
- child.unref();
5658
- const pid = child.pid;
5659
- fs8.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
5660
- const client = createHttpClient({ timeout: 2e3 });
5661
- let ready = false;
5662
- for (let i = 0; i < 60; i++) {
5663
- if (!isProcessRunning(pid)) break;
5664
- try {
5665
- await client.get(`${TEST_API}/version`);
5666
- ready = true;
5667
- break;
5668
- } catch {
5669
- await sleep(500);
6028
+ if (!daemonEnabled) {
6029
+ if (hasRootResidue()) {
6030
+ console.error(`${colors.red("\u9519\u8BEF:")} \u5B58\u5728\u9700\u8981 root \u6743\u9650\u6E05\u7406\u7684\u6B8B\u7559\u8FDB\u7A0B/\u6587\u4EF6`);
6031
+ console.error(`\u8BF7\u5148\u624B\u52A8\u6E05\u7406: sudo pkill -9 mihomo && sudo rm -f ${PATHS.pidFile}`);
6032
+ console.error("\u6216\u5207\u6362\u5230 TUN \u6A21\u5F0F\u542F\u52A8\uFF08\u81EA\u52A8\u6E05\u7406\uFF09: mihomo start tun");
6033
+ process.exit(1);
5670
6034
  }
5671
- }
5672
- if (!isProcessRunning(pid)) {
5673
- let errorDetail = "";
5674
- try {
5675
- errorDetail = fs8.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
5676
- } catch {
6035
+ const status = getStatus();
6036
+ const hasProcess = status.running || status.allProcesses.length > 0;
6037
+ if (hasProcess) {
6038
+ const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
6039
+ console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
5677
6040
  }
5678
- throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
5679
- ${errorDetail}` : ""}`);
5680
- }
5681
- if (!ready) {
5682
- throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
5683
- }
5684
- }
5685
- function stopTestInstance() {
5686
- let pid;
5687
- try {
5688
- pid = parseInt(fs8.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
5689
- } catch {
5690
- return;
5691
- }
5692
- if (pid > 0 && isProcessRunning(pid)) {
5693
- process.kill(pid, "SIGKILL");
5694
- for (let i = 0; i < 20; i++) {
5695
- if (!isProcessRunning(pid)) break;
5696
- sleepSync(100);
6041
+ handleStopResult(stop());
6042
+ if (hasProcess) {
6043
+ console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
6044
+ `);
5697
6045
  }
5698
6046
  }
6047
+ let configInfo;
5699
6048
  try {
5700
- fs8.unlinkSync(TEST_PATHS.pidFile);
5701
- } catch {
6049
+ configInfo = prepareConfigForStart(targetMode, sub.name);
6050
+ } catch (e) {
6051
+ console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
6052
+ process.exit(1);
5702
6053
  }
5703
- }
5704
- async function withTestInstance(subName, fn) {
5705
- cleanupTestDir();
5706
- buildTestConfig(subName);
5707
- const unregister = registerCleanup(() => {
5708
- stopTestInstance();
5709
- cleanupTestDir();
5710
- });
6054
+ const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
6055
+ console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
5711
6056
  try {
5712
- await startTestInstance();
5713
- return await fn(TEST_API);
5714
- } finally {
5715
- unregister();
5716
- stopTestInstance();
5717
- cleanupTestDir();
5718
- }
5719
- }
5720
-
5721
- // src/commands/subscription.ts
5722
- var IS_TTY = process.stdout.isTTY === true;
5723
- var BAR_WIDTH = 20;
5724
- function createProgressPrinter(totalRounds = 1) {
5725
- let alive = 0;
5726
- let dead = 0;
5727
- const resultMap = /* @__PURE__ */ new Map();
5728
- function render(done, total) {
5729
- if (!IS_TTY) return;
5730
- const pct = Math.round(done / total * 100);
5731
- const filled = Math.round(done / total * BAR_WIDTH);
5732
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
5733
- process.stdout.write(`\r${bar} ${done}/${total} (${pct}%) | ${colors.green(`\u2713${alive}`)} ${colors.red(`\u2717${dead}`)}`);
6057
+ const pid = await launchOrRestart(targetMode);
6058
+ const label = daemonEnabled ? "\u5DF2\u542F\u52A8 (\u4FDD\u6D3B)" : "\u5DF2\u542F\u52A8";
6059
+ console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""}`);
6060
+ } catch (e) {
6061
+ const msg = e.message;
6062
+ const lines = msg.split("\n");
6063
+ console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${lines[0]}`);
6064
+ if (lines.length > 1) {
6065
+ for (const line of lines.slice(1)) console.error(line);
6066
+ }
6067
+ process.exit(1);
5734
6068
  }
5735
- return {
5736
- onResult(result, index, total, round = 1) {
5737
- if (resultMap.size === 0 && totalRounds > 1) {
5738
- console.log(`--- \u7B2C 1 \u8F6E\u6D4B\u8BD5 (${total} \u4E2A\u8282\u70B9) ---`);
5739
- }
5740
- if (result.delay !== null) alive++;
5741
- else dead++;
5742
- resultMap.set(result.name, { result, round });
5743
- render(index + 1, total);
5744
- },
5745
- onRetryRound(round, count) {
5746
- if (IS_TTY) {
5747
- process.stdout.write("\n");
5748
- }
5749
- console.log(`--- \u7B2C ${round} \u8F6E\u91CD\u8BD5 (${count} \u4E2A\u8282\u70B9) ---`);
5750
- alive = 0;
5751
- dead = 0;
5752
- },
5753
- finish() {
5754
- if (IS_TTY) {
5755
- process.stdout.write("\n");
5756
- }
6069
+ const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
6070
+ if (!skipClean && configInfo.proxies > cleanThreshold) {
6071
+ const cache = readSubscriptionCache();
6072
+ const lastCleanAt = cache[sub.name]?.last_auto_clean_at;
6073
+ const withinCooldown = !!lastCleanAt && Date.now() - new Date(lastCleanAt).getTime() < AUTO_CLEAN_COOLDOWN_HOURS * 60 * 60 * 1e3;
6074
+ if (!withinCooldown) {
5757
6075
  console.log("");
5758
- const entries = [...resultMap.values()];
5759
- entries.sort((a, b) => a.result.name.localeCompare(b.result.name));
5760
- const total = entries.length;
5761
- console.log("\u8282\u70B9\u6700\u7EC8\u72B6\u6001:");
5762
- for (let i = 0; i < entries.length; i++) {
5763
- const { result, round } = entries[i];
5764
- const prefix = `[${i + 1}/${total}]`;
5765
- if (result.delay !== null) {
5766
- const delayColor = result.delay < 300 ? colors.green : result.delay < 800 ? colors.yellow : colors.red;
5767
- const retryNote = round > 1 ? colors.gray(` (\u7B2C${round}\u8F6E\u901A\u8FC7)`) : "";
5768
- console.log(`${prefix} ${colors.green("\u2713")} ${result.name} ${delayColor(`${result.delay}ms`)}${retryNote}`);
5769
- } else {
5770
- console.log(`${prefix} ${colors.red("\u2717")} ${result.name} ${colors.gray(result.error || "timeout")}`);
6076
+ 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...`);
6077
+ console.log("");
6078
+ await sleep(1e3);
6079
+ const progress = createProgressPrinter(rounds);
6080
+ const cleanResult = await autoCleanSubscription(sub.name, {
6081
+ timeout,
6082
+ concurrency,
6083
+ rounds,
6084
+ onResult: progress.onResult,
6085
+ onRetryRound: progress.onRetryRound
6086
+ });
6087
+ progress.finish();
6088
+ console.log(formatTestSummary(cleanResult.summary));
6089
+ if (cleanResult.skipped) {
6090
+ 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"));
6091
+ } else if (cleanResult.removedProxies > 0) {
6092
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
6093
+ console.log("");
6094
+ console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
6095
+ if (!daemonEnabled) handleStopResult(stop());
6096
+ try {
6097
+ configInfo = prepareConfigForStart(targetMode, sub.name);
6098
+ const pid = await launchOrRestart(targetMode);
6099
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
6100
+ } catch (e) {
6101
+ console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6102
+ process.exit(1);
5771
6103
  }
5772
6104
  }
5773
- console.log("");
6105
+ saveSubscriptionCache(sub.name, { last_auto_clean_at: (/* @__PURE__ */ new Date()).toISOString() });
5774
6106
  }
5775
- };
5776
- }
5777
- function formatCleanSummary(result) {
5778
- const parts = [`\u79FB\u9664 ${result.removedProxies} \u4E2A\u8282\u70B9`];
5779
- if (result.removedGroups > 0) parts.push(`\u5220\u9664 ${result.removedGroups} \u4E2A\u7A7A\u5206\u7EC4`);
5780
- if (result.updatedGroups > 0) parts.push(`\u66F4\u65B0 ${result.updatedGroups} \u4E2A\u5206\u7EC4`);
5781
- return parts.join(", ");
5782
- }
5783
- function formatTestSummary(summary) {
5784
- return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
5785
- }
5786
- function githubRepoUrl(rawUrl) {
5787
- const match = rawUrl.match(/raw\.githubusercontent\.com\/([^/]+\/[^/]+)/);
5788
- if (match) return `https://github.com/${match[1]}`;
5789
- return null;
5790
- }
5791
- function resolveTestTarget(args) {
5792
- const subs = getSubscriptions();
5793
- if (subs.length === 0) {
5794
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5795
- process.exit(1);
5796
- }
5797
- const nameArg = getNonFlagArg(args, 2);
5798
- const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
5799
- const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
5800
- let target;
5801
- if (nameArg) {
5802
- const matches = findSubscriptionFuzzy(subs, nameArg);
5803
- target = pickSingleSubscription(matches, nameArg);
5804
- } else {
5805
- const activeSub = getActiveSubscription();
5806
- if (!activeSub) {
5807
- console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
5808
- process.exit(1);
5809
- }
5810
- target = activeSub;
5811
6107
  }
5812
- return { target, timeout, concurrency };
6108
+ printStatus();
5813
6109
  }
5814
- async function printSubscriptionList(options) {
5815
- if (options?.autoUpdate !== false) {
5816
- const updateResult = await autoUpdateStaleSubscription();
5817
- if (updateResult.total > 0) console.log("");
5818
- }
5819
- const subs = getSubscriptionsWithCache();
5820
- if (subs.length === 0) {
5821
- console.log("\u6CA1\u6709\u8BA2\u9605");
6110
+
6111
+ // src/commands/overwrite.ts
6112
+ function printOverwriteList() {
6113
+ const info = listOverwriteFile();
6114
+ const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
6115
+ console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}`);
6116
+ console.log(`${colors.gray("\u4F4D\u7F6E: ")}${info.dir}`);
6117
+ console.log("");
6118
+ if (info.files.length === 0) {
6119
+ console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
5822
6120
  console.log("");
5823
- console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
6121
+ console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path7.join(info.dir, "overwrite.yaml")}`);
6122
+ console.log(` \u6216 ${path7.join(info.dir, "overwrite.dns.yaml")}`);
5824
6123
  console.log("");
5825
- return;
5826
- }
5827
- const activeSub = getActiveSubscription();
5828
- console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
5829
- subs.forEach((s, i) => {
5830
- const time = formatDate(s.updated_at);
5831
- const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
5832
- const mergeBadge = isMultiUrl(s.url) ? colors.cyan(` [\u5408\u5E76 ${splitUrls(s.url).length} \u6E90]`) : "";
5833
- const interval = resolveUpdateInterval(s.url, s.update_interval);
5834
- console.log(` ${i + 1}. ${s.name}${defaultMark}${mergeBadge}`);
5835
- console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
5836
- if (s.username) {
5837
- console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
5838
- }
5839
- if (s.download !== void 0 || s.total !== void 0) {
5840
- const used = (s.upload || 0) + (s.download || 0);
5841
- const usedStr = formatBytes(used);
5842
- const totalStr = formatBytes(s.total);
5843
- let percentStr = "";
5844
- if (s.total && s.total > 0) {
5845
- const percent = Math.min(used / s.total * 100, 100);
5846
- percentStr = ` (${percent.toFixed(1)}%)`;
5847
- }
5848
- console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
5849
- }
5850
- if (s.expire !== void 0) {
5851
- console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
5852
- }
5853
- if (s.web_page_url) {
5854
- console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
5855
- }
5856
- });
5857
- console.log("");
5858
- console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
5859
- console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
5860
- console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
5861
- console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
5862
- console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
5863
- console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
5864
- console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
5865
- console.log("");
5866
- }
5867
- async function cmdSubscription(args) {
5868
- const action = args[1];
5869
- if (!action || action === "list") {
5870
- await printSubscriptionList();
5871
- return;
5872
- }
5873
- if (action === "add") {
5874
- const url = args[2];
5875
- const name = args[3] || "default";
5876
- if (!url) {
5877
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
5878
- process.exit(1);
5879
- }
5880
- if (isMultiUrl(url)) {
5881
- const urls = splitUrls(url);
5882
- for (const u of urls) {
5883
- if (!u.startsWith("http")) {
5884
- console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
5885
- process.exit(1);
5886
- }
5887
- }
5888
- console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
5889
- try {
5890
- addSubscription(url, name);
5891
- setDefaultSubscription(name);
5892
- const info = await downloadMergedSubscription(urls, name);
5893
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
5894
- } catch (e) {
5895
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
5896
- process.exit(1);
5897
- }
5898
- } else {
5899
- if (!url.startsWith("http")) {
5900
- console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
5901
- process.exit(1);
6124
+ } else {
6125
+ console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
6126
+ console.log("");
6127
+ info.files.forEach((f, i) => {
6128
+ const num = i < 10 ? ` ${i}` : `${i}`;
6129
+ console.log(` ${num}. ${f.name}`);
6130
+ if (f.scope) {
6131
+ console.log(` ${colors.gray("\u4F5C\u7528\u57DF: ")}${f.scope}`);
5902
6132
  }
5903
- console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
5904
- try {
5905
- addSubscription(url, name);
5906
- setDefaultSubscription(name);
5907
- const info = await downloadSubscription(url, name);
5908
- const repoUrl = githubRepoUrl(url);
5909
- if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
5910
- console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
5911
- } catch (e) {
5912
- console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
5913
- process.exit(1);
6133
+ if (f.keys.length > 0) {
6134
+ console.log(` ${colors.gray("\u5B57\u6BB5: ")}${f.keys.join(", ")}`);
5914
6135
  }
5915
- }
6136
+ });
5916
6137
  console.log("");
5917
- await printSubscriptionList();
5918
- return;
5919
6138
  }
5920
- if (action === "update") {
5921
- const name = args[2];
5922
- const subs = getSubscriptions();
5923
- if (subs.length === 0) {
5924
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
5925
- process.exit(1);
5926
- }
5927
- if (!name) {
5928
- console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
5929
- const results = await Promise.all(subs.map((sub) => tryUpdateOne(sub)));
5930
- let ok = 0;
5931
- for (const r of results) {
5932
- if (r.success) ok++;
5933
- printUpdateResult(r);
5934
- }
5935
- if (ok === 0) process.exit(1);
6139
+ console.log("\u542F\u7528\u8986\u5199: mihomo ow on");
6140
+ console.log("\u7981\u7528\u8986\u5199: mihomo ow off");
6141
+ console.log("");
6142
+ }
6143
+ async function cmdOverwrite(args) {
6144
+ const action = args?.[1];
6145
+ const currentMode = getRuntimeMode();
6146
+ const restartNeeded = isRestartNeededOnChange();
6147
+ if (action === "on" || action === "enable") {
6148
+ if (isOverwriteEnabled()) {
6149
+ console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u542F\u7528\u72B6\u6001");
5936
6150
  console.log("");
5937
- await printSubscriptionList();
6151
+ printOverwriteList();
5938
6152
  return;
5939
6153
  }
5940
- const matches = findSubscriptionFuzzy(subs, name);
5941
- const target = pickSingleSubscription(matches, name);
5942
- console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
5943
- const result = await tryUpdateOne(target);
5944
- if (!result.success) {
5945
- console.error(`\u66F4\u65B0\u5931\u8D25: ${(result.error || "").split("\n")[0]}`);
5946
- process.exit(1);
6154
+ setOverwriteEnabled(true);
6155
+ console.log("\u5DF2\u542F\u7528\u8986\u5199\u914D\u7F6E");
6156
+ if (restartNeeded) {
6157
+ console.log("");
6158
+ await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
6159
+ return;
5947
6160
  }
5948
- console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(result)})`);
5949
6161
  console.log("");
5950
- await printSubscriptionList();
6162
+ printOverwriteList();
5951
6163
  return;
5952
6164
  }
5953
- if (action === "use") {
5954
- const name = args[2];
5955
- const subs = getSubscriptions();
5956
- if (!name) {
5957
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
5958
- if (subs.length > 0) {
5959
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
5960
- for (const s of subs) console.log(` ${s.name}`);
5961
- }
5962
- process.exit(1);
5963
- }
5964
- const matches = findSubscriptionFuzzy(subs, name);
5965
- const target = pickSingleSubscription(matches, name);
5966
- const currentDefault = getActiveSubscription();
5967
- const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
5968
- if (isAlreadyDefault) {
5969
- console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
6165
+ if (action === "off" || action === "disable") {
6166
+ if (!isOverwriteEnabled()) {
6167
+ console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u7981\u7528\u72B6\u6001");
5970
6168
  console.log("");
5971
- await printSubscriptionList();
6169
+ printOverwriteList();
5972
6170
  return;
5973
6171
  }
5974
- const status = getStatus();
5975
- const configInfo = getConfigInfo();
5976
- const currentMode = isDaemonEnabled() ? "mixed" : configInfo?.tun ? "tun" : "mixed";
5977
- const success = setDefaultSubscription(target.name);
5978
- if (success) {
5979
- console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
5980
- } else {
5981
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
5982
- process.exit(1);
5983
- }
5984
- if (status.running || isDaemonEnabled()) {
6172
+ setOverwriteEnabled(false);
6173
+ console.log("\u5DF2\u7981\u7528\u8986\u5199\u914D\u7F6E");
6174
+ if (restartNeeded) {
5985
6175
  console.log("");
5986
- await cmdStart(["start", currentMode]);
6176
+ await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
5987
6177
  return;
5988
6178
  }
5989
6179
  console.log("");
5990
- await printSubscriptionList();
6180
+ printOverwriteList();
5991
6181
  return;
5992
6182
  }
5993
- if (action === "web" || action === "open") {
5994
- const name = args[2];
5995
- const subs = getSubscriptionsWithCache();
5996
- if (subs.length === 0) {
5997
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6183
+ console.log("");
6184
+ printOverwriteList();
6185
+ }
6186
+
6187
+ // src/commands/reset.ts
6188
+ import fs8 from "fs";
6189
+ import readline from "readline";
6190
+ var RESET_TARGETS = [
6191
+ {
6192
+ id: "subs",
6193
+ aliases: ["sub", "subs", "subscription", "subscriptions"],
6194
+ label: "\u8BA2\u9605",
6195
+ paths: () => [DIRS.subscriptions],
6196
+ needsStop: true,
6197
+ // 同步清空 settings 里的订阅列表:只删缓存文件会留下"列表存在但无配置"的半重置状态
6198
+ // (start 会报"未找到订阅配置")。active_subscription 一并清除
6199
+ onAfter: () => writeSettings({ subscriptions: void 0, active_subscription: void 0 })
6200
+ },
6201
+ {
6202
+ id: "logs",
6203
+ aliases: ["log", "logs"],
6204
+ label: "\u65E5\u5FD7",
6205
+ paths: () => [DIRS.logs],
6206
+ needsStop: false
6207
+ },
6208
+ {
6209
+ id: "data",
6210
+ aliases: ["data"],
6211
+ label: "\u8FD0\u884C\u6570\u636E",
6212
+ paths: () => [DIRS.data],
6213
+ needsStop: true
6214
+ },
6215
+ {
6216
+ id: "runtime",
6217
+ aliases: ["runtime"],
6218
+ label: "\u8FD0\u884C\u65F6",
6219
+ paths: () => [DIRS.runtime],
6220
+ needsStop: true
6221
+ },
6222
+ {
6223
+ id: "settings",
6224
+ aliases: ["setting", "settings", "config"],
6225
+ label: "\u8BBE\u7F6E",
6226
+ paths: () => [PATHS.settingsFile],
6227
+ needsStop: false
6228
+ },
6229
+ {
6230
+ id: "kernel",
6231
+ aliases: ["kernel", "core"],
6232
+ label: "\u5185\u6838",
6233
+ paths: () => [DIRS.kernel],
6234
+ needsStop: false,
6235
+ onAfter: () => clearKernelVersionCache(),
6236
+ checkEmpty: () => !hasKernel(),
6237
+ emptyMsg: "\u5185\u6838\u672A\u5B89\u88C5\uFF0C\u65E0\u9700\u5220\u9664",
6238
+ warnIfRunning: true
6239
+ },
6240
+ {
6241
+ id: "overwrites",
6242
+ aliases: ["overwrite", "overwrites", "ow"],
6243
+ label: "\u8986\u5199",
6244
+ paths: () => {
6245
+ const dir = USER_DATA_DIR;
6246
+ if (!fs8.existsSync(dir)) return [];
6247
+ return fs8.readdirSync(dir).filter(isOverwriteFilename).map((f) => `${dir}/${f}`);
6248
+ },
6249
+ needsStop: false
6250
+ },
6251
+ {
6252
+ id: "daemon",
6253
+ aliases: ["daemon"],
6254
+ label: "\u4FDD\u6D3B",
6255
+ // 卸载由确认后的 disablesDaemon 段统一处理(需 sudo,受取消保护);
6256
+ // 此处 paths 返回空(plist 在系统目录,用户态删不掉,且不应提前删破坏卸载),
6257
+ // onAfter 因幂等守卫(plist 已删)成为 no-op,仅作单独 reset 未走前段时的兜底。
6258
+ paths: () => [],
6259
+ needsStop: false,
6260
+ onAfter: () => disableDaemon(),
6261
+ checkEmpty: () => !isDaemonEnabled(),
6262
+ emptyMsg: "\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u9700\u5220\u9664"
6263
+ }
6264
+ ];
6265
+ function resolveResetTargets(names) {
6266
+ const matched = [];
6267
+ const unmatched = [];
6268
+ for (const name of names) {
6269
+ const t = RESET_TARGETS.find((t2) => t2.aliases.includes(name.toLowerCase()));
6270
+ if (t) {
6271
+ if (!matched.find((m) => m.id === t.id)) matched.push(t);
6272
+ } else {
6273
+ unmatched.push(name);
6274
+ }
6275
+ }
6276
+ return { matched, unmatched };
6277
+ }
6278
+ async function confirmPrompt(question) {
6279
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6280
+ const answer = await new Promise((resolve) => {
6281
+ rl.question(`${question} (y/N) `, (a) => {
6282
+ rl.close();
6283
+ resolve(a);
6284
+ });
6285
+ });
6286
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
6287
+ }
6288
+ async function cmdReset(args) {
6289
+ const flags = (args || []).filter((a) => a.startsWith("-"));
6290
+ const names = (args || []).slice(1).filter((a) => !a.startsWith("-"));
6291
+ const KNOWN_FLAGS = /* @__PURE__ */ new Set(["--full", "--yes", "-y"]);
6292
+ const unknownFlags = flags.filter((f) => !KNOWN_FLAGS.has(f));
6293
+ if (unknownFlags.length > 0) {
6294
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u9009\u9879: ${unknownFlags.join(", ")}`);
6295
+ console.log("");
6296
+ console.log("\u53EF\u7528\u9009\u9879: --full\uFF08\u5220\u5168\u90E8\uFF09, -y/--yes\uFF08\u8DF3\u8FC7\u786E\u8BA4\uFF09");
6297
+ process.exit(1);
6298
+ }
6299
+ const fullReset = flags.includes("--full");
6300
+ const skipConfirm = flags.includes("--yes") || flags.includes("-y");
6301
+ let targets;
6302
+ if (fullReset) {
6303
+ targets = RESET_TARGETS;
6304
+ } else if (names.length > 0) {
6305
+ const { matched, unmatched } = resolveResetTargets(names);
6306
+ if (unmatched.length > 0) {
6307
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u91CD\u7F6E\u76EE\u6807: ${unmatched.join(", ")}`);
6308
+ console.log("");
6309
+ console.log(`\u53EF\u7528\u76EE\u6807: ${RESET_TARGETS.map((t) => t.aliases[0]).join(", ")}`);
6310
+ console.log("");
6311
+ console.log("\u793A\u4F8B:");
6312
+ console.log(" mihomo reset sub log # \u5220\u9664\u8BA2\u9605\u548C\u65E5\u5FD7");
6313
+ console.log(" mihomo reset kernel # \u53EA\u5220\u5185\u6838");
6314
+ console.log(" mihomo reset --full # \u5220\u9664\u5168\u90E8");
6315
+ console.log(" mihomo reset # \u5220\u9664\u5168\u90E8\uFF08\u4FDD\u7559\u8BBE\u7F6E\u3001\u5185\u6838\u3001\u8986\u5199\uFF09");
5998
6316
  process.exit(1);
5999
6317
  }
6000
- let target;
6001
- if (name) {
6002
- const matches = findSubscriptionFuzzy(subs, name);
6003
- target = pickSingleSubscription(matches, name);
6004
- } else {
6005
- target = getActiveSubscription() || subs[0];
6318
+ targets = matched;
6319
+ } else {
6320
+ targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
6321
+ }
6322
+ for (const t of targets) {
6323
+ if (t.checkEmpty?.()) {
6324
+ if (targets.length === 1) {
6325
+ console.log(t.emptyMsg);
6326
+ return;
6327
+ }
6006
6328
  }
6007
- const cached = subs.find((s) => s.name === target.name);
6008
- let webPageUrl = cached?.web_page_url;
6009
- if (!webPageUrl) {
6010
- console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u66F4\u65B0\u8BA2\u9605...");
6011
- try {
6012
- await downloadSubscription(target.url, target.name);
6013
- const cache = readSubscriptionCache();
6014
- if (cache[target.name]?.web_page_url) {
6015
- webPageUrl = cache[target.name].web_page_url;
6016
- } else {
6017
- console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
6018
- process.exit(1);
6329
+ }
6330
+ const needsStop = targets.some((t) => t.needsStop);
6331
+ const warnRunning = targets.some((t) => t.warnIfRunning);
6332
+ const kernelTargeted = targets.some((t) => t.id === "kernel");
6333
+ const daemonTargeted = targets.some((t) => t.id === "daemon");
6334
+ const disablesDaemon = needsStop || kernelTargeted || daemonTargeted;
6335
+ const pids = needsStop || warnRunning ? getMihomoPids() : [];
6336
+ if (warnRunning && pids.length > 0) {
6337
+ console.log(colors.yellow(`\u8B66\u544A: mihomo \u6B63\u5728\u8FD0\u884C (PID ${pids.join(", ")})\uFF0C\u5220\u9664\u5185\u6838\u540E\u5C06\u65E0\u6CD5\u91CD\u65B0\u542F\u52A8`));
6338
+ }
6339
+ if (disablesDaemon && isDaemonEnabled()) {
6340
+ 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"));
6341
+ }
6342
+ console.log(`\u5C06\u5220\u9664: ${targets.map((t) => t.label).join("\u3001")}`);
6343
+ if (!skipConfirm && !await confirmPrompt("\u786E\u8BA4?")) {
6344
+ console.log("\u5DF2\u53D6\u6D88");
6345
+ return;
6346
+ }
6347
+ if (disablesDaemon && isDaemonEnabled()) {
6348
+ try {
6349
+ disableDaemon();
6350
+ } catch (e) {
6351
+ console.error(`${colors.red("\u4FDD\u6D3B\u5173\u95ED\u5DF2\u53D6\u6D88\uFF0C\u91CD\u7F6E\u4E2D\u6B62:")} ${e.message.split("\n")[0]}`);
6352
+ return;
6353
+ }
6354
+ }
6355
+ if (needsStop && getMihomoPids().length > 0) {
6356
+ console.log("\u505C\u6B62\u8FDB\u7A0B...");
6357
+ cleanupAll();
6358
+ for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6359
+ if (getMihomoPids().length === 0) break;
6360
+ await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6361
+ }
6362
+ }
6363
+ for (const t of targets) {
6364
+ for (const p of t.paths()) {
6365
+ if (fs8.existsSync(p)) {
6366
+ try {
6367
+ rmrf(p);
6368
+ } catch (e) {
6369
+ console.warn(` \u8B66\u544A: \u65E0\u6CD5\u5220\u9664 ${p}: ${e.message}`);
6019
6370
  }
6020
- } catch (e) {
6021
- console.error(`\u66F4\u65B0\u5931\u8D25: ${e.message}`);
6022
- process.exit(1);
6023
6371
  }
6024
6372
  }
6025
- console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
6026
- const opened = openUrl(webPageUrl);
6027
- if (!opened) {
6028
- console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
6373
+ t.onAfter?.();
6374
+ }
6375
+ ensureDirs();
6376
+ if (targets.some((t) => t.id === "settings")) {
6377
+ invalidateSettingsCache();
6378
+ }
6379
+ console.log(colors.green(`\u5DF2\u91CD\u7F6E: ${targets.map((t) => t.label).join("\u3001")}`));
6380
+ }
6381
+
6382
+ // src/test-instance.ts
6383
+ import { spawn as spawn2 } from "child_process";
6384
+ import fs9 from "fs";
6385
+ import path8 from "path";
6386
+ var TEST_DIR = path8.join(USER_DATA_DIR, "test");
6387
+ var TEST_DIRS = {
6388
+ data: path8.join(TEST_DIR, "data"),
6389
+ runtime: path8.join(TEST_DIR, "runtime")
6390
+ };
6391
+ var TEST_PATHS = {
6392
+ configFile: path8.join(TEST_DIRS.runtime, "config.yaml"),
6393
+ pidFile: path8.join(TEST_DIRS.runtime, "pid"),
6394
+ logFile: path8.join(TEST_DIR, "test.log")
6395
+ };
6396
+ var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
6397
+ function ensureTestDirs() {
6398
+ for (const dir of Object.values(TEST_DIRS)) {
6399
+ fs9.mkdirSync(dir, { recursive: true, mode: 448 });
6400
+ }
6401
+ }
6402
+ function cleanupTestDir() {
6403
+ rmrf(TEST_DIR);
6404
+ }
6405
+ function buildTestConfig(subName) {
6406
+ ensureTestDirs();
6407
+ const rawContent = readSubscriptionRawConfig(subName);
6408
+ if (!rawContent) {
6409
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
6410
+ }
6411
+ const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
6412
+ const proxies = (parsed.proxies || []).filter(isProxyValid);
6413
+ if (proxies.length === 0) {
6414
+ throw new Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
6415
+ }
6416
+ const nameCount = /* @__PURE__ */ new Map();
6417
+ for (const proxy of proxies) {
6418
+ const count = (nameCount.get(proxy.name) || 0) + 1;
6419
+ nameCount.set(proxy.name, count);
6420
+ if (count > 1) {
6421
+ proxy.name = `${proxy.name} #${count}`;
6029
6422
  }
6030
- return;
6031
6423
  }
6032
- if (action === "remove" || action === "rm" || action === "delete") {
6033
- const name = args[2];
6034
- const subs = getSubscriptions();
6035
- if (!name) {
6036
- console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
6037
- if (subs.length > 0) {
6038
- console.log("\n\u53EF\u7528\u8BA2\u9605:");
6039
- for (const s of subs) console.log(` ${s.name}`);
6424
+ const config = {
6425
+ ...TEST_CONFIG,
6426
+ proxies,
6427
+ "proxy-groups": [
6428
+ {
6429
+ name: "PROXY",
6430
+ type: "select",
6431
+ proxies: proxies.map((p) => p.name)
6040
6432
  }
6041
- process.exit(1);
6433
+ ],
6434
+ rules: ["MATCH,PROXY"]
6435
+ };
6436
+ const content = dumpYaml(config);
6437
+ fs9.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
6438
+ }
6439
+ async function startTestInstance() {
6440
+ const binary = PATHS.mihomoBinary;
6441
+ if (!fs9.existsSync(binary)) throw new Error('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
6442
+ stopTestInstance();
6443
+ const logFd = fs9.openSync(TEST_PATHS.logFile, "a");
6444
+ const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
6445
+ detached: true,
6446
+ stdio: ["ignore", logFd, logFd]
6447
+ });
6448
+ child.on("error", () => {
6449
+ });
6450
+ fs9.closeSync(logFd);
6451
+ child.unref();
6452
+ const pid = child.pid;
6453
+ 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");
6454
+ fs9.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
6455
+ const client = createHttpClient({ timeout: 2e3 });
6456
+ let ready = false;
6457
+ for (let i = 0; i < 60; i++) {
6458
+ if (!isProcessRunning(pid)) break;
6459
+ try {
6460
+ await client.get(`${TEST_API}/version`);
6461
+ ready = true;
6462
+ break;
6463
+ } catch {
6464
+ await sleep(500);
6042
6465
  }
6043
- const matches = findSubscriptionFuzzy(subs, name);
6044
- const target = pickSingleSubscription(matches, name);
6045
- const switchedTo = removeSubscription(target.name);
6046
- console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
6047
- if (switchedTo) {
6048
- console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
6466
+ }
6467
+ if (!isProcessRunning(pid)) {
6468
+ let errorDetail = "";
6469
+ try {
6470
+ errorDetail = fs9.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
6471
+ } catch {
6049
6472
  }
6050
- console.log("");
6051
- await printSubscriptionList({ autoUpdate: false });
6473
+ throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
6474
+ ${errorDetail}` : ""}`);
6475
+ }
6476
+ if (!ready) {
6477
+ throw new Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
6478
+ }
6479
+ }
6480
+ function stopTestInstance() {
6481
+ let pid;
6482
+ try {
6483
+ pid = parseInt(fs9.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
6484
+ } catch {
6052
6485
  return;
6053
6486
  }
6054
- if (action === "clean") {
6055
- const { target, timeout, concurrency } = resolveTestTarget(args);
6056
- const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6057
- console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
6058
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6059
- console.log("");
6060
- const progress = createProgressPrinter(rounds);
6061
- const result = await withTestInstance(target.name, async (apiBase) => {
6062
- return autoCleanSubscription(target.name, {
6063
- timeout,
6064
- concurrency,
6065
- rounds,
6066
- apiBase,
6067
- onResult: progress.onResult,
6068
- onRetryRound: progress.onRetryRound
6069
- });
6070
- });
6071
- progress.finish();
6072
- console.log(formatTestSummary(result.summary));
6073
- if (result.skipped) {
6074
- console.log("");
6075
- console.log(colors.yellow("\u5B58\u6D3B\u8282\u70B9\u4E0D\u8DB3 1%\uFF0C\u8DF3\u8FC7\u6E05\u7406\u3002\u8BF7\u68C0\u67E5\u539F\u59CB\u8BA2\u9605\u662F\u5426\u6709\u6548"));
6076
- } else if (result.removedProxies > 0) {
6077
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6078
- const status = getStatus();
6079
- if (status.running) {
6080
- console.log("");
6081
- console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
6082
- }
6487
+ if (pid > 0 && isProcessRunning(pid)) {
6488
+ process.kill(pid, "SIGKILL");
6489
+ for (let i = 0; i < 20; i++) {
6490
+ if (!isProcessRunning(pid)) break;
6491
+ sleepSync(100);
6083
6492
  }
6084
- return;
6085
6493
  }
6086
- if (action === "test") {
6087
- const { target, timeout, concurrency } = resolveTestTarget(args);
6088
- console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
6089
- console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6090
- console.log("");
6091
- const progress = createProgressPrinter();
6092
- const summary = await withTestInstance(target.name, async (apiBase) => {
6093
- return testSubscriptionProxies(target.name, {
6094
- timeout,
6095
- concurrency,
6096
- apiBase,
6097
- onResult: progress.onResult
6098
- });
6099
- });
6100
- progress.finish();
6101
- console.log(formatTestSummary(summary));
6102
- return;
6494
+ try {
6495
+ fs9.unlinkSync(TEST_PATHS.pidFile);
6496
+ } catch {
6103
6497
  }
6104
- console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
6105
- console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
6106
- process.exit(1);
6107
6498
  }
6108
-
6109
- // src/commands/start.ts
6110
- var DAEMON_RESTART_WAIT_MS = 500;
6111
- async function cmdStart(args) {
6112
- if (!hasKernel()) {
6113
- console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
6114
- process.exit(1);
6499
+ async function withTestInstance(subName, fn) {
6500
+ cleanupTestDir();
6501
+ buildTestConfig(subName);
6502
+ const unregister = registerCleanup(() => {
6503
+ stopTestInstance();
6504
+ cleanupTestDir();
6505
+ });
6506
+ try {
6507
+ await startTestInstance();
6508
+ return await fn(TEST_API);
6509
+ } finally {
6510
+ unregister();
6511
+ stopTestInstance();
6512
+ cleanupTestDir();
6115
6513
  }
6116
- const targetMode = args[1] === "tun" ? "tun" : "mixed";
6117
- const daemonEnabled = isDaemonEnabled();
6118
- if (targetMode === "tun" && daemonEnabled) {
6119
- 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`);
6120
- console.error("\u8BF7\u5148\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
6514
+ }
6515
+
6516
+ // src/commands/subscription.ts
6517
+ function githubRepoUrl(rawUrl) {
6518
+ const match = rawUrl.match(/raw\.githubusercontent\.com\/([^/]+\/[^/]+)/);
6519
+ if (match) return `https://github.com/${match[1]}`;
6520
+ return null;
6521
+ }
6522
+ function resolveTestTarget(args) {
6523
+ const subs = getSubscriptions();
6524
+ if (subs.length === 0) {
6525
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6121
6526
  process.exit(1);
6122
6527
  }
6123
- const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6528
+ const nameArg = getNonFlagArg(args, 2);
6124
6529
  const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
6125
6530
  const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
6126
- const skipUpdate = hasFlag(args, "-s", "--no-update");
6127
- const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
6128
- const sub = getActiveSubscription();
6129
- if (!sub) {
6130
- console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
6131
- process.exit(1);
6531
+ let target;
6532
+ if (nameArg) {
6533
+ const matches = findSubscriptionFuzzy(subs, nameArg);
6534
+ target = pickSingleSubscription(matches, nameArg);
6535
+ } else {
6536
+ const activeSub = getActiveSubscription();
6537
+ if (!activeSub) {
6538
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605\uFF0C\u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
6539
+ process.exit(1);
6540
+ }
6541
+ target = activeSub;
6132
6542
  }
6133
- if (!skipUpdate) {
6134
- await autoUpdateStaleSubscription({ timeout: updateTimeout });
6543
+ return { target, timeout, concurrency };
6544
+ }
6545
+ function printRestartHintIfRunning() {
6546
+ if (getRunningState().running) {
6547
+ console.log(colors.yellow("\u63D0\u793A: \u8FD0\u884C\u4E2D\u7684\u5B9E\u4F8B\u4ECD\u4F7F\u7528\u65E7\u914D\u7F6E\uFF0C\u6267\u884C mihomo start \u4F7F\u66F4\u65B0\u751F\u6548"));
6548
+ console.log("");
6135
6549
  }
6136
- async function launchOrRestart() {
6137
- if (daemonEnabled) {
6138
- restartDaemon();
6139
- await sleep(DAEMON_RESTART_WAIT_MS);
6140
- return getDaemonStatus().pid;
6141
- }
6142
- const result = await start(targetMode);
6143
- return result.pid;
6550
+ }
6551
+ function printSubscriptionList() {
6552
+ const subs = getSubscriptionsWithCache();
6553
+ if (subs.length === 0) {
6554
+ console.log("\u6CA1\u6709\u8BA2\u9605");
6555
+ console.log("");
6556
+ console.log("\u6DFB\u52A0\u8BA2\u9605: mihomo sub add <url> [name]");
6557
+ console.log("");
6558
+ return;
6144
6559
  }
6145
- if (!daemonEnabled) {
6146
- const status = getStatus();
6147
- const hasProcess = status.running || status.allProcesses.length > 0;
6148
- if (hasProcess) {
6149
- const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
6150
- console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
6560
+ const activeSub = getActiveSubscription();
6561
+ console.log(colors.cyan("\u8BA2\u9605\u5217\u8868:"));
6562
+ subs.forEach((s, i) => {
6563
+ const time = formatDate(s.updated_at);
6564
+ const defaultMark = activeSub && s.name === activeSub.name ? colors.green(" [\u4F7F\u7528\u4E2D]") : "";
6565
+ const mergeBadge = isMultiUrl(s.url) ? colors.cyan(` [\u5408\u5E76 ${splitUrls(s.url).length} \u6E90]`) : "";
6566
+ const interval = resolveUpdateInterval(s.url, s.update_interval);
6567
+ console.log(` ${i + 1}. ${s.name}${defaultMark}${mergeBadge}`);
6568
+ console.log(` ${colors.gray("\u66F4\u65B0: ")}${time} (\u95F4\u9694: ${interval}h)`);
6569
+ if (s.username) {
6570
+ console.log(` ${colors.gray("\u7528\u6237: ")}${s.username}`);
6151
6571
  }
6152
- handleStopResult(stop());
6153
- if (hasProcess) {
6154
- console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
6155
- `);
6572
+ if (s.download !== void 0 || s.total !== void 0) {
6573
+ const used = (s.upload || 0) + (s.download || 0);
6574
+ const usedStr = formatBytes(used);
6575
+ const totalStr = formatBytes(s.total);
6576
+ let percentStr = "";
6577
+ if (s.total && s.total > 0) {
6578
+ const percent = Math.min(used / s.total * 100, 100);
6579
+ percentStr = ` (${percent.toFixed(1)}%)`;
6580
+ }
6581
+ console.log(` ${colors.gray("\u6D41\u91CF: ")}${usedStr} / ${totalStr}${percentStr}`);
6156
6582
  }
6583
+ if (s.expire !== void 0) {
6584
+ console.log(` ${colors.gray("\u5230\u671F: ")}${formatTimestamp(s.expire)}`);
6585
+ }
6586
+ if (s.web_page_url) {
6587
+ console.log(` ${colors.gray("\u9875\u9762: ")}${s.web_page_url}`);
6588
+ }
6589
+ });
6590
+ console.log("");
6591
+ console.log("\u5207\u6362\u8BA2\u9605: mihomo sub use <name>");
6592
+ console.log("\u65B0\u589E\u8BA2\u9605: mihomo sub add <url> [name]");
6593
+ console.log("\u66F4\u65B0\u8BA2\u9605: mihomo sub update [name]");
6594
+ console.log("\u5220\u9664\u8BA2\u9605: mihomo sub remove <name>");
6595
+ console.log("\u6D4B\u8BD5\u8282\u70B9: mihomo sub test [name]");
6596
+ console.log("\u6E05\u7406\u8282\u70B9: mihomo sub clean [name]");
6597
+ console.log("\u6253\u5F00\u9875\u9762: mihomo sub web [name]");
6598
+ console.log("");
6599
+ }
6600
+ async function cmdSubscription(args) {
6601
+ const action = args[1];
6602
+ if (!action || action === "list") {
6603
+ printSubscriptionList();
6604
+ return;
6157
6605
  }
6158
- let configInfo;
6159
- try {
6160
- configInfo = prepareConfigForStart(targetMode, sub.name);
6161
- } catch (e) {
6162
- console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
6163
- process.exit(1);
6164
- }
6165
- const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
6166
- console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
6167
- try {
6168
- const pid = await launchOrRestart();
6169
- const label = daemonEnabled ? "\u5DF2\u542F\u52A8 (\u4FDD\u6D3B)" : "\u5DF2\u542F\u52A8";
6170
- console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""}`);
6171
- } catch (e) {
6172
- const msg = e.message;
6173
- const lines = msg.split("\n");
6174
- console.error(`${colors.red("\u542F\u52A8\u5931\u8D25:")} ${lines[0]}`);
6175
- if (lines.length > 1) {
6176
- for (const line of lines.slice(1)) console.error(line);
6606
+ if (action === "add") {
6607
+ const url = args[2]?.trim();
6608
+ const name = args[3] || "default";
6609
+ if (!url) {
6610
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6611
+ process.exit(1);
6177
6612
  }
6178
- process.exit(1);
6179
- }
6180
- const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
6181
- if (configInfo.proxies > cleanThreshold) {
6182
- console.log("");
6183
- console.log(`\u8282\u70B9\u6570 ${configInfo.proxies} \u8D85\u8FC7 ${cleanThreshold}\uFF0C\u81EA\u52A8\u6E05\u7406...`);
6184
- console.log("");
6185
- await sleep(1e3);
6186
- const progress = createProgressPrinter(rounds);
6187
- const cleanResult = await autoCleanSubscription(sub.name, {
6188
- timeout,
6189
- concurrency,
6190
- rounds,
6191
- onResult: progress.onResult,
6192
- onRetryRound: progress.onRetryRound
6193
- });
6194
- progress.finish();
6195
- console.log(formatTestSummary(cleanResult.summary));
6196
- if (cleanResult.skipped) {
6197
- 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"));
6198
- } else if (cleanResult.removedProxies > 0) {
6199
- console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
6200
- console.log("");
6201
- console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
6202
- if (!daemonEnabled) handleStopResult(stop());
6613
+ if (isMultiUrl(url)) {
6614
+ const urls = splitUrls(url);
6615
+ if (urls.length === 0) {
6616
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL");
6617
+ process.exit(1);
6618
+ }
6619
+ for (const u of urls) {
6620
+ if (!isValidHttpUrl(u)) {
6621
+ console.error(`\u9519\u8BEF: \u65E0\u6548\u7684 URL: ${u}`);
6622
+ process.exit(1);
6623
+ }
6624
+ }
6625
+ const normalizedUrl = urls.join(",");
6626
+ console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
6203
6627
  try {
6204
- configInfo = prepareConfigForStart(targetMode, sub.name);
6205
- const pid = await launchOrRestart();
6206
- console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
6628
+ addSubscription(normalizedUrl, name);
6629
+ setDefaultSubscription(name);
6630
+ const info = await downloadMergedSubscription(urls, name);
6631
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
6207
6632
  } catch (e) {
6208
- console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6633
+ removeSubscription(name);
6634
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
6209
6635
  process.exit(1);
6210
6636
  }
6211
- }
6212
- }
6213
- printStatus();
6214
- }
6215
-
6216
- // src/commands/overwrite.ts
6217
- function printOverwriteList() {
6218
- const info = listOverwriteFile();
6219
- const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
6220
- console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}`);
6221
- console.log(`${colors.gray("\u4F4D\u7F6E: ")}${info.dir}`);
6222
- console.log("");
6223
- if (info.files.length === 0) {
6224
- console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
6225
- console.log("");
6226
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path7.join(info.dir, "overwrite.yaml")}`);
6227
- console.log(` \u6216 ${path7.join(info.dir, "overwrite.dns.yaml")}`);
6228
- console.log("");
6229
- } else {
6230
- console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
6231
- console.log("");
6232
- info.files.forEach((f, i) => {
6233
- const num = i < 10 ? ` ${i}` : `${i}`;
6234
- console.log(` ${num}. ${f.name}`);
6235
- if (f.keys.length > 0) {
6236
- console.log(` ${colors.gray("\u5B57\u6BB5: ")}${f.keys.join(", ")}`);
6637
+ } else {
6638
+ if (!isValidHttpUrl(url)) {
6639
+ console.error("\u9519\u8BEF: \u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL\uFF08\u9700\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF09");
6640
+ process.exit(1);
6641
+ }
6642
+ console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
6643
+ try {
6644
+ addSubscription(url, name);
6645
+ setDefaultSubscription(name);
6646
+ const info = await downloadSubscription(url, name);
6647
+ const repoUrl = githubRepoUrl(url);
6648
+ if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
6649
+ console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
6650
+ } catch (e) {
6651
+ removeSubscription(name);
6652
+ console.error(`\u6DFB\u52A0\u5931\u8D25: ${e.message}`);
6653
+ process.exit(1);
6237
6654
  }
6238
- });
6239
- console.log("");
6240
- }
6241
- console.log("\u542F\u7528\u8986\u5199: mihomo ow on");
6242
- console.log("\u7981\u7528\u8986\u5199: mihomo ow off");
6243
- console.log("");
6244
- }
6245
- async function cmdOverwrite(args) {
6246
- const action = args?.[1];
6247
- const status = getStatus();
6248
- const configInfo = getConfigInfo();
6249
- const currentMode = isDaemonEnabled() ? "mixed" : configInfo?.tun ? "tun" : "mixed";
6250
- if (action === "on" || action === "enable") {
6251
- if (isOverwriteEnabled()) {
6252
- console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u542F\u7528\u72B6\u6001");
6253
- console.log("");
6254
- printOverwriteList();
6255
- return;
6256
- }
6257
- setOverwriteEnabled(true);
6258
- console.log("\u5DF2\u542F\u7528\u8986\u5199\u914D\u7F6E");
6259
- if (status.running || isDaemonEnabled()) {
6260
- console.log("");
6261
- await cmdStart(["start", currentMode]);
6262
- return;
6263
6655
  }
6264
6656
  console.log("");
6265
- printOverwriteList();
6657
+ printSubscriptionList();
6266
6658
  return;
6267
6659
  }
6268
- if (action === "off" || action === "disable") {
6269
- if (!isOverwriteEnabled()) {
6270
- console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u7981\u7528\u72B6\u6001");
6271
- console.log("");
6272
- printOverwriteList();
6273
- return;
6660
+ if (action === "update") {
6661
+ const name = args[2];
6662
+ const subs = getSubscriptions();
6663
+ if (subs.length === 0) {
6664
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6665
+ process.exit(1);
6274
6666
  }
6275
- setOverwriteEnabled(false);
6276
- console.log("\u5DF2\u7981\u7528\u8986\u5199\u914D\u7F6E");
6277
- if (status.running || isDaemonEnabled()) {
6667
+ if (!name) {
6668
+ console.log(`\u66F4\u65B0\u6240\u6709 ${subs.length} \u4E2A\u8BA2\u9605...`);
6669
+ const results = await Promise.all(subs.map((sub) => tryUpdateOne(sub)));
6670
+ let ok = 0;
6671
+ for (const r of results) {
6672
+ if (r.success) ok++;
6673
+ printUpdateResult(r);
6674
+ }
6675
+ if (ok === 0) process.exit(1);
6278
6676
  console.log("");
6279
- await cmdStart(["start", currentMode]);
6677
+ printRestartHintIfRunning();
6678
+ printSubscriptionList();
6280
6679
  return;
6281
6680
  }
6681
+ const matches = findSubscriptionFuzzy(subs, name);
6682
+ const target = pickSingleSubscription(matches, name);
6683
+ console.log(`\u66F4\u65B0\u8BA2\u9605: ${target.name}`);
6684
+ const result = await tryUpdateOne(target);
6685
+ if (!result.success) {
6686
+ console.error(`\u66F4\u65B0\u5931\u8D25: ${(result.error || "").split("\n")[0]}`);
6687
+ process.exit(1);
6688
+ }
6689
+ console.log(`\u5DF2\u66F4\u65B0 (${formatProxySummary(result)})`);
6282
6690
  console.log("");
6283
- printOverwriteList();
6691
+ printRestartHintIfRunning();
6692
+ printSubscriptionList();
6284
6693
  return;
6285
6694
  }
6286
- console.log("");
6287
- printOverwriteList();
6288
- }
6289
-
6290
- // src/commands/reset.ts
6291
- import fs9 from "fs";
6292
- import readline from "readline";
6293
- var RESET_TARGETS = [
6294
- {
6295
- id: "subs",
6296
- aliases: ["sub", "subs", "subscription", "subscriptions"],
6297
- label: "\u8BA2\u9605",
6298
- paths: () => [DIRS.subscriptions],
6299
- needsStop: true
6300
- },
6301
- {
6302
- id: "logs",
6303
- aliases: ["log", "logs"],
6304
- label: "\u65E5\u5FD7",
6305
- paths: () => [DIRS.logs],
6306
- needsStop: false
6307
- },
6308
- {
6309
- id: "data",
6310
- aliases: ["data"],
6311
- label: "\u8FD0\u884C\u6570\u636E",
6312
- paths: () => [DIRS.data],
6313
- needsStop: true
6314
- },
6315
- {
6316
- id: "runtime",
6317
- aliases: ["runtime"],
6318
- label: "\u8FD0\u884C\u65F6",
6319
- paths: () => [DIRS.runtime],
6320
- needsStop: true
6321
- },
6322
- {
6323
- id: "settings",
6324
- aliases: ["setting", "settings", "config"],
6325
- label: "\u8BBE\u7F6E",
6326
- paths: () => [PATHS.settingsFile],
6327
- needsStop: false
6328
- },
6329
- {
6330
- id: "kernel",
6331
- aliases: ["kernel", "core"],
6332
- label: "\u5185\u6838",
6333
- paths: () => [DIRS.kernel],
6334
- needsStop: false,
6335
- onAfter: () => clearKernelVersionCache(),
6336
- checkEmpty: () => !hasKernel(),
6337
- emptyMsg: "\u5185\u6838\u672A\u5B89\u88C5\uFF0C\u65E0\u9700\u5220\u9664",
6338
- warnIfRunning: true
6339
- },
6340
- {
6341
- id: "overwrites",
6342
- aliases: ["overwrite", "overwrites", "ow"],
6343
- label: "\u8986\u5199",
6344
- paths: () => {
6345
- const dir = USER_DATA_DIR;
6346
- if (!fs9.existsSync(dir)) return [];
6347
- return fs9.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
6348
- },
6349
- needsStop: false
6350
- },
6351
- {
6352
- id: "daemon",
6353
- aliases: ["daemon"],
6354
- label: "\u4FDD\u6D3B",
6355
- // 删除全部交给 onAfter:disableDaemon 会先 bootout 卸载、再删 plist、再兜底清理进程,
6356
- // 顺序正确(先卸载再删文件);此处返回空避免提前删掉 plist 破坏卸载。
6357
- paths: () => [],
6358
- needsStop: false,
6359
- onAfter: () => disableDaemon(),
6360
- checkEmpty: () => !isDaemonEnabled(),
6361
- emptyMsg: "\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u9700\u5220\u9664"
6362
- }
6363
- ];
6364
- function resolveResetTargets(names) {
6365
- const matched = [];
6366
- const unmatched = [];
6367
- for (const name of names) {
6368
- const t = RESET_TARGETS.find((t2) => t2.aliases.includes(name.toLowerCase()));
6369
- if (t) {
6370
- if (!matched.find((m) => m.id === t.id)) matched.push(t);
6371
- } else {
6372
- unmatched.push(name);
6695
+ if (action === "use") {
6696
+ const name = args[2];
6697
+ const subs = getSubscriptions();
6698
+ if (!name) {
6699
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8BA2\u9605\u540D\u79F0");
6700
+ if (subs.length > 0) {
6701
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
6702
+ for (const s of subs) console.log(` ${s.name}`);
6703
+ }
6704
+ process.exit(1);
6373
6705
  }
6374
- }
6375
- return { matched, unmatched };
6376
- }
6377
- async function confirmPrompt(question) {
6378
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6379
- const answer = await new Promise((resolve) => {
6380
- rl.question(`${question} (y/N) `, (a) => {
6381
- rl.close();
6382
- resolve(a);
6383
- });
6384
- });
6385
- return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
6386
- }
6387
- async function cmdReset(args) {
6388
- const flags = (args || []).filter((a) => a.startsWith("-"));
6389
- const names = (args || []).slice(1).filter((a) => !a.startsWith("-"));
6390
- const fullReset = flags.includes("--full") || flags.includes("-f");
6391
- const skipConfirm = flags.includes("--yes") || flags.includes("-y");
6392
- let targets;
6393
- if (fullReset) {
6394
- targets = RESET_TARGETS;
6395
- } else if (names.length > 0) {
6396
- const { matched, unmatched } = resolveResetTargets(names);
6397
- if (unmatched.length > 0) {
6398
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u91CD\u7F6E\u76EE\u6807: ${unmatched.join(", ")}`);
6399
- console.log("");
6400
- console.log(`\u53EF\u7528\u76EE\u6807: ${RESET_TARGETS.map((t) => t.aliases[0]).join(", ")}`);
6706
+ const matches = findSubscriptionFuzzy(subs, name);
6707
+ const target = pickSingleSubscription(matches, name);
6708
+ const currentDefault = getActiveSubscription();
6709
+ const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
6710
+ if (isAlreadyDefault) {
6711
+ console.log(`"${target.name}" \u5DF2\u662F\u5F53\u524D\u4F7F\u7528\u7684\u8BA2\u9605`);
6401
6712
  console.log("");
6402
- console.log("\u793A\u4F8B:");
6403
- console.log(" mihomo reset sub log # \u5220\u9664\u8BA2\u9605\u548C\u65E5\u5FD7");
6404
- console.log(" mihomo reset kernel # \u53EA\u5220\u5185\u6838");
6405
- console.log(" mihomo reset --full # \u5220\u9664\u5168\u90E8");
6406
- console.log(" mihomo reset # \u5220\u9664\u5168\u90E8\uFF08\u4FDD\u7559\u8BBE\u7F6E\u3001\u5185\u6838\u3001\u8986\u5199\uFF09");
6713
+ printSubscriptionList();
6714
+ return;
6715
+ }
6716
+ const currentMode = getRuntimeMode();
6717
+ const restartNeeded = isRestartNeededOnChange();
6718
+ const success = setDefaultSubscription(target.name);
6719
+ if (success) {
6720
+ console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
6721
+ } else {
6722
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
6407
6723
  process.exit(1);
6408
6724
  }
6409
- targets = matched;
6410
- } else {
6411
- targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
6725
+ if (restartNeeded) {
6726
+ console.log("");
6727
+ await cmdStart(["start", currentMode, ...extractStartOptions(args)]);
6728
+ return;
6729
+ }
6730
+ console.log("");
6731
+ printSubscriptionList();
6732
+ return;
6412
6733
  }
6413
- for (const t of targets) {
6414
- if (t.checkEmpty?.()) {
6415
- if (targets.length === 1) {
6416
- console.log(t.emptyMsg);
6417
- return;
6734
+ if (action === "web" || action === "open") {
6735
+ const name = args[2];
6736
+ const subs = getSubscriptionsWithCache();
6737
+ if (subs.length === 0) {
6738
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605");
6739
+ process.exit(1);
6740
+ }
6741
+ let target;
6742
+ if (name) {
6743
+ const matches = findSubscriptionFuzzy(subs, name);
6744
+ target = pickSingleSubscription(matches, name);
6745
+ } else {
6746
+ target = getActiveSubscription() || subs[0];
6747
+ }
6748
+ const cached = subs.find((s) => s.name === target.name);
6749
+ let webPageUrl = cached?.web_page_url;
6750
+ if (!webPageUrl) {
6751
+ console.log("\u8BA2\u9605\u4FE1\u606F\u4E2D\u7F3A\u5C11\u9875\u9762\u5730\u5740\uFF0C\u6B63\u5728\u67E5\u8BE2\u8BA2\u9605...");
6752
+ try {
6753
+ const info = isMultiUrl(target.url) ? await downloadMergedSubscription(splitUrls(target.url), target.name, void 0, false) : await downloadSubscription(target.url, target.name, void 0, false);
6754
+ if (info.webPageUrl) {
6755
+ webPageUrl = info.webPageUrl;
6756
+ } else {
6757
+ console.error("\u9519\u8BEF: \u8BE5\u8BA2\u9605\u6CA1\u6709\u63D0\u4F9B\u9875\u9762\u5730\u5740");
6758
+ process.exit(1);
6759
+ }
6760
+ } catch (e) {
6761
+ console.error(`\u67E5\u8BE2\u5931\u8D25: ${e.message}`);
6762
+ process.exit(1);
6418
6763
  }
6419
6764
  }
6420
- }
6421
- const needsStop = targets.some((t) => t.needsStop);
6422
- const warnRunning = targets.some((t) => t.warnIfRunning);
6423
- const kernelTargeted = targets.some((t) => t.id === "kernel");
6424
- const disablesDaemon = needsStop || kernelTargeted;
6425
- const pids = needsStop || warnRunning ? getAllMihomoPids() : [];
6426
- if (warnRunning && pids.length > 0) {
6427
- console.log(colors.yellow(`\u8B66\u544A: mihomo \u6B63\u5728\u8FD0\u884C (PID ${pids.join(", ")})\uFF0C\u5220\u9664\u5185\u6838\u540E\u5C06\u65E0\u6CD5\u91CD\u65B0\u542F\u52A8`));
6428
- }
6429
- if (disablesDaemon && isDaemonEnabled()) {
6430
- 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"));
6431
- }
6432
- console.log(`\u5C06\u5220\u9664: ${targets.map((t) => t.label).join("\u3001")}`);
6433
- if (!skipConfirm && !await confirmPrompt("\u786E\u8BA4?")) {
6434
- console.log("\u5DF2\u53D6\u6D88");
6765
+ console.log(`\u6253\u5F00\u8BA2\u9605\u9875\u9762: ${webPageUrl}`);
6766
+ const opened = openUrl(webPageUrl);
6767
+ if (!opened) {
6768
+ console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
6769
+ }
6435
6770
  return;
6436
6771
  }
6437
- if (disablesDaemon && isDaemonEnabled()) {
6438
- disableDaemon();
6439
- }
6440
- if (needsStop && getAllMihomoPids().length > 0) {
6441
- console.log("\u505C\u6B62\u8FDB\u7A0B...");
6442
- cleanupAll();
6443
- for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6444
- if (getAllMihomoPids().length === 0) break;
6445
- await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6772
+ if (action === "remove" || action === "rm" || action === "delete") {
6773
+ const name = args[2];
6774
+ const subs = getSubscriptions();
6775
+ if (!name) {
6776
+ console.error("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0");
6777
+ if (subs.length > 0) {
6778
+ console.log("\n\u53EF\u7528\u8BA2\u9605:");
6779
+ for (const s of subs) console.log(` ${s.name}`);
6780
+ }
6781
+ process.exit(1);
6782
+ }
6783
+ const matches = findSubscriptionFuzzy(subs, name);
6784
+ const target = pickSingleSubscription(matches, name);
6785
+ const switchedTo = removeSubscription(target.name);
6786
+ console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
6787
+ if (switchedTo) {
6788
+ console.log(`\u5DF2\u81EA\u52A8\u5207\u6362\u5230 "${switchedTo}"`);
6446
6789
  }
6790
+ console.log("");
6791
+ printSubscriptionList();
6792
+ return;
6447
6793
  }
6448
- for (const t of targets) {
6449
- for (const p of t.paths()) {
6450
- if (fs9.existsSync(p)) {
6451
- try {
6452
- rmrf(p);
6453
- } catch (e) {
6454
- console.warn(` \u8B66\u544A: \u65E0\u6CD5\u5220\u9664 ${p}: ${e.message}`);
6455
- }
6794
+ if (action === "clean") {
6795
+ const { target, timeout, concurrency } = resolveTestTarget(args);
6796
+ const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
6797
+ console.log(`\u6E05\u7406\u8BA2\u9605 "${target.name}"...`);
6798
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6799
+ console.log("");
6800
+ const progress = createProgressPrinter(rounds);
6801
+ const result = await withTestInstance(target.name, async (apiBase) => {
6802
+ return autoCleanSubscription(target.name, {
6803
+ timeout,
6804
+ concurrency,
6805
+ rounds,
6806
+ apiBase,
6807
+ onResult: progress.onResult,
6808
+ onRetryRound: progress.onRetryRound
6809
+ });
6810
+ });
6811
+ progress.finish();
6812
+ console.log(formatTestSummary(result.summary));
6813
+ if (result.skipped) {
6814
+ console.log("");
6815
+ 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"));
6816
+ } else if (result.removedProxies > 0) {
6817
+ console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6818
+ if (getRunningState().running) {
6819
+ console.log("");
6820
+ console.log("\u63D0\u793A: \u9700\u8981\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548 (mihomo start)");
6456
6821
  }
6457
6822
  }
6458
- t.onAfter?.();
6823
+ return;
6459
6824
  }
6460
- ensureDirs();
6461
- if (targets.some((t) => t.id === "settings")) {
6462
- invalidateSettingsCache();
6825
+ if (action === "test") {
6826
+ const { target, timeout, concurrency } = resolveTestTarget(args);
6827
+ console.log(`\u6D4B\u8BD5\u8BA2\u9605 "${target.name}" \u7684\u8282\u70B9\u8FDE\u901A\u6027...`);
6828
+ console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
6829
+ console.log("");
6830
+ const progress = createProgressPrinter();
6831
+ const summary = await withTestInstance(target.name, async (apiBase) => {
6832
+ return testSubscriptionProxies(target.name, {
6833
+ timeout,
6834
+ concurrency,
6835
+ apiBase,
6836
+ onResult: progress.onResult
6837
+ });
6838
+ });
6839
+ progress.finish();
6840
+ console.log(formatTestSummary(summary));
6841
+ return;
6463
6842
  }
6464
- console.log(colors.green(`\u5DF2\u91CD\u7F6E: ${targets.map((t) => t.label).join("\u3001")}`));
6843
+ console.error("\u9519\u8BEF: \u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4");
6844
+ console.log("\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]");
6845
+ process.exit(1);
6465
6846
  }
6466
6847
 
6467
6848
  // src/commands/test.ts
6468
6849
  function requireRunning() {
6469
- if (isDaemonEnabled()) {
6470
- if (!isDaemonRunning(getDaemonStatus())) {
6471
- console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo daemon on)");
6472
- process.exit(1);
6473
- }
6474
- return;
6475
- }
6476
- const status = getStatus();
6477
- if (!status.running) {
6478
- console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
6850
+ const state = getRunningState();
6851
+ if (!state.running) {
6852
+ const hint = state.daemon ? "mihomo daemon on" : "mihomo start";
6853
+ console.error(`\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (${hint})`);
6479
6854
  process.exit(1);
6480
6855
  }
6481
6856
  }
@@ -6532,21 +6907,24 @@ async function cmdClean(args) {
6532
6907
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6533
6908
  console.log("");
6534
6909
  console.log("\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548...");
6535
- if (isDaemonEnabled()) {
6536
- const configInfo = prepareConfigForStart("mixed", activeSub.name);
6537
- try {
6538
- restartDaemon();
6539
- } catch (e) {
6540
- console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6541
- process.exit(1);
6910
+ const mode = getRuntimeMode();
6911
+ const daemonManaged = isDaemonEnabled();
6912
+ try {
6913
+ if (!daemonManaged) {
6914
+ if (hasRootResidue()) {
6915
+ console.error(`${colors.red("\u9519\u8BEF:")} \u4E3B\u5B9E\u4F8B\u4EE5 root \u8FD0\u884C\uFF08TUN\uFF09\uFF0C\u505C\u6B62\u5B83\u9700\u8981 sudo`);
6916
+ console.error("\u8BF7\u6539\u7528 mihomo sub clean\uFF08\u9694\u79BB\u5B9E\u4F8B\u6D4B\u901F\uFF0C\u65E0\u9700\u505C\u6B62\u4E3B\u5B9E\u4F8B\uFF09");
6917
+ process.exit(1);
6918
+ }
6919
+ handleStopResult(stop());
6542
6920
  }
6543
- console.log(`${colors.green("\u5DF2\u91CD\u542F (\u4FDD\u6D3B)")} \xB7 ${formatProxySummary(configInfo)}`);
6544
- } else {
6545
- const currentMode = getConfigInfo()?.tun ? "tun" : "mixed";
6546
- handleStopResult(stop());
6547
- const configInfo = prepareConfigForStart(currentMode, activeSub.name);
6548
- const startResult = await start(currentMode);
6549
- console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${startResult.pid}) \xB7 ${formatProxySummary(configInfo)}`);
6921
+ const configInfo = prepareConfigForStart(mode, activeSub.name);
6922
+ const pid = await launchOrRestart(mode);
6923
+ const label = daemonManaged ? "\u5DF2\u91CD\u542F (\u4FDD\u6D3B)" : "\u5DF2\u91CD\u542F";
6924
+ console.log(`${colors.green(label)}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
6925
+ } catch (e) {
6926
+ console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6927
+ process.exit(1);
6550
6928
  }
6551
6929
  }
6552
6930
  }
@@ -6562,6 +6940,10 @@ function cmdUI(args) {
6562
6940
  const url = UI_URLS[uiName];
6563
6941
  console.log(`\u6253\u5F00 Web UI: ${uiName}`);
6564
6942
  console.log(`\u5730\u5740: ${url}`);
6943
+ const secret = readSettings().controller_secret;
6944
+ if (secret) {
6945
+ console.log("\u5DF2\u914D\u7F6E\u8BBF\u95EE\u5BC6\u94A5\uFF08UI \u8FDE\u63A5 127.0.0.1:9090 \u65F6\u9700\u8F93\u5165\uFF0C\u5BC6\u94A5\u89C1 settings.json\uFF09");
6946
+ }
6565
6947
  const success = openUrl(url);
6566
6948
  if (!success) {
6567
6949
  console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
@@ -6583,11 +6965,16 @@ async function cmdUpdate() {
6583
6965
  if (code === 0) {
6584
6966
  resolve();
6585
6967
  } else {
6968
+ 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");
6586
6969
  process.exit(code || 1);
6587
6970
  }
6588
6971
  });
6589
6972
  npm.on("error", (e) => {
6590
- console.error(`\u6267\u884C\u5931\u8D25: ${e.message}`);
6973
+ if (e.message.includes("EACCES") || e.message.includes("permission")) {
6974
+ console.error("\u6743\u9650\u4E0D\u8DB3\uFF0C\u53EF\u5C1D\u8BD5: sudo npm install -g mihomo-cli");
6975
+ } else {
6976
+ console.error(`\u6267\u884C\u5931\u8D25: ${e.message}`);
6977
+ }
6591
6978
  process.exit(1);
6592
6979
  });
6593
6980
  });
@@ -6607,9 +6994,204 @@ async function cmdUpdate() {
6607
6994
  }
6608
6995
  }
6609
6996
 
6997
+ // src/commands/registry.ts
6998
+ var COMMANDS = [
6999
+ // === 控制 ===
7000
+ {
7001
+ name: "start",
7002
+ aliases: ["up"],
7003
+ handler: cmdStart,
7004
+ group: "control",
7005
+ usage: ["start [tun|mixed] [-s] [-u ms] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)", " [-r N] [-t ms] [-j N] [--no-clean]"]
7006
+ },
7007
+ {
7008
+ name: "tun",
7009
+ aliases: [],
7010
+ handler: cmdStart,
7011
+ rewrite: (args) => ["start", "tun", ...args.slice(1)],
7012
+ group: "control",
7013
+ usage: []
7014
+ },
7015
+ {
7016
+ name: "stop",
7017
+ aliases: ["down"],
7018
+ handler: cmdStop,
7019
+ group: "control",
7020
+ usage: ["stop \u505C\u6B62\u4EE3\u7406"]
7021
+ },
7022
+ {
7023
+ name: "status",
7024
+ aliases: [],
7025
+ handler: printStatus,
7026
+ group: "control",
7027
+ usage: ["status \u67E5\u770B\u72B6\u6001"]
7028
+ },
7029
+ // === 界面 ===
7030
+ {
7031
+ name: "ui",
7032
+ aliases: [],
7033
+ handler: cmdUI,
7034
+ group: "interface",
7035
+ usage: ["ui [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)"]
7036
+ },
7037
+ {
7038
+ name: "log",
7039
+ aliases: [],
7040
+ handler: cmdLog,
7041
+ group: "interface",
7042
+ usage: ["log [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09"]
7043
+ },
7044
+ {
7045
+ name: "logs",
7046
+ aliases: [],
7047
+ handler: cmdLogs,
7048
+ group: "interface",
7049
+ usage: ["logs [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09"]
7050
+ },
7051
+ // === 订阅 ===
7052
+ {
7053
+ name: "subscription",
7054
+ aliases: ["sub", "subscriptions"],
7055
+ handler: cmdSubscription,
7056
+ group: "subscription",
7057
+ usage: [
7058
+ "subscription \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09",
7059
+ "subscription use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605",
7060
+ "subscription add <url> [name] \u6DFB\u52A0\u8BA2\u9605",
7061
+ "subscription update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09",
7062
+ "subscription remove <name> \u5220\u9664\u8BA2\u9605",
7063
+ "subscription web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762",
7064
+ "subscription test [name] \u6D4B\u8BD5\u8282\u70B9\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u65E0\u9700\u8FD0\u884C\uFF09",
7065
+ "subscription clean [name] \u6D4B\u901F\u6E05\u7406\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u4E0D\u52A8\u4E3B\u5B9E\u4F8B\uFF09"
7066
+ ]
7067
+ },
7068
+ {
7069
+ name: "use",
7070
+ aliases: [],
7071
+ handler: cmdSubscription,
7072
+ rewrite: (args) => ["sub", "use", ...args.slice(1)],
7073
+ group: "subscription",
7074
+ usage: []
7075
+ },
7076
+ {
7077
+ name: "test",
7078
+ aliases: [],
7079
+ handler: cmdTest,
7080
+ group: "subscription",
7081
+ usage: ["test [-t ms] [-j N] \u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\uFF08\u7ECF\u8FD0\u884C\u4E2D\u7684\u4E3B\u5B9E\u4F8B\uFF09"]
7082
+ },
7083
+ {
7084
+ name: "clean",
7085
+ aliases: [],
7086
+ handler: cmdClean,
7087
+ group: "subscription",
7088
+ usage: ["clean [-t ms] [-j N] [-r N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u91CD\u542F\uFF08\u7ECF\u4E3B\u5B9E\u4F8B\uFF09"]
7089
+ },
7090
+ // === 配置 ===
7091
+ {
7092
+ name: "overwrite",
7093
+ aliases: ["ow"],
7094
+ handler: cmdOverwrite,
7095
+ group: "config",
7096
+ usage: ["overwrite \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09", "overwrite on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E"]
7097
+ },
7098
+ {
7099
+ name: "on",
7100
+ aliases: [],
7101
+ handler: cmdOverwrite,
7102
+ rewrite: () => ["ow", "on"],
7103
+ group: "config",
7104
+ usage: []
7105
+ },
7106
+ {
7107
+ name: "off",
7108
+ aliases: [],
7109
+ handler: cmdOverwrite,
7110
+ rewrite: () => ["ow", "off"],
7111
+ group: "config",
7112
+ usage: []
7113
+ },
7114
+ {
7115
+ name: "directory",
7116
+ aliases: ["dir", "dirs", "directories"],
7117
+ handler: cmdDirectory,
7118
+ group: "config",
7119
+ usage: ["directory \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09", "directory open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|..."]
7120
+ },
7121
+ {
7122
+ name: "open",
7123
+ aliases: [],
7124
+ handler: cmdDirectory,
7125
+ rewrite: (args) => ["dir", "open", ...args.slice(1)],
7126
+ group: "config",
7127
+ usage: []
7128
+ },
7129
+ // === 系统 ===
7130
+ {
7131
+ name: "kernel",
7132
+ aliases: [],
7133
+ handler: cmdKernel,
7134
+ group: "system",
7135
+ usage: ["kernel [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09"]
7136
+ },
7137
+ {
7138
+ name: "daemon",
7139
+ aliases: [],
7140
+ handler: cmdDaemon,
7141
+ group: "system",
7142
+ usage: ["daemon on|off \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF08\u4EC5 Mixed\uFF0C\u9700\u7BA1\u7406\u5458\u5BC6\u7801\uFF09", "daemon status \u67E5\u770B\u4FDD\u6D3B\u72B6\u6001"]
7143
+ },
7144
+ {
7145
+ name: "update",
7146
+ aliases: ["upd", "upgrade"],
7147
+ handler: cmdUpdate,
7148
+ group: "system",
7149
+ usage: ["update \u66F4\u65B0 mihomo-cli (npm install -g)"]
7150
+ },
7151
+ {
7152
+ name: "reset",
7153
+ aliases: [],
7154
+ handler: cmdReset,
7155
+ group: "system",
7156
+ usage: ["reset [\u76EE\u6807...] [--full] [-y] \u91CD\u7F6E: \u7559\u7A7A\u4FDD\u7559\u8BBE\u7F6E/\u5185\u6838/\u8986\u5199, \u6307\u5B9A\u76EE\u6807\u5220\u5BF9\u5E94\u9879, --full \u5220\u5168\u90E8, -y \u8DF3\u8FC7\u786E\u8BA4"]
7157
+ },
7158
+ // === meta(不在分组清单展示,help 末尾单列) ===
7159
+ {
7160
+ name: "help",
7161
+ aliases: ["-h", "--help"],
7162
+ handler: () => printHelp(COMMANDS),
7163
+ group: "meta",
7164
+ usage: ["help, -h \u663E\u793A\u5E2E\u52A9"]
7165
+ },
7166
+ {
7167
+ name: "version",
7168
+ aliases: ["-v", "--version"],
7169
+ handler: printVersion,
7170
+ group: "meta",
7171
+ usage: ["version, -v \u663E\u793A\u7248\u672C"]
7172
+ }
7173
+ ];
7174
+ var COMMAND_INDEX = (() => {
7175
+ const index = /* @__PURE__ */ new Map();
7176
+ for (const cmd of COMMANDS) {
7177
+ for (const token of [cmd.name, ...cmd.aliases]) {
7178
+ if (index.has(token)) {
7179
+ throw new Error(`\u547D\u4EE4\u6CE8\u518C\u8868\u5B58\u5728\u91CD\u590D token: "${token}"\uFF08${index.get(token)?.name} \u4E0E ${cmd.name}\uFF09`);
7180
+ }
7181
+ index.set(token, cmd);
7182
+ }
7183
+ }
7184
+ return index;
7185
+ })();
7186
+ function findCommand(token) {
7187
+ return COMMAND_INDEX.get(token);
7188
+ }
7189
+
6610
7190
  // src/index.ts
6611
7191
  process.on("SIGINT", () => {
6612
- console.log("\n\u6B63\u5728\u9000\u51FA...");
7192
+ if (!isSilentSigint()) {
7193
+ console.log("\n\u6B63\u5728\u9000\u51FA...");
7194
+ }
6613
7195
  runCleanup();
6614
7196
  process.exit(130);
6615
7197
  });
@@ -6623,12 +7205,14 @@ process.on("uncaughtException", (e) => {
6623
7205
  if (e.stack) {
6624
7206
  console.error(e.stack.split("\n").slice(1).join("\n"));
6625
7207
  }
7208
+ runCleanup();
6626
7209
  process.exit(1);
6627
7210
  });
6628
7211
  process.on("unhandledRejection", (reason) => {
6629
7212
  const msg = reason instanceof Error ? reason.message : String(reason);
6630
7213
  console.error(`
6631
7214
  \u672A\u5904\u7406\u7684 Promise \u62D2\u7EDD: ${msg}`);
7215
+ runCleanup();
6632
7216
  process.exit(1);
6633
7217
  });
6634
7218
  function clearProxyEnv() {
@@ -6648,94 +7232,18 @@ async function main() {
6648
7232
  printShortHelp();
6649
7233
  return;
6650
7234
  }
6651
- const cmd = args[0].toLowerCase();
6652
- if (["help", "-h", "--help"].includes(cmd)) {
6653
- printHelp();
6654
- return;
6655
- }
6656
- if (["version", "-v", "--version"].includes(cmd)) {
6657
- printVersion();
6658
- return;
6659
- }
6660
- switch (cmd) {
6661
- case "up":
6662
- case "start":
6663
- await cmdStart(args);
6664
- break;
6665
- case "tun":
6666
- await cmdStart(["start", "tun", ...args.slice(1)]);
6667
- break;
6668
- case "down":
6669
- case "stop":
6670
- await cmdStop();
6671
- break;
6672
- case "status":
6673
- printStatus();
6674
- break;
6675
- case "log":
6676
- cmdLog(args);
6677
- break;
6678
- case "logs":
6679
- cmdLogs(args);
6680
- break;
6681
- case "open":
6682
- cmdDirectory(["dir", "open", ...args.slice(1)]);
6683
- break;
6684
- case "ui":
6685
- cmdUI(args);
6686
- break;
6687
- case "kernel":
6688
- await cmdKernel(args);
6689
- break;
6690
- case "upd":
6691
- case "update":
6692
- case "upgrade":
6693
- await cmdUpdate();
6694
- break;
6695
- case "use":
6696
- await cmdSubscription(["sub", "use", ...args.slice(1)]);
6697
- break;
6698
- case "sub":
6699
- case "subscription":
6700
- case "subscriptions":
6701
- await cmdSubscription(args);
6702
- break;
6703
- case "dir":
6704
- case "dirs":
6705
- case "directory":
6706
- case "directories":
6707
- cmdDirectory(args);
6708
- break;
6709
- case "reset":
6710
- await cmdReset(args);
6711
- break;
6712
- case "daemon":
6713
- await cmdDaemon(args);
6714
- break;
6715
- case "on":
6716
- await cmdOverwrite(["ow", "on"]);
6717
- break;
6718
- case "off":
6719
- await cmdOverwrite(["ow", "off"]);
6720
- break;
6721
- case "ow":
6722
- case "overwrite":
6723
- await cmdOverwrite(args);
6724
- break;
6725
- case "test":
6726
- await cmdTest(args);
6727
- break;
6728
- case "clean":
6729
- await cmdClean(args);
6730
- break;
6731
- default:
6732
- console.error(`\u672A\u77E5\u547D\u4EE4: ${cmd}`);
6733
- console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');
6734
- process.exit(1);
7235
+ const token = args[0].toLowerCase();
7236
+ const command = findCommand(token);
7237
+ if (!command) {
7238
+ console.error(`\u672A\u77E5\u547D\u4EE4: ${token}`);
7239
+ console.error('\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9');
7240
+ process.exit(1);
6735
7241
  }
7242
+ await command.handler(command.rewrite ? command.rewrite(args) : args);
6736
7243
  }
6737
7244
  main().catch((e) => {
6738
7245
  console.error(`\u9519\u8BEF: ${e.message}`);
7246
+ runCleanup();
6739
7247
  process.exit(1);
6740
7248
  });
6741
7249
  /*! Bundled license information: