mihomo-cli 2.10.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,71 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/paths.ts
4
- import fs from "fs";
5
- import os from "os";
6
- import path from "path";
7
- function getUserDataDir() {
8
- if (process.env.MIHOMO_CLI_DIR) {
9
- return process.env.MIHOMO_CLI_DIR;
10
- }
11
- return path.join(os.homedir(), ".mihomo-cli");
12
- }
13
- var USER_DATA_DIR = getUserDataDir();
14
- var DIRS = {
15
- kernel: path.join(USER_DATA_DIR, "kernel"),
16
- subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
17
- logs: path.join(USER_DATA_DIR, "logs"),
18
- data: path.join(USER_DATA_DIR, "data"),
19
- runtime: path.join(USER_DATA_DIR, "runtime")
20
- };
21
- var PATHS = {
22
- mihomoBinary: path.join(DIRS.kernel, "mihomo"),
23
- settingsFile: path.join(USER_DATA_DIR, "settings.json"),
24
- subscriptionsCacheFile: path.join(DIRS.subscriptions, "cache.json"),
25
- configFile: path.join(DIRS.runtime, "config.yaml"),
26
- logFile: path.join(DIRS.logs, "mihomo.log"),
27
- pidFile: path.join(DIRS.runtime, "pid"),
28
- configStage1Subscription: path.join(DIRS.runtime, "1.subscription.yaml"),
29
- configStage2Overwrite: path.join(DIRS.runtime, "2.overwrite.yaml"),
30
- configStage3System: path.join(DIRS.runtime, "3.system.yaml")
31
- };
32
- var DIRECTORY_TARGETS = {
33
- root: { path: null, label: "\u6839\u76EE\u5F55" },
34
- subs: { path: DIRS.subscriptions, label: "\u8BA2\u9605\u76EE\u5F55" },
35
- logs: { path: DIRS.logs, label: "\u65E5\u5FD7\u76EE\u5F55" },
36
- data: { path: DIRS.data, label: "mihomo \u6570\u636E\u76EE\u5F55" },
37
- runtime: { path: DIRS.runtime, label: "\u8FD0\u884C\u65F6\u76EE\u5F55" },
38
- kernel: { path: DIRS.kernel, label: "\u5185\u6838\u76EE\u5F55" }
39
- };
40
- function ensureDirs() {
41
- for (const dir of Object.values(DIRS)) {
42
- if (!fs.existsSync(dir)) {
43
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
44
- }
45
- }
46
- }
47
- function atomicWriteFileSync(filePath, content, options) {
48
- const tmp = `${filePath}.${process.pid}.tmp`;
49
- try {
50
- fs.writeFileSync(tmp, content, options);
51
- fs.renameSync(tmp, filePath);
52
- } catch (e) {
53
- try {
54
- fs.unlinkSync(tmp);
55
- } catch {
56
- }
57
- throw e;
58
- }
59
- }
60
- function rmrf(dir) {
61
- fs.rmSync(dir, { recursive: true, force: true });
62
- }
63
-
64
- // src/process.ts
65
- import { spawn, spawnSync as spawnSync3 } from "child_process";
66
- import fs5 from "fs";
67
- import path3 from "path";
68
-
69
3
  // src/config.ts
70
4
  import { spawnSync } from "child_process";
71
5
  import fs4 from "fs";
@@ -2989,6 +2923,7 @@ var UI_URLS = {
2989
2923
  dash: "https://metacubex.github.io/metacubexd",
2990
2924
  yacd: "https://yacd.metacubex.one"
2991
2925
  };
2926
+ var LAUNCH_AGENT_LABEL = "com.mihomo-cli.daemon";
2992
2927
  var TUN_CONFIG = {
2993
2928
  tun: {
2994
2929
  enable: true,
@@ -3040,6 +2975,69 @@ var AUTO_CLEAN_THRESHOLD_GITHUB = 50;
3040
2975
  import fs3 from "fs";
3041
2976
  import path2 from "path";
3042
2977
 
2978
+ // src/paths.ts
2979
+ import fs from "fs";
2980
+ import os from "os";
2981
+ import path from "path";
2982
+ function getUserDataDir() {
2983
+ if (process.env.MIHOMO_CLI_DIR) {
2984
+ return process.env.MIHOMO_CLI_DIR;
2985
+ }
2986
+ return path.join(os.homedir(), ".mihomo-cli");
2987
+ }
2988
+ var USER_DATA_DIR = getUserDataDir();
2989
+ var DIRS = {
2990
+ kernel: path.join(USER_DATA_DIR, "kernel"),
2991
+ subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
2992
+ logs: path.join(USER_DATA_DIR, "logs"),
2993
+ data: path.join(USER_DATA_DIR, "data"),
2994
+ runtime: path.join(USER_DATA_DIR, "runtime")
2995
+ };
2996
+ var PATHS = {
2997
+ mihomoBinary: path.join(DIRS.kernel, "mihomo"),
2998
+ settingsFile: path.join(USER_DATA_DIR, "settings.json"),
2999
+ subscriptionsCacheFile: path.join(DIRS.subscriptions, "cache.json"),
3000
+ configFile: path.join(DIRS.runtime, "config.yaml"),
3001
+ logFile: path.join(DIRS.logs, "mihomo.log"),
3002
+ pidFile: path.join(DIRS.runtime, "pid"),
3003
+ configStage1Subscription: path.join(DIRS.runtime, "1.subscription.yaml"),
3004
+ configStage2Overwrite: path.join(DIRS.runtime, "2.overwrite.yaml"),
3005
+ 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`)
3008
+ };
3009
+ var DIRECTORY_TARGETS = {
3010
+ root: { path: null, label: "\u6839\u76EE\u5F55" },
3011
+ subs: { path: DIRS.subscriptions, label: "\u8BA2\u9605\u76EE\u5F55" },
3012
+ logs: { path: DIRS.logs, label: "\u65E5\u5FD7\u76EE\u5F55" },
3013
+ data: { path: DIRS.data, label: "mihomo \u6570\u636E\u76EE\u5F55" },
3014
+ runtime: { path: DIRS.runtime, label: "\u8FD0\u884C\u65F6\u76EE\u5F55" },
3015
+ kernel: { path: DIRS.kernel, label: "\u5185\u6838\u76EE\u5F55" }
3016
+ };
3017
+ function ensureDirs() {
3018
+ for (const dir of Object.values(DIRS)) {
3019
+ if (!fs.existsSync(dir)) {
3020
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
3021
+ }
3022
+ }
3023
+ }
3024
+ function atomicWriteFileSync(filePath, content, options) {
3025
+ const tmp = `${filePath}.${process.pid}.tmp`;
3026
+ try {
3027
+ fs.writeFileSync(tmp, content, options);
3028
+ fs.renameSync(tmp, filePath);
3029
+ } catch (e) {
3030
+ try {
3031
+ fs.unlinkSync(tmp);
3032
+ } catch {
3033
+ }
3034
+ throw e;
3035
+ }
3036
+ }
3037
+ function rmrf(dir) {
3038
+ fs.rmSync(dir, { recursive: true, force: true });
3039
+ }
3040
+
3043
3041
  // src/settings.ts
3044
3042
  import fs2 from "fs";
3045
3043
  var settingsCache = null;
@@ -3581,6 +3579,16 @@ function clearKernelVersionCache() {
3581
3579
  kernelVersionCached = false;
3582
3580
  }
3583
3581
 
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
+
3584
3592
  // src/utils.ts
3585
3593
  import { spawnSync as spawnSync2 } from "child_process";
3586
3594
  import { createRequire } from "module";
@@ -4271,1021 +4279,1234 @@ function viewLogWithTail(logPath, options) {
4271
4279
  });
4272
4280
  }
4273
4281
 
4274
- // src/commands/directory.ts
4275
- function cmdDirectory(args) {
4276
- const action = args?.[1];
4277
- if (action === "open") {
4278
- const target = args[2];
4279
- if (!target || target === "root") {
4280
- console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
4281
- const success = openUrl(USER_DATA_DIR);
4282
- if (!success) {
4283
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
4284
- }
4285
- return;
4286
- }
4287
- const key = target.toLowerCase();
4288
- const targetInfo = Object.hasOwn(DIRECTORY_TARGETS, key) ? DIRECTORY_TARGETS[key] : void 0;
4289
- if (targetInfo) {
4290
- const targetPath = targetInfo.path || USER_DATA_DIR;
4291
- console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
4292
- const success = openUrl(targetPath);
4293
- if (!success) {
4294
- console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${targetPath}`);
4295
- }
4296
- return;
4282
+ // 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");
4296
+ }
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
+ }
4309
+ function escapeXml(s) {
4310
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4311
+ }
4312
+ function buildPlist() {
4313
+ const programArguments = [PATHS.mihomoBinary, "-d", DIRS.data, "-f", PATHS.configFile];
4314
+ const argsXml = programArguments.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
4315
+ return `<?xml version="1.0" encoding="UTF-8"?>
4316
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4317
+ <plist version="1.0">
4318
+ <dict>
4319
+ <key>Label</key>
4320
+ <string>${escapeXml(LAUNCH_AGENT_LABEL)}</string>
4321
+ <key>ProgramArguments</key>
4322
+ <array>
4323
+ ${argsXml}
4324
+ </array>
4325
+ <key>RunAtLoad</key>
4326
+ <true/>
4327
+ <key>KeepAlive</key>
4328
+ <true/>
4329
+ <key>StandardOutPath</key>
4330
+ <string>${escapeXml(PATHS.logFile)}</string>
4331
+ <key>StandardErrorPath</key>
4332
+ <string>${escapeXml(PATHS.logFile)}</string>
4333
+ <key>WorkingDirectory</key>
4334
+ <string>/tmp</string>
4335
+ </dict>
4336
+ </plist>
4337
+ `;
4338
+ }
4339
+ function isDaemonEnabled() {
4340
+ return fs6.existsSync(PATHS.launchAgentPlist);
4341
+ }
4342
+ function getDaemonStatus() {
4343
+ if (!isDaemonEnabled()) {
4344
+ return { enabled: false, loaded: false, pid: null };
4345
+ }
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 };
4358
+ }
4359
+ function isDaemonRunning(status) {
4360
+ return status.loaded && status.pid !== null;
4361
+ }
4362
+ function enableDaemon() {
4363
+ if (!fs6.existsSync(PATHS.mihomoBinary)) {
4364
+ throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
4365
+ }
4366
+ if (!fs6.existsSync(PATHS.configFile)) {
4367
+ throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4368
+ }
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)]);
4377
+ try {
4378
+ fs6.unlinkSync(PATHS.launchAgentPlist);
4379
+ } catch {
4297
4380
  }
4298
- console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`);
4299
- console.log("");
4300
- console.log("\u53EF\u7528\u76EE\u6807:");
4301
- console.log(" root (\u9ED8\u8BA4) \u6839\u76EE\u5F55");
4302
- for (const [key2, val] of Object.entries(DIRECTORY_TARGETS)) {
4303
- if (key2 !== "root") {
4304
- console.log(` ${key2.padEnd(14)}${val.label}`);
4305
- }
4381
+ throw launchctlError("bootstrap", result);
4382
+ }
4383
+ }
4384
+ function disableDaemon() {
4385
+ const uid = process.getuid?.();
4386
+ if (uid !== void 0) {
4387
+ launchctl(["bootout", serviceTarget(uid)]);
4388
+ }
4389
+ try {
4390
+ fs6.unlinkSync(PATHS.launchAgentPlist);
4391
+ } catch {
4392
+ }
4393
+ }
4394
+ function restartDaemon() {
4395
+ const uid = getUid();
4396
+ if (!fs6.existsSync(PATHS.launchAgentPlist)) {
4397
+ throw new Error("\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u6CD5\u91CD\u542F");
4398
+ }
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);
4306
4403
  }
4307
- console.log("");
4308
- process.exit(1);
4404
+ return;
4405
+ }
4406
+ const result = launchctl(["bootstrap", guiDomain(uid), PATHS.launchAgentPlist]);
4407
+ if (result.status !== 0) {
4408
+ throw launchctlError("bootstrap", result);
4309
4409
  }
4310
- console.log("");
4311
- console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
4312
- console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
4313
- console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
4314
- console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
4315
- console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
4316
- console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
4317
- console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
4318
- console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
4319
- console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
4320
- console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
4321
- console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
4322
- console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
4323
- console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
4324
- console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
4325
- console.log("");
4326
- console.log("\u6253\u5F00\u76EE\u5F55:");
4327
- console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
4328
- console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
4329
- console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
4330
- console.log(" mihomo dir open data \u6253\u5F00 mihomo \u6570\u636E\u76EE\u5F55");
4331
- console.log(" mihomo dir open runtime \u6253\u5F00\u8FD0\u884C\u65F6\u76EE\u5F55");
4332
- console.log(" mihomo dir open kernel \u6253\u5F00\u5185\u6838\u76EE\u5F55");
4333
- console.log("");
4334
- console.log("\u73AF\u5883\u53D8\u91CF:");
4335
- console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
4336
- console.log("");
4337
4410
  }
4338
4411
 
4339
- // src/commands/help.ts
4340
- function printShortHelp() {
4341
- console.log(`
4342
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))} (mihomo help \u67E5\u770B\u5B8C\u6574\u5E2E\u52A9)
4343
- `);
4344
- console.log(
4345
- `\u5E38\u7528\u547D\u4EE4:
4346
- ${colors.bold("start")} [tun|mixed] \u542F\u52A8/\u5207\u6362\u4EE3\u7406
4347
- ${colors.bold("sub")} [use|update] \u8BA2\u9605\u7BA1\u7406
4348
- ${colors.bold("ow")} [on|off] \u8986\u5199\u914D\u7F6E
4349
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI
4350
- `
4351
- );
4412
+ // src/subscription.ts
4413
+ function isGithubUrl(url) {
4414
+ return /github\.com|raw\.githubusercontent\.com/i.test(url);
4352
4415
  }
4353
- function printHelp() {
4354
- console.log(
4355
- `
4356
- ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}
4357
-
4358
- \u547D\u4EE4\u522B\u540D: mihomo, mhm, mh
4359
-
4360
- \u7528\u6CD5:
4361
- mihomo <\u547D\u4EE4> [\u9009\u9879]
4362
-
4363
- ${colors.cyan("\u63A7\u5236:")}
4364
- ${colors.bold("start")} [tun|mixed] [-s] [-u ms] \u542F\u52A8/\u5207\u6362\u4EE3\u7406 (\u9ED8\u8BA4 mixed)
4365
- [-r N] [-t ms] [-j N]
4366
- ${colors.bold("stop")} \u505C\u6B62\u4EE3\u7406
4367
- ${colors.bold("status")} \u67E5\u770B\u72B6\u6001
4368
-
4369
- ${colors.cyan("\u754C\u9762:")}
4370
- ${colors.bold("ui")} [zash|dash|yacd] \u6253\u5F00 Web UI (\u9ED8\u8BA4 zash)
4371
- ${colors.bold("log")} [-o] \u5B9E\u65F6\u65E5\u5FD7\uFF08-o \u6253\u5F00\u6587\u4EF6\uFF09
4372
- ${colors.bold("logs")} [\u7F16\u53F7] [-n N] [-o] \u65E5\u5FD7\u5217\u8868\uFF080=\u5F53\u524D\uFF0C1+=\u5F52\u6863\uFF09
4373
-
4374
- ${colors.cyan("\u8BA2\u9605:")}
4375
- ${colors.bold("subscription")} \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09
4376
- ${colors.bold("subscription")} use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605
4377
- ${colors.bold("subscription")} add <url> [name] \u6DFB\u52A0\u8BA2\u9605
4378
- ${colors.bold("subscription")} update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09
4379
- ${colors.bold("subscription")} remove <name> \u5220\u9664\u8BA2\u9605
4380
- ${colors.bold("subscription")} web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762
4381
- ${colors.bold("subscription")} test [name] \u6D4B\u8BD5\u8282\u70B9\u8FDE\u901A\u6027
4382
- ${colors.bold("subscription")} clean [name] \u6D4B\u901F\u5E76\u6E05\u7406\u5931\u8D25\u8282\u70B9
4383
- ${colors.bold("test")} [-t ms] [-j N] \u5FEB\u901F\u6D4B\u8BD5\u5F53\u524D\u8282\u70B9\u8FDE\u901A\u6027
4384
- ${colors.bold("clean")} [-t ms] [-j N] [-r N] \u6E05\u7406\u5931\u8D25\u8282\u70B9\u5E76\u81EA\u52A8\u91CD\u542F
4385
-
4386
- ${colors.cyan("\u914D\u7F6E:")}
4387
- ${colors.bold("overwrite")} \u67E5\u770B\u8986\u5199\u72B6\u6001\uFF08\u522B\u540D ow\uFF09
4388
- ${colors.bold("overwrite")} on|off \u542F\u7528/\u7981\u7528\u8986\u5199\u914D\u7F6E
4389
- ${colors.bold("directory")} \u663E\u793A\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E\uFF08\u522B\u540D dir\uFF09
4390
- ${colors.bold("directory")} open [target] \u6253\u5F00\u76EE\u5F55: root|subs|logs|runtime|...
4391
-
4392
- ${colors.cyan("\u7CFB\u7EDF:")}
4393
- ${colors.bold("kernel")} [--mirror [\u955C\u50CF]] \u66F4\u65B0\u5185\u6838\uFF08\u9ED8\u8BA4\u76F4\u8FDE\uFF0C--mirror \u4F7F\u7528 v6\uFF09
4394
- ${colors.bold("update")} \u66F4\u65B0 mihomo-cli (npm install -g)
4395
- ${colors.bold("reset")} [\u76EE\u6807...] [--full] \u91CD\u7F6E: \u7559\u7A7A\u4FDD\u7559\u8BBE\u7F6E/\u5185\u6838/\u8986\u5199, \u6307\u5B9A\u76EE\u6807\u5220\u5BF9\u5E94\u9879, --full \u5220\u5168\u90E8
4396
- ${colors.bold("help")}, -h \u663E\u793A\u5E2E\u52A9
4397
- ${colors.bold("version")}, -v \u663E\u793A\u7248\u672C
4398
-
4399
- ${colors.cyan("\u793A\u4F8B:")}
4400
- mihomo start # \u542F\u52A8/\u91CD\u542F Mixed \u6A21\u5F0F
4401
- mihomo start tun # \u5207\u6362\u5230 TUN \u900F\u660E\u4EE3\u7406\u6A21\u5F0F
4402
- mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605
4403
- mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)
4404
- mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)
4405
- mihomo ui # \u6253\u5F00 Web UI
4406
-
4407
- ${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}
4408
- mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)
4409
- tun \u900F\u660E\u4EE3\u7406\uFF0C\u5168\u5C40\u81EA\u52A8\u8DEF\u7531\uFF0C\u9700\u8981 sudo
4410
-
4411
- ${colors.cyan("\u6570\u636E\u76EE\u5F55:")}
4412
- \u73AF\u5883\u53D8\u91CF MIHOMO_CLI_DIR \u53EF\u81EA\u5B9A\u4E49\u4F4D\u7F6E
4413
- \u9ED8\u8BA4: ${USER_DATA_DIR}
4414
- `
4415
- );
4416
- }
4417
- function printVersion() {
4418
- const kv = getKernelVersion() || "\u672A\u5B89\u88C5";
4419
- console.log(colors.cyan(colors.bold(`mihomo-cli v${VERSION}`)));
4420
- console.log(`${colors.gray("\u5185\u6838: ")}${kv}`);
4421
- console.log(`${colors.gray("\u6570\u636E\u76EE\u5F55: ")}${USER_DATA_DIR}`);
4416
+ function getDefaultUpdateInterval(url) {
4417
+ return isGithubUrl(url) ? DEFAULT_UPDATE_INTERVAL_HOURS_GITHUB : DEFAULT_UPDATE_INTERVAL_HOURS;
4422
4418
  }
4423
-
4424
- // src/kernel.ts
4425
- import { spawnSync as spawnSync4 } from "child_process";
4426
- import fs6 from "fs";
4427
- import path4 from "path";
4428
-
4429
- // node_modules/compare-versions/lib/esm/utils.js
4430
- var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
4431
- var validateAndParse = (version) => {
4432
- if (typeof version !== "string") {
4433
- throw new TypeError("Invalid argument expected string");
4434
- }
4435
- const match = version.match(semver);
4436
- if (!match) {
4437
- throw new Error(`Invalid argument not valid semver ('${version}' received)`);
4438
- }
4439
- match.shift();
4440
- return match;
4441
- };
4442
- var isWildcard = (s) => s === "*" || s === "x" || s === "X";
4443
- var tryParse = (v) => {
4444
- const n = parseInt(v, 10);
4445
- return isNaN(n) ? v : n;
4446
- };
4447
- var forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b];
4448
- var compareStrings = (a, b) => {
4449
- if (isWildcard(a) || isWildcard(b))
4450
- return 0;
4451
- const [ap, bp] = forceType(tryParse(a), tryParse(b));
4452
- if (ap > bp)
4453
- return 1;
4454
- if (ap < bp)
4455
- return -1;
4456
- return 0;
4457
- };
4458
- var compareSegments = (a, b) => {
4459
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
4460
- const r = compareStrings(a[i] || "0", b[i] || "0");
4461
- if (r !== 0)
4462
- return r;
4463
- }
4464
- return 0;
4465
- };
4466
-
4467
- // node_modules/compare-versions/lib/esm/compareVersions.js
4468
- var compareVersions = (v1, v2) => {
4469
- const n1 = validateAndParse(v1);
4470
- const n2 = validateAndParse(v2);
4471
- const p1 = n1.pop();
4472
- const p2 = n2.pop();
4473
- const r = compareSegments(n1, n2);
4474
- if (r !== 0)
4475
- return r;
4476
- if (p1 && p2) {
4477
- return compareSegments(p1.split("."), p2.split("."));
4478
- } else if (p1 || p2) {
4479
- return p1 ? -1 : 1;
4480
- }
4481
- return 0;
4482
- };
4483
-
4484
- // src/kernel.ts
4485
- var GITHUB_REPO = "MetaCubeX/mihomo";
4486
- var KERNEL_HTTP_TIMEOUT = 12e4;
4487
- var KERNEL_DOWNLOAD_TIMEOUT = 18e4;
4488
- var HTTP_CLIENT = createHttpClient({ timeout: KERNEL_HTTP_TIMEOUT });
4489
- function withMirror(url, mirror) {
4490
- if (mirror && (url.startsWith("https://github.com/") || url.startsWith("https://api.github.com/"))) {
4491
- return mirror + url;
4492
- }
4493
- return url;
4419
+ function resolveUpdateInterval(url, cachedInterval) {
4420
+ return cachedInterval && cachedInterval > 0 ? cachedInterval : getDefaultUpdateInterval(url);
4494
4421
  }
4495
- function getArch() {
4496
- const arch = process.arch;
4497
- if (arch === "arm64") return "arm64";
4498
- if (arch === "x64") return "amd64";
4499
- return arch;
4422
+ var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, schema: CORE_SCHEMA };
4423
+ var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
4424
+ function isMultiUrl(url) {
4425
+ return url.includes(",");
4500
4426
  }
4501
- function findMatchingAsset(assets, platform, arch) {
4502
- const prefix = `mihomo-${platform}-${arch}`;
4503
- const matchingAssets = assets.filter(
4504
- (a) => a.name.startsWith(prefix) && a.name.endsWith(".gz") || a.name.startsWith(`${prefix}-`) && a.name.endsWith(".gz")
4505
- );
4506
- if (matchingAssets.length === 0) return null;
4507
- if (matchingAssets.length === 1) return matchingAssets[0];
4508
- const standardAsset = matchingAssets.find((a) => {
4509
- const nameWithoutGz = a.name.slice(0, -3);
4510
- const parts = nameWithoutGz.split("-");
4511
- const lastPart = parts[parts.length - 1];
4512
- return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go");
4513
- });
4514
- return standardAsset || matchingAssets[0];
4427
+ function splitUrls(url) {
4428
+ return url.split(",").map((u) => u.trim()).filter(Boolean);
4515
4429
  }
4516
- async function getLatestRelease(repo, mirror) {
4517
- const url = withMirror(`https://api.github.com/repos/${repo}/releases`, mirror);
4518
- const response = await HTTP_CLIENT.get(url, { responseType: "json" });
4519
- const releases = response.data;
4520
- if (!Array.isArray(releases) || releases.length === 0) {
4521
- throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7248\u672C\u4FE1\u606F");
4430
+ function loadSubscriptionConfig(subName) {
4431
+ const rawContent = readSubscriptionRawConfig(subName);
4432
+ if (!rawContent) {
4433
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4522
4434
  }
4523
- const stableReleases = releases.filter(
4524
- (r) => !r.prerelease && !r.tag_name.toLowerCase().includes("alpha") && !r.tag_name.toLowerCase().includes("beta") && !r.tag_name.toLowerCase().includes("prerelease")
4525
- );
4526
- return stableReleases.length > 0 ? stableReleases[0] : releases[0];
4435
+ const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4436
+ return {
4437
+ raw,
4438
+ proxies: raw.proxies || [],
4439
+ proxyGroups: raw["proxy-groups"] || []
4440
+ };
4527
4441
  }
4528
- async function checkUpdate(mirror) {
4529
- const currentVersion = getKernelVersion();
4530
- const latest = await getLatestRelease(GITHUB_REPO, mirror);
4531
- const latestVersion = latest.tag_name;
4532
- let needsUpdate = false;
4533
- const currentDisplay = currentVersion || "\u672A\u5B89\u88C5";
4534
- if (!currentVersion) {
4535
- needsUpdate = true;
4536
- } else {
4537
- try {
4538
- needsUpdate = compareVersions(latestVersion.replace(/^v/, ""), currentVersion.replace(/^v/, "")) > 0;
4539
- } catch {
4540
- needsUpdate = latestVersion !== currentVersion;
4442
+ function saveSubscriptionConfig(subName, parsed) {
4443
+ normalizeProxyNamesBeforeSave(parsed);
4444
+ parsed.raw.proxies = parsed.proxies;
4445
+ parsed.raw["proxy-groups"] = parsed.proxyGroups;
4446
+ saveSubscriptionRawConfig(subName, dump(parsed.raw, YAML_DUMP_OPTS));
4447
+ }
4448
+ function parseUserInfo(header) {
4449
+ if (!header) return null;
4450
+ const info = {};
4451
+ const parts = header.split(";").map((p) => p.trim());
4452
+ for (const part of parts) {
4453
+ const [key, val] = part.split("=").map((s) => s.trim());
4454
+ if (key && val !== void 0) {
4455
+ const numVal = parseFloat(val);
4456
+ info[key] = Number.isNaN(numVal) ? 0 : numVal;
4541
4457
  }
4542
4458
  }
4459
+ return info;
4460
+ }
4461
+ function parsePositiveInterval(header) {
4462
+ if (!header) return null;
4463
+ const n = parseInt(header, 10);
4464
+ return Number.isFinite(n) && n > 0 ? n : null;
4465
+ }
4466
+ function parseUsernameFromContentDisposition(header) {
4467
+ if (!header) return null;
4468
+ const match = header.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
4469
+ if (!match) return null;
4470
+ const filename = match[1];
4471
+ const parts = filename.split("/");
4472
+ return parts[parts.length - 1] || null;
4473
+ }
4474
+ function extractSubscriptionMeta(headers) {
4543
4475
  return {
4544
- current: currentDisplay,
4545
- latest: latestVersion,
4546
- needsUpdate,
4547
- assets: latest.assets,
4548
- release: latest
4476
+ userInfo: parseUserInfo(headers?.get("subscription-userinfo") ?? null),
4477
+ updateInterval: parsePositiveInterval(headers?.get("profile-update-interval")),
4478
+ webPageUrl: headers?.get("profile-web-page-url") || null,
4479
+ username: parseUsernameFromContentDisposition(headers?.get("content-disposition") ?? null)
4549
4480
  };
4550
4481
  }
4551
- function findBinaryInDir(dir, maxDepth = 4) {
4552
- if (maxDepth <= 0) return null;
4553
- const files = fs6.readdirSync(dir);
4554
- for (const f of files) {
4555
- const fullPath = path4.join(dir, f);
4556
- const stat = fs6.statSync(fullPath);
4557
- if (stat.isDirectory()) {
4558
- const found = findBinaryInDir(fullPath, maxDepth - 1);
4559
- if (found) return found;
4560
- continue;
4561
- }
4562
- if (f === "mihomo") return fullPath;
4563
- if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
4482
+ function saveSubscriptionMeta(subName, meta) {
4483
+ const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4484
+ if (meta.userInfo) {
4485
+ cacheData.upload = meta.userInfo.upload;
4486
+ cacheData.download = meta.userInfo.download;
4487
+ cacheData.total = meta.userInfo.total;
4488
+ cacheData.expire = meta.userInfo.expire;
4564
4489
  }
4565
- return null;
4490
+ if (meta.updateInterval) cacheData.update_interval = meta.updateInterval;
4491
+ if (meta.webPageUrl) cacheData.web_page_url = meta.webPageUrl;
4492
+ if (meta.username) cacheData.username = meta.username;
4493
+ saveSubscriptionCache(subName, cacheData);
4566
4494
  }
4567
- async function downloadKernel(progressCallback, mirror, releaseInfo) {
4568
- ensureDirs();
4569
- const latest = releaseInfo || await getLatestRelease(GITHUB_REPO, mirror);
4570
- const arch = getArch();
4571
- const platform = process.platform;
4572
- const asset = findMatchingAsset(latest.assets, platform, arch);
4573
- if (!asset) {
4574
- const available = latest.assets.map((a) => a.name).join(", ");
4575
- let hint = "";
4576
- if (available) hint = `
4577
- \u53EF\u7528\u7248\u672C: ${available}`;
4578
- throw new Error(`\u672A\u627E\u5230\u5339\u914D\u7684\u5185\u6838\u6587\u4EF6
4579
- \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
4580
- }
4581
- const downloadUrl = withMirror(asset.browser_download_url, mirror);
4582
- const tempPath = path4.join(DIRS.kernel, asset.name);
4583
- const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
4584
- if (mirror && progressCallback) {
4585
- progressCallback("\u63D0\u793A: \u7ECF\u7B2C\u4E09\u65B9\u955C\u50CF\u4E2D\u8F6C\u4E0B\u8F7D\uFF0C\u65E0\u6CD5\u9A8C\u8BC1\u6765\u6E90\u5B8C\u6574\u6027\uFF0C\u5EFA\u8BAE\u76F4\u8FDE\u6216\u81EA\u884C\u6821\u9A8C\u4EA7\u7269");
4586
- }
4587
- if (progressCallback) {
4588
- progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
4495
+ function formatProxySummary(info) {
4496
+ const parts = [];
4497
+ if (info.proxyGroups && info.proxyGroups > 0) parts.push(`${info.proxyGroups} \u7EC4`);
4498
+ parts.push(`${info.proxies || 0} \u8282\u70B9`);
4499
+ return parts.join(", ");
4500
+ }
4501
+ function getActiveSubscription() {
4502
+ const subs = getSubscriptions();
4503
+ if (subs.length === 0) return null;
4504
+ const settings = readSettings();
4505
+ const activeName = settings.active_subscription;
4506
+ if (activeName) {
4507
+ const found = subs.find((s) => s.name === activeName);
4508
+ if (found) return found;
4589
4509
  }
4590
- const curlResult = spawnSync4(
4591
- "curl",
4592
- ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
4593
- { stdio: "inherit" }
4594
- );
4595
- if (curlResult.status !== 0) {
4596
- try {
4597
- fs6.unlinkSync(tempPath);
4598
- } catch {
4510
+ return subs[0];
4511
+ }
4512
+ function findSubscriptionFuzzy(subs, pattern) {
4513
+ const lowerPattern = pattern.toLowerCase();
4514
+ const exact = [];
4515
+ const prefix = [];
4516
+ const includes = [];
4517
+ for (const s of subs) {
4518
+ const name = s.name.toLowerCase();
4519
+ if (name === lowerPattern) {
4520
+ exact.push(s);
4521
+ } else if (name.startsWith(lowerPattern)) {
4522
+ prefix.push(s);
4523
+ } else if (name.includes(lowerPattern)) {
4524
+ includes.push(s);
4599
4525
  }
4600
- throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
4601
- }
4602
- if (!fs6.existsSync(tempPath)) {
4603
- throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
4604
4526
  }
4605
- if (progressCallback) {
4606
- progressCallback("\u89E3\u538B\u5185\u6838...");
4527
+ if (exact.length > 0) return exact;
4528
+ if (prefix.length > 0) return prefix;
4529
+ return includes;
4530
+ }
4531
+ function pickSingleSubscription(subs, pattern) {
4532
+ if (subs.length === 0) {
4533
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${pattern}" \u7684\u8BA2\u9605`);
4534
+ process.exit(1);
4607
4535
  }
4608
- const extractPath = DIRS.kernel;
4609
- let extractedBinary = null;
4536
+ if (subs.length === 1) return subs[0];
4537
+ console.error("\u9519\u8BEF: \u5339\u914D\u5230\u591A\u4E2A\u8BA2\u9605\uFF0C\u8BF7\u66F4\u7CBE\u786E\u6307\u5B9A");
4538
+ console.log("\n\u5339\u914D\u7684\u8BA2\u9605:");
4539
+ for (const s of subs) console.log(` ${s.name}`);
4540
+ process.exit(1);
4541
+ }
4542
+ async function downloadSubscription(url, subName = "default", signal) {
4543
+ let response;
4610
4544
  try {
4611
- if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
4612
- const tarResult = spawnSync4("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
4613
- if (tarResult.error) throw tarResult.error;
4614
- if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
4615
- } else if (tempPath.endsWith(".gz")) {
4616
- const baseName = path4.basename(tempPath, ".gz");
4617
- const outputPath = path4.join(extractPath, baseName);
4618
- const gzipResult = spawnSync4("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
4619
- if (gzipResult.error) throw gzipResult.error;
4620
- if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
4621
- fs6.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
4622
- extractedBinary = outputPath;
4623
- }
4545
+ response = await HTTP_CLIENT.get(url, { responseType: "text", signal });
4624
4546
  } catch (e) {
4625
- try {
4626
- fs6.unlinkSync(tempPath);
4627
- } catch {
4547
+ const maskedUrl = maskUrl(url);
4548
+ let errorMsg = `\u83B7\u53D6\u8BA2\u9605\u5931\u8D25: ${e.message}`;
4549
+ const err = e;
4550
+ if (err.response) {
4551
+ errorMsg += ` (HTTP ${err.response.status})`;
4628
4552
  }
4629
- throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
4553
+ errorMsg += `
4554
+ URL: ${maskedUrl}`;
4555
+ throw new Error(errorMsg);
4630
4556
  }
4631
- const foundBinary = extractedBinary || findBinaryInDir(extractPath);
4632
- if (!foundBinary) {
4633
- try {
4634
- fs6.unlinkSync(tempPath);
4635
- } catch {
4636
- }
4637
- throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
4557
+ const content = response.data;
4558
+ if (!content?.trim()) {
4559
+ throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4638
4560
  }
4639
- const targetPath = PATHS.mihomoBinary;
4640
- if (foundBinary !== targetPath) {
4641
- if (fs6.existsSync(targetPath)) {
4642
- fs6.chmodSync(targetPath, 493);
4561
+ const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
4562
+ if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4563
+ saveSubscriptionRawConfig(subName, content);
4564
+ const meta = extractSubscriptionMeta(response.headers);
4565
+ saveSubscriptionMeta(subName, meta);
4566
+ const proxies = parsed.proxies;
4567
+ const proxyGroups = parsed["proxy-groups"];
4568
+ return {
4569
+ proxies: proxies ? proxies.length : 0,
4570
+ proxyGroups: proxyGroups ? proxyGroups.length : 0,
4571
+ userInfo: meta.userInfo,
4572
+ updateInterval: meta.updateInterval,
4573
+ webPageUrl: meta.webPageUrl,
4574
+ username: meta.username
4575
+ };
4576
+ }
4577
+ async function downloadMergedSubscription(urls, subName, signal) {
4578
+ const responses = await Promise.all(
4579
+ urls.map(async (url, index) => {
4643
4580
  try {
4644
- fs6.unlinkSync(targetPath);
4645
- } catch {
4581
+ const response = await HTTP_CLIENT.get(url, { responseType: "text", signal });
4582
+ return { url, index, response, error: null };
4583
+ } catch (e) {
4584
+ return { url, index, response: null, error: e };
4646
4585
  }
4586
+ })
4587
+ );
4588
+ for (const r of responses) {
4589
+ if (r.error) {
4590
+ const maskedUrl = maskUrl(r.url);
4591
+ throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${r.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${r.error.message}
4592
+ URL: ${maskedUrl}`);
4647
4593
  }
4648
- fs6.renameSync(foundBinary, targetPath);
4649
4594
  }
4650
- fs6.chmodSync(targetPath, 493);
4651
- if (progressCallback) {
4652
- progressCallback("\u6821\u9A8C\u5185\u6838...");
4653
- }
4654
- const check = spawnSync4(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
4655
- const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
4656
- if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
4657
- try {
4658
- fs6.unlinkSync(targetPath);
4659
- } catch {
4660
- }
4661
- try {
4662
- fs6.unlinkSync(tempPath);
4663
- } catch {
4595
+ const parsed = responses.map((r, i) => {
4596
+ const content = r.response?.data;
4597
+ if (!content?.trim()) throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A URL \u5185\u5BB9\u4E3A\u7A7A`);
4598
+ return parseYamlOrJson(content, `\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A`);
4599
+ });
4600
+ const base = parsed[0];
4601
+ const baseProxies = base.proxies || [];
4602
+ const seenNames = new Set(baseProxies.map((p) => p.name));
4603
+ for (let i = 1; i < parsed.length; i++) {
4604
+ const extraProxies = parsed[i].proxies || [];
4605
+ for (const proxy of extraProxies) {
4606
+ if (!seenNames.has(proxy.name)) {
4607
+ baseProxies.push(proxy);
4608
+ seenNames.add(proxy.name);
4609
+ }
4664
4610
  }
4665
- throw new Error(`\u5185\u6838\u81EA\u68C0\u5931\u8D25\uFF08\u53EF\u80FD\u4E0B\u8F7D\u635F\u574F\u6216\u67B6\u6784\u4E0D\u5339\u914D\uFF09\uFF0C\u5DF2\u5220\u9664
4666
- \u9000\u51FA\u7801: ${check.status}
4667
- \u8F93\u51FA: ${checkOutput || "(\u7A7A)"}`);
4668
4611
  }
4669
- try {
4670
- fs6.unlinkSync(tempPath);
4671
- } catch {
4672
- }
4673
- clearKernelVersionCache();
4674
- return { version: latest.tag_name, path: targetPath };
4612
+ base.proxies = baseProxies;
4613
+ const mergedContent = dump(base, YAML_DUMP_OPTS);
4614
+ saveSubscriptionRawConfig(subName, mergedContent);
4615
+ const meta = extractSubscriptionMeta(responses[0].response?.headers);
4616
+ saveSubscriptionMeta(subName, meta);
4617
+ const proxyGroups = base["proxy-groups"];
4618
+ return {
4619
+ proxies: baseProxies.length,
4620
+ proxyGroups: proxyGroups ? proxyGroups.length : 0,
4621
+ userInfo: meta.userInfo,
4622
+ updateInterval: meta.updateInterval,
4623
+ webPageUrl: meta.webPageUrl,
4624
+ username: meta.username
4625
+ };
4675
4626
  }
4676
-
4677
- // src/commands/kernel.ts
4678
- async function cmdKernel(args) {
4679
- const mirrorInfo = parseMirrorArg(args);
4680
- const effectiveMirror = mirrorInfo.mirror;
4681
- if (effectiveMirror) {
4682
- const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
4683
- console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
4627
+ function prepareConfigForStart(mode, subName = "default") {
4628
+ const rawContent = readSubscriptionRawConfig(subName);
4629
+ if (!rawContent) {
4630
+ throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
4684
4631
  }
4685
- console.log("\n\u63D0\u793A: \u5982\u679C\u4E0B\u8F7D\u901F\u5EA6\u8FC7\u6162\u6216\u76F4\u8FDE\u5931\u8D25\uFF0C\u53EF\u4F7F\u7528 --mirror \u53C2\u6570\u901A\u8FC7\u955C\u50CF\u4E0B\u8F7D");
4686
- console.log("\n\u7528\u6CD5:");
4687
- console.log(" mihomo kernel # \u76F4\u8FDE");
4688
- console.log(" mihomo kernel --mirror # \u4E0B\u8F7D\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF (v6.gh-proxy.org)");
4689
- console.log(" mihomo kernel --mirror hk.gh-proxy.org # \u4E0B\u8F7D\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
4690
- console.log(" mihomo kernel --mirror-all # API\u8BF7\u6C42\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u9ED8\u8BA4\u955C\u50CF");
4691
- console.log(" mihomo kernel --mirror-all hk.gh-proxy.org # API\u548C\u4E0B\u8F7D\u90FD\u4F7F\u7528\u6307\u5B9A\u955C\u50CF");
4692
- console.log("\n\u53EF\u7528\u955C\u50CF:");
4693
- for (const m of AVAILABLE_MIRRORS) {
4694
- const isCurrent = effectiveMirror && (effectiveMirror.includes(`//${m}/`) || effectiveMirror.includes(`//${m}:`) || effectiveMirror.endsWith(`//${m}`));
4695
- console.log(` ${m}${isCurrent ? " (\u5F53\u524D)" : ""}`);
4632
+ const buildResult = buildConfig(rawContent, mode);
4633
+ if (buildResult.warnings.length > 0) {
4634
+ for (const warning of buildResult.warnings) {
4635
+ console.log(`${colors.yellow("\u81EA\u52A8\u4FEE\u590D:")} ${warning}`);
4636
+ }
4637
+ console.log("");
4696
4638
  }
4697
- console.log("");
4698
- console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
4639
+ writeMihomoConfig(buildResult.config);
4640
+ writeDebugConfig(buildResult);
4641
+ const proxies = buildResult.config.proxies;
4642
+ const proxyGroups = buildResult.config["proxy-groups"];
4643
+ return {
4644
+ proxies: proxies ? proxies.length : 0,
4645
+ proxyGroups: proxyGroups ? proxyGroups.length : 0
4646
+ };
4647
+ }
4648
+ function needsAutoUpdate(sub) {
4649
+ if (!sub.updated_at) return true;
4650
+ const lastUpdate = new Date(sub.updated_at).getTime();
4651
+ if (Number.isNaN(lastUpdate)) return true;
4652
+ const intervalHours = resolveUpdateInterval(sub.url, sub.update_interval);
4653
+ const intervalMs = intervalHours * 60 * 60 * 1e3;
4654
+ return Date.now() - lastUpdate > intervalMs;
4655
+ }
4656
+ async function tryUpdateOne(sub, signal) {
4699
4657
  try {
4700
- const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
4701
- const info = await checkUpdate(apiMirror);
4702
- console.log(`\u5F53\u524D: ${info.current}`);
4703
- console.log(`\u6700\u65B0: ${info.latest}`);
4704
- if (!info.needsUpdate) {
4705
- console.log("\u5DF2\u662F\u6700\u65B0\u7248\u672C");
4658
+ let info;
4659
+ if (isMultiUrl(sub.url)) {
4660
+ info = await downloadMergedSubscription(splitUrls(sub.url), sub.name, signal);
4706
4661
  } else {
4707
- console.log("\n\u6B63\u5728\u4E0B\u8F7D...");
4708
- const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
4709
- console.log(`
4710
- \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
4662
+ info = await downloadSubscription(sub.url, sub.name, signal);
4711
4663
  }
4664
+ return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
4712
4665
  } catch (e) {
4713
- console.error(`
4714
- \u66F4\u65B0\u5931\u8D25: ${e.message}`);
4715
- const err = e;
4716
- if (err.response?.data) {
4717
- if (err.response.data.message) {
4718
- console.error(`\u539F\u56E0: ${err.response.data.message}`);
4719
- }
4720
- if (err.response.data.documentation_url) {
4721
- console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
4722
- }
4723
- }
4724
- process.exit(1);
4666
+ return { name: sub.name, success: false, error: e.message };
4725
4667
  }
4726
4668
  }
4727
-
4728
- // src/commands/log.ts
4729
- function cmdLog(args) {
4730
- const logPath = getLogPath();
4731
- if (hasFlag(args, "-o", "--open")) {
4732
- openLogFile(logPath);
4733
- return;
4734
- }
4735
- viewLogWithTail(logPath, { follow: true, lines: 50 });
4669
+ function printUpdateResult(r) {
4670
+ if (r.success) {
4671
+ console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
4672
+ } else {
4673
+ console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
4674
+ }
4736
4675
  }
4737
- function cmdLogs(args) {
4738
- const targetName = getNonFlagArg(args, 1);
4739
- const lines = parseIntArg(args, "-n", "--lines", 100);
4740
- const openInViewer = hasFlag(args, "-o", "--open");
4741
- if (targetName) {
4742
- let logPath;
4743
- if (targetName === "current" || targetName === "0") {
4744
- logPath = getLogPath();
4745
- } else {
4746
- const parsedIdx = parseInt(targetName, 10);
4747
- if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
4748
- const archiveLogs = listLogs();
4749
- const archive = archiveLogs.archives[parsedIdx - 1];
4750
- if (!archive) {
4751
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
4752
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
4753
- process.exit(1);
4754
- }
4755
- logPath = archive.path;
4756
- } else {
4757
- logPath = getLogPathByName(targetName);
4758
- }
4676
+ async function autoUpdateStaleSubscription(options = {}) {
4677
+ const allSubs = getSubscriptionsWithCache();
4678
+ const staleSubs = allSubs.filter(needsAutoUpdate);
4679
+ if (staleSubs.length === 0) {
4680
+ return { total: 0, updated: 0, failed: 0 };
4681
+ }
4682
+ if (staleSubs.length === 1) {
4683
+ const sub = staleSubs[0];
4684
+ const interval = resolveUpdateInterval(sub.url, sub.update_interval);
4685
+ console.log(`\u8BA2\u9605 "${sub.name}" \u8D85\u8FC7 ${interval} \u5C0F\u65F6\u672A\u66F4\u65B0\uFF0C\u6B63\u5728\u66F4\u65B0...`);
4686
+ } else {
4687
+ console.log(`\u68C0\u67E5\u5230 ${staleSubs.length} \u4E2A\u8BA2\u9605\u9700\u8981\u66F4\u65B0\uFF0C\u6B63\u5728\u5E76\u884C\u66F4\u65B0...`);
4688
+ }
4689
+ const timeoutMs = options.timeout ?? DEFAULT_AUTO_UPDATE_TIMEOUT;
4690
+ const controller = new AbortController();
4691
+ let results;
4692
+ try {
4693
+ results = await withTimeout(Promise.all(staleSubs.map((sub) => tryUpdateOne(sub, controller.signal))), timeoutMs);
4694
+ } 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 };
4759
4699
  }
4760
- if (!logPath) {
4761
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
4762
- console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
4763
- process.exit(1);
4700
+ throw e;
4701
+ }
4702
+ let updatedCount = 0;
4703
+ for (const r of results) {
4704
+ if (r.success) updatedCount++;
4705
+ printUpdateResult(r);
4706
+ }
4707
+ return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
4708
+ }
4709
+ var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
4710
+ async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API_BASE) {
4711
+ const encodedName = encodeURIComponent(proxyName);
4712
+ const url = `${apiBase}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
4713
+ try {
4714
+ const response = await client.get(url);
4715
+ const data = JSON.parse(response.data);
4716
+ if (data.delay && data.delay > 0) {
4717
+ return { name: proxyName, delay: data.delay };
4764
4718
  }
4765
- if (openInViewer) {
4766
- openLogFile(logPath);
4767
- return;
4719
+ return { name: proxyName, delay: null, error: data.message || "no delay" };
4720
+ } catch (e) {
4721
+ const err = e;
4722
+ let errorMsg = "timeout";
4723
+ if (err.response?.data?.message) {
4724
+ errorMsg = String(err.response.data.message);
4725
+ } else if (err.message) {
4726
+ errorMsg = err.message;
4768
4727
  }
4769
- viewLogWithTail(logPath, { follow: false, lines });
4770
- return;
4728
+ return { name: proxyName, delay: null, error: errorMsg };
4771
4729
  }
4772
- const logs = listLogs();
4773
- const all = [];
4774
- if (logs.current) all.push(logs.current);
4775
- all.push(...logs.archives);
4776
- if (all.length === 0) {
4777
- console.log("\u6682\u65E0\u65E5\u5FD7");
4778
- return;
4730
+ }
4731
+ async function testSubscriptionProxies(subName, options = {}) {
4732
+ const { timeout = DEFAULT_TEST_TIMEOUT, concurrency = DEFAULT_TEST_CONCURRENCY, testUrl = DEFAULT_TEST_URL, apiBase = API_BASE, onResult } = options;
4733
+ const { proxies } = options.parsed || loadSubscriptionConfig(subName);
4734
+ if (proxies.length === 0) {
4735
+ return { total: 0, alive: 0, dead: 0, results: [] };
4779
4736
  }
4780
- console.log("");
4781
- console.log("\u65E5\u5FD7\u5217\u8868:");
4782
- console.log("");
4783
- let archiveCounter = 0;
4784
- for (const log of all) {
4785
- let num;
4786
- if (log.isCurrent) {
4787
- num = " 0";
4737
+ const client = createHttpClient({ timeout: timeout + 3e3 });
4738
+ const results = new Array(proxies.length);
4739
+ let completedCount = 0;
4740
+ let nextIndex = 0;
4741
+ async function runNext() {
4742
+ while (nextIndex < proxies.length) {
4743
+ const idx = nextIndex++;
4744
+ const result = await testProxyDelay(proxies[idx].name, timeout, testUrl, client, apiBase);
4745
+ results[idx] = result;
4746
+ onResult?.(result, completedCount, proxies.length);
4747
+ completedCount++;
4748
+ }
4749
+ }
4750
+ const workers = Array.from({ length: Math.min(concurrency, proxies.length) }, () => runNext());
4751
+ await Promise.all(workers);
4752
+ const alive = results.filter((r) => r.delay !== null).length;
4753
+ return { total: results.length, alive, dead: results.length - alive, results };
4754
+ }
4755
+ function normalizeProxyNamesBeforeSave(parsed) {
4756
+ const { proxies, proxyGroups } = parsed;
4757
+ const renameMap = /* @__PURE__ */ new Map();
4758
+ const usedNames = /* @__PURE__ */ new Set();
4759
+ for (const proxy of proxies) {
4760
+ const shortened = proxy.name.replace(/_github\.com\/[^_]+/, "");
4761
+ if (shortened !== proxy.name && !usedNames.has(shortened)) {
4762
+ renameMap.set(proxy.name, shortened);
4763
+ usedNames.add(shortened);
4788
4764
  } else {
4789
- archiveCounter++;
4790
- num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
4765
+ usedNames.add(proxy.name);
4791
4766
  }
4792
- const time = formatDate(log.mtime);
4793
- const size = formatBytes(log.size);
4794
- const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
4795
- console.log(` ${num}. ${name}`);
4796
- console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
4797
- if (!log.isCurrent) {
4798
- console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
4767
+ }
4768
+ if (renameMap.size === 0) return 0;
4769
+ for (const proxy of proxies) {
4770
+ const newName = renameMap.get(proxy.name);
4771
+ if (newName) proxy.name = newName;
4772
+ }
4773
+ for (const group of proxyGroups) {
4774
+ if (Array.isArray(group.proxies)) {
4775
+ group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
4799
4776
  }
4800
- console.log("");
4801
4777
  }
4802
- console.log("\u7528\u6CD5:");
4803
- console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
4804
- console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
4805
- console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
4806
- console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
4807
- console.log("");
4808
- }
4809
-
4810
- // src/commands/overwrite.ts
4811
- import path6 from "path";
4812
-
4813
- // src/subscription.ts
4814
- function isGithubUrl(url) {
4815
- return /github\.com|raw\.githubusercontent\.com/i.test(url);
4816
- }
4817
- function getDefaultUpdateInterval(url) {
4818
- return isGithubUrl(url) ? DEFAULT_UPDATE_INTERVAL_HOURS_GITHUB : DEFAULT_UPDATE_INTERVAL_HOURS;
4819
- }
4820
- function resolveUpdateInterval(url, cachedInterval) {
4821
- return cachedInterval && cachedInterval > 0 ? cachedInterval : getDefaultUpdateInterval(url);
4822
- }
4823
- var YAML_DUMP_OPTS = { indent: 2, lineWidth: -1, schema: CORE_SCHEMA };
4824
- var HTTP_CLIENT2 = createHttpClient({ timeout: 6e4 });
4825
- function isMultiUrl(url) {
4826
- return url.includes(",");
4827
- }
4828
- function splitUrls(url) {
4829
- return url.split(",").map((u) => u.trim()).filter(Boolean);
4778
+ return renameMap.size;
4830
4779
  }
4831
- function loadSubscriptionConfig(subName) {
4832
- const rawContent = readSubscriptionRawConfig(subName);
4833
- if (!rawContent) {
4834
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
4780
+ function cleanDeadProxies(parsed, deadNames) {
4781
+ const { proxies, proxyGroups } = parsed;
4782
+ const originalCount = proxies.length;
4783
+ parsed.proxies = proxies.filter((p) => !deadNames.has(p.name));
4784
+ const removedProxies = originalCount - parsed.proxies.length;
4785
+ let updatedGroups = 0;
4786
+ const removedGroupNames = /* @__PURE__ */ new Set();
4787
+ for (const group of proxyGroups) {
4788
+ if (Array.isArray(group.proxies)) {
4789
+ const before = group.proxies.length;
4790
+ group.proxies = group.proxies.filter((name) => !deadNames.has(name));
4791
+ if (group.proxies.length < before) {
4792
+ updatedGroups++;
4793
+ }
4794
+ if (group.proxies.length === 0) {
4795
+ removedGroupNames.add(group.name);
4796
+ }
4797
+ }
4835
4798
  }
4836
- const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
4837
- return {
4838
- raw,
4839
- proxies: raw.proxies || [],
4840
- proxyGroups: raw["proxy-groups"] || []
4841
- };
4842
- }
4843
- function saveSubscriptionConfig(subName, parsed) {
4844
- normalizeProxyNamesBeforeSave(parsed);
4845
- parsed.raw.proxies = parsed.proxies;
4846
- parsed.raw["proxy-groups"] = parsed.proxyGroups;
4847
- saveSubscriptionRawConfig(subName, dump(parsed.raw, YAML_DUMP_OPTS));
4848
- }
4849
- function parseUserInfo(header) {
4850
- if (!header) return null;
4851
- const info = {};
4852
- const parts = header.split(";").map((p) => p.trim());
4853
- for (const part of parts) {
4854
- const [key, val] = part.split("=").map((s) => s.trim());
4855
- if (key && val !== void 0) {
4856
- const numVal = parseFloat(val);
4857
- info[key] = Number.isNaN(numVal) ? 0 : numVal;
4799
+ if (removedGroupNames.size > 0) {
4800
+ parsed.proxyGroups = proxyGroups.filter((g) => !removedGroupNames.has(g.name));
4801
+ for (const group of parsed.proxyGroups) {
4802
+ if (Array.isArray(group.proxies)) {
4803
+ group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
4804
+ }
4805
+ }
4806
+ const rules = parsed.raw.rules;
4807
+ if (Array.isArray(rules)) {
4808
+ parsed.raw.rules = rules.filter((rule) => {
4809
+ 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());
4813
+ });
4858
4814
  }
4859
4815
  }
4860
- return info;
4861
- }
4862
- function parsePositiveInterval(header) {
4863
- if (!header) return null;
4864
- const n = parseInt(header, 10);
4865
- return Number.isFinite(n) && n > 0 ? n : null;
4816
+ return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
4866
4817
  }
4867
- function parseUsernameFromContentDisposition(header) {
4868
- if (!header) return null;
4869
- const match = header.match(/filename\s*=\s*["']?([^"';\s]+)["']?/i);
4870
- if (!match) return null;
4871
- const filename = match[1];
4872
- const parts = filename.split("/");
4873
- return parts[parts.length - 1] || null;
4874
- }
4875
- function extractSubscriptionMeta(headers) {
4876
- return {
4877
- userInfo: parseUserInfo(headers?.get("subscription-userinfo") ?? null),
4878
- updateInterval: parsePositiveInterval(headers?.get("profile-update-interval")),
4879
- webPageUrl: headers?.get("profile-web-page-url") || null,
4880
- username: parseUsernameFromContentDisposition(headers?.get("content-disposition") ?? null)
4881
- };
4882
- }
4883
- function saveSubscriptionMeta(subName, meta) {
4884
- const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
4885
- if (meta.userInfo) {
4886
- cacheData.upload = meta.userInfo.upload;
4887
- cacheData.download = meta.userInfo.download;
4888
- cacheData.total = meta.userInfo.total;
4889
- cacheData.expire = meta.userInfo.expire;
4818
+ async function autoCleanSubscription(subName, options = {}) {
4819
+ const parsed = loadSubscriptionConfig(subName);
4820
+ const { onResult, onRetryRound, rounds = DEFAULT_CLEAN_ROUNDS, ...testOptions } = options;
4821
+ const wrapOnResult = (round) => onResult ? (r, i, t) => onResult(r, i, t, round) : void 0;
4822
+ const summary = await testSubscriptionProxies(subName, {
4823
+ ...testOptions,
4824
+ parsed,
4825
+ onResult: wrapOnResult(1)
4826
+ });
4827
+ let removedProxies = 0;
4828
+ let updatedGroups = 0;
4829
+ let removedGroups = 0;
4830
+ let skipped = false;
4831
+ if (summary.dead > 0) {
4832
+ if (summary.alive === 0 || summary.alive / summary.total < 0.01) {
4833
+ skipped = true;
4834
+ } else {
4835
+ const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
4836
+ const deadProxies = parsed.proxies.filter((p) => deadNames.has(p.name));
4837
+ for (let retry = 0; retry < rounds - 1; retry++) {
4838
+ const round = retry + 2;
4839
+ const retryTargets = deadProxies.filter((p) => deadNames.has(p.name));
4840
+ if (retryTargets.length === 0) break;
4841
+ onRetryRound?.(round, retryTargets.length);
4842
+ const retryParsed = { raw: {}, proxies: retryTargets, proxyGroups: [] };
4843
+ const retrySummary = await testSubscriptionProxies(subName, {
4844
+ ...testOptions,
4845
+ parsed: retryParsed,
4846
+ onResult: wrapOnResult(round)
4847
+ });
4848
+ for (const r of retrySummary.results) {
4849
+ if (r.delay !== null) {
4850
+ deadNames.delete(r.name);
4851
+ }
4852
+ }
4853
+ }
4854
+ summary.dead = deadNames.size;
4855
+ summary.alive = summary.total - summary.dead;
4856
+ if (deadNames.size > 0) {
4857
+ const cleanResult = cleanDeadProxies(parsed, deadNames);
4858
+ removedProxies = cleanResult.removedProxies;
4859
+ updatedGroups = cleanResult.updatedGroups;
4860
+ removedGroups = cleanResult.removedGroups;
4861
+ }
4862
+ }
4890
4863
  }
4891
- if (meta.updateInterval) cacheData.update_interval = meta.updateInterval;
4892
- if (meta.webPageUrl) cacheData.web_page_url = meta.webPageUrl;
4893
- if (meta.username) cacheData.username = meta.username;
4894
- saveSubscriptionCache(subName, cacheData);
4895
- }
4896
- function formatProxySummary(info) {
4897
- const parts = [];
4898
- if (info.proxyGroups && info.proxyGroups > 0) parts.push(`${info.proxyGroups} \u7EC4`);
4899
- parts.push(`${info.proxies || 0} \u8282\u70B9`);
4900
- return parts.join(", ");
4864
+ if (!skipped && removedProxies > 0) {
4865
+ saveSubscriptionConfig(subName, parsed);
4866
+ }
4867
+ return { summary, removedProxies, updatedGroups, removedGroups, skipped };
4901
4868
  }
4902
- function getActiveSubscription() {
4903
- const subs = getSubscriptions();
4904
- if (subs.length === 0) return null;
4905
- const settings = readSettings();
4906
- const activeName = settings.active_subscription;
4907
- if (activeName) {
4908
- const found = subs.find((s) => s.name === activeName);
4909
- if (found) return found;
4869
+
4870
+ // src/commands/daemon.ts
4871
+ function printDaemonStatus() {
4872
+ const status = getDaemonStatus();
4873
+ const stateText = status.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
4874
+ console.log(`${colors.gray("\u4FDD\u6D3B: ")}${stateText}`);
4875
+ if (status.enabled) {
4876
+ const runText = isDaemonRunning(status) ? colors.green(`\u8FD0\u884C\u4E2D (PID ${status.pid})`) : colors.yellow("\u672A\u8FD0\u884C");
4877
+ console.log(`${colors.gray("\u5185\u6838: ")}${runText}`);
4910
4878
  }
4911
- return subs[0];
4879
+ console.log("");
4880
+ if (status.enabled) {
4881
+ console.log("\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off");
4882
+ } else {
4883
+ console.log("\u5F00\u542F\u4FDD\u6D3B: mihomo daemon on");
4884
+ console.log(colors.gray(" \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF08\u4EC5 Mixed \u6A21\u5F0F\uFF09"));
4885
+ }
4886
+ console.log("");
4912
4887
  }
4913
- function findSubscriptionFuzzy(subs, pattern) {
4914
- const lowerPattern = pattern.toLowerCase();
4915
- const exact = [];
4916
- const prefix = [];
4917
- const includes = [];
4918
- for (const s of subs) {
4919
- const name = s.name.toLowerCase();
4920
- if (name === lowerPattern) {
4921
- exact.push(s);
4922
- } else if (name.startsWith(lowerPattern)) {
4923
- prefix.push(s);
4924
- } else if (name.includes(lowerPattern)) {
4925
- includes.push(s);
4888
+ async function cmdDaemon(args) {
4889
+ const action = args?.[1];
4890
+ if (action === "on" || action === "enable") {
4891
+ if (!hasKernel()) {
4892
+ console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
4893
+ process.exit(1);
4926
4894
  }
4895
+ const sub = getActiveSubscription();
4896
+ if (!sub) {
4897
+ console.error("\u9519\u8BEF: \u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
4898
+ process.exit(1);
4899
+ }
4900
+ let configInfo;
4901
+ try {
4902
+ configInfo = prepareConfigForStart("mixed", sub.name);
4903
+ } catch (e) {
4904
+ console.error(`${colors.red("\u914D\u7F6E\u9519\u8BEF:")} ${e.message}`);
4905
+ process.exit(1);
4906
+ }
4907
+ try {
4908
+ enableDaemon();
4909
+ } catch (e) {
4910
+ console.error(`${colors.red("\u542F\u7528\u4FDD\u6D3B\u5931\u8D25:")} ${e.message}`);
4911
+ process.exit(1);
4912
+ }
4913
+ console.log(`${colors.green("\u5DF2\u542F\u7528\u4FDD\u6D3B")} \xB7 ${sub.name} \xB7 ${formatProxySummary(configInfo)}`);
4914
+ console.log(colors.gray("\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u81EA\u52A8\u91CD\u542F\uFF0C\u4EE3\u7406\u5C06\u5728\u540E\u53F0\u5E38\u9A7B"));
4915
+ console.log("");
4916
+ printDaemonStatus();
4917
+ return;
4927
4918
  }
4928
- if (exact.length > 0) return exact;
4929
- if (prefix.length > 0) return prefix;
4930
- return includes;
4931
- }
4932
- function pickSingleSubscription(subs, pattern) {
4933
- if (subs.length === 0) {
4934
- console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u5339\u914D "${pattern}" \u7684\u8BA2\u9605`);
4919
+ if (action === "off" || action === "disable") {
4920
+ if (!isDaemonEnabled()) {
4921
+ console.log("\u4FDD\u6D3B\u5DF2\u662F\u5173\u95ED\u72B6\u6001");
4922
+ console.log("");
4923
+ printDaemonStatus();
4924
+ return;
4925
+ }
4926
+ try {
4927
+ disableDaemon();
4928
+ } catch (e) {
4929
+ console.error(`${colors.red("\u5173\u95ED\u4FDD\u6D3B\u5931\u8D25:")} ${e.message}`);
4930
+ process.exit(1);
4931
+ }
4932
+ console.log(`${colors.green("\u5DF2\u5173\u95ED\u4FDD\u6D3B")}\uFF0C\u4EE3\u7406\u5DF2\u505C\u6B62`);
4933
+ console.log(colors.gray("\u91CD\u65B0\u542F\u7528: mihomo daemon on"));
4934
+ console.log("");
4935
+ return;
4936
+ }
4937
+ if (action !== void 0 && action !== "status") {
4938
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684 daemon \u5B50\u547D\u4EE4: ${action}`);
4939
+ console.log("");
4940
+ console.log("\u53EF\u7528\u5B50\u547D\u4EE4: on, off, status");
4935
4941
  process.exit(1);
4936
4942
  }
4937
- if (subs.length === 1) return subs[0];
4938
- console.error("\u9519\u8BEF: \u5339\u914D\u5230\u591A\u4E2A\u8BA2\u9605\uFF0C\u8BF7\u66F4\u7CBE\u786E\u6307\u5B9A");
4939
- console.log("\n\u5339\u914D\u7684\u8BA2\u9605:");
4940
- for (const s of subs) console.log(` ${s.name}`);
4941
- process.exit(1);
4943
+ console.log("");
4944
+ printDaemonStatus();
4942
4945
  }
4943
- async function downloadSubscription(url, subName = "default", signal) {
4944
- let response;
4945
- try {
4946
- response = await HTTP_CLIENT2.get(url, { responseType: "text", signal });
4947
- } catch (e) {
4948
- const maskedUrl = maskUrl(url);
4949
- let errorMsg = `\u83B7\u53D6\u8BA2\u9605\u5931\u8D25: ${e.message}`;
4950
- const err = e;
4951
- if (err.response) {
4952
- errorMsg += ` (HTTP ${err.response.status})`;
4946
+
4947
+ // src/commands/directory.ts
4948
+ function cmdDirectory(args) {
4949
+ const action = args?.[1];
4950
+ if (action === "open") {
4951
+ const target = args[2];
4952
+ if (!target || target === "root") {
4953
+ console.log("\u6B63\u5728\u6253\u5F00: \u6839\u76EE\u5F55");
4954
+ const success = openUrl(USER_DATA_DIR);
4955
+ if (!success) {
4956
+ console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${USER_DATA_DIR}`);
4957
+ }
4958
+ return;
4953
4959
  }
4954
- errorMsg += `
4955
- URL: ${maskedUrl}`;
4956
- throw new Error(errorMsg);
4957
- }
4958
- const content = response.data;
4959
- if (!content?.trim()) {
4960
- throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4960
+ const key = target.toLowerCase();
4961
+ const targetInfo = Object.hasOwn(DIRECTORY_TARGETS, key) ? DIRECTORY_TARGETS[key] : void 0;
4962
+ if (targetInfo) {
4963
+ const targetPath = targetInfo.path || USER_DATA_DIR;
4964
+ console.log(`\u6B63\u5728\u6253\u5F00: ${targetInfo.label}`);
4965
+ const success = openUrl(targetPath);
4966
+ if (!success) {
4967
+ console.log(`\u8BF7\u624B\u52A8\u6253\u5F00: ${targetPath}`);
4968
+ }
4969
+ return;
4970
+ }
4971
+ console.error(`\u9519\u8BEF: \u672A\u77E5\u7684\u76EE\u5F55\u76EE\u6807 "${target}"`);
4972
+ console.log("");
4973
+ console.log("\u53EF\u7528\u76EE\u6807:");
4974
+ console.log(" root (\u9ED8\u8BA4) \u6839\u76EE\u5F55");
4975
+ for (const [key2, val] of Object.entries(DIRECTORY_TARGETS)) {
4976
+ if (key2 !== "root") {
4977
+ console.log(` ${key2.padEnd(14)}${val.label}`);
4978
+ }
4979
+ }
4980
+ console.log("");
4981
+ process.exit(1);
4961
4982
  }
4962
- const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
4963
- if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
4964
- saveSubscriptionRawConfig(subName, content);
4965
- const meta = extractSubscriptionMeta(response.headers);
4966
- saveSubscriptionMeta(subName, meta);
4967
- const proxies = parsed.proxies;
4968
- const proxyGroups = parsed["proxy-groups"];
4969
- return {
4970
- proxies: proxies ? proxies.length : 0,
4971
- proxyGroups: proxyGroups ? proxyGroups.length : 0,
4972
- userInfo: meta.userInfo,
4973
- updateInterval: meta.updateInterval,
4974
- webPageUrl: meta.webPageUrl,
4975
- username: meta.username
4976
- };
4983
+ console.log("");
4984
+ console.log("\u6570\u636E\u76EE\u5F55\u4F4D\u7F6E:");
4985
+ console.log(` \u6839\u76EE\u5F55: ${USER_DATA_DIR}`);
4986
+ console.log(` \u5168\u5C40\u8BBE\u7F6E: ${PATHS.settingsFile}`);
4987
+ console.log(` \u5185\u6838\u76EE\u5F55: ${DIRS.kernel}`);
4988
+ console.log(` \u5185\u6838\u6587\u4EF6: ${PATHS.mihomoBinary}`);
4989
+ console.log(` \u8BA2\u9605\u76EE\u5F55: ${DIRS.subscriptions}`);
4990
+ console.log(" - cache.json (\u8BA2\u9605\u7F13\u5B58\uFF1A\u66F4\u65B0\u65F6\u95F4\u3001\u6D41\u91CF\u7B49)");
4991
+ console.log(" - xxx.yaml (\u8BA2\u9605\u539F\u59CB\u914D\u7F6E)");
4992
+ console.log(` \u8FD0\u884C\u65F6\u76EE\u5F55: ${DIRS.runtime}`);
4993
+ console.log(" - config.yaml (\u542F\u52A8\u65F6\u751F\u6210\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
4994
+ console.log(" - pid (PID \u6587\u4EF6\uFF0Cstop \u81EA\u52A8\u6E05\u9664)");
4995
+ console.log(` \u65E5\u5FD7\u6587\u4EF6: ${PATHS.logFile}`);
4996
+ console.log(` mihomo \u6570\u636E: ${DIRS.data}`);
4997
+ console.log(" - cache.db, Geo*.dat \u7B49 (mihomo \u81EA\u884C\u7BA1\u7406)");
4998
+ console.log("");
4999
+ console.log("\u6253\u5F00\u76EE\u5F55:");
5000
+ console.log(" mihomo dir open \u6253\u5F00\u6839\u76EE\u5F55");
5001
+ console.log(" mihomo dir open subs \u6253\u5F00\u8BA2\u9605\u76EE\u5F55");
5002
+ console.log(" mihomo dir open logs \u6253\u5F00\u65E5\u5FD7\u76EE\u5F55");
5003
+ console.log(" mihomo dir open data \u6253\u5F00 mihomo \u6570\u636E\u76EE\u5F55");
5004
+ console.log(" mihomo dir open runtime \u6253\u5F00\u8FD0\u884C\u65F6\u76EE\u5F55");
5005
+ console.log(" mihomo dir open kernel \u6253\u5F00\u5185\u6838\u76EE\u5F55");
5006
+ console.log("");
5007
+ console.log("\u73AF\u5883\u53D8\u91CF:");
5008
+ console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
5009
+ console.log("");
4977
5010
  }
4978
- async function downloadMergedSubscription(urls, subName, signal) {
4979
- const responses = await Promise.all(
4980
- urls.map(async (url, index) => {
4981
- try {
4982
- const response = await HTTP_CLIENT2.get(url, { responseType: "text", signal });
4983
- return { url, index, response, error: null };
4984
- } catch (e) {
4985
- return { url, index, response: null, error: e };
4986
- }
4987
- })
5011
+
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
+ `
4988
5024
  );
4989
- for (const r of responses) {
4990
- if (r.error) {
4991
- const maskedUrl = maskUrl(r.url);
4992
- throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${r.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${r.error.message}
4993
- URL: ${maskedUrl}`);
4994
- }
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
+ // src/kernel.ts
5101
+ import { spawnSync as spawnSync5 } from "child_process";
5102
+ import fs7 from "fs";
5103
+ import path5 from "path";
5104
+
5105
+ // node_modules/compare-versions/lib/esm/utils.js
5106
+ var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
5107
+ var validateAndParse = (version) => {
5108
+ if (typeof version !== "string") {
5109
+ throw new TypeError("Invalid argument expected string");
4995
5110
  }
4996
- const parsed = responses.map((r, i) => {
4997
- const content = r.response?.data;
4998
- if (!content?.trim()) throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A URL \u5185\u5BB9\u4E3A\u7A7A`);
4999
- return parseYamlOrJson(content, `\u5408\u5E76\u8BA2\u9605\u7B2C ${i + 1} \u4E2A`);
5111
+ const match = version.match(semver);
5112
+ if (!match) {
5113
+ throw new Error(`Invalid argument not valid semver ('${version}' received)`);
5114
+ }
5115
+ match.shift();
5116
+ return match;
5117
+ };
5118
+ var isWildcard = (s) => s === "*" || s === "x" || s === "X";
5119
+ var tryParse = (v) => {
5120
+ const n = parseInt(v, 10);
5121
+ return isNaN(n) ? v : n;
5122
+ };
5123
+ var forceType = (a, b) => typeof a !== typeof b ? [String(a), String(b)] : [a, b];
5124
+ var compareStrings = (a, b) => {
5125
+ if (isWildcard(a) || isWildcard(b))
5126
+ return 0;
5127
+ const [ap, bp] = forceType(tryParse(a), tryParse(b));
5128
+ if (ap > bp)
5129
+ return 1;
5130
+ if (ap < bp)
5131
+ return -1;
5132
+ return 0;
5133
+ };
5134
+ var compareSegments = (a, b) => {
5135
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
5136
+ const r = compareStrings(a[i] || "0", b[i] || "0");
5137
+ if (r !== 0)
5138
+ return r;
5139
+ }
5140
+ return 0;
5141
+ };
5142
+
5143
+ // node_modules/compare-versions/lib/esm/compareVersions.js
5144
+ var compareVersions = (v1, v2) => {
5145
+ const n1 = validateAndParse(v1);
5146
+ const n2 = validateAndParse(v2);
5147
+ const p1 = n1.pop();
5148
+ const p2 = n2.pop();
5149
+ const r = compareSegments(n1, n2);
5150
+ if (r !== 0)
5151
+ return r;
5152
+ if (p1 && p2) {
5153
+ return compareSegments(p1.split("."), p2.split("."));
5154
+ } else if (p1 || p2) {
5155
+ return p1 ? -1 : 1;
5156
+ }
5157
+ return 0;
5158
+ };
5159
+
5160
+ // src/kernel.ts
5161
+ var GITHUB_REPO = "MetaCubeX/mihomo";
5162
+ var KERNEL_HTTP_TIMEOUT = 12e4;
5163
+ var KERNEL_DOWNLOAD_TIMEOUT = 18e4;
5164
+ var HTTP_CLIENT2 = createHttpClient({ timeout: KERNEL_HTTP_TIMEOUT });
5165
+ function withMirror(url, mirror) {
5166
+ if (mirror && (url.startsWith("https://github.com/") || url.startsWith("https://api.github.com/"))) {
5167
+ return mirror + url;
5168
+ }
5169
+ return url;
5170
+ }
5171
+ function getArch() {
5172
+ const arch = process.arch;
5173
+ if (arch === "arm64") return "arm64";
5174
+ if (arch === "x64") return "amd64";
5175
+ return arch;
5176
+ }
5177
+ function findMatchingAsset(assets, platform, arch) {
5178
+ const prefix = `mihomo-${platform}-${arch}`;
5179
+ const matchingAssets = assets.filter(
5180
+ (a) => a.name.startsWith(prefix) && a.name.endsWith(".gz") || a.name.startsWith(`${prefix}-`) && a.name.endsWith(".gz")
5181
+ );
5182
+ if (matchingAssets.length === 0) return null;
5183
+ if (matchingAssets.length === 1) return matchingAssets[0];
5184
+ const standardAsset = matchingAssets.find((a) => {
5185
+ const nameWithoutGz = a.name.slice(0, -3);
5186
+ const parts = nameWithoutGz.split("-");
5187
+ const lastPart = parts[parts.length - 1];
5188
+ return /^v?\d+\.\d+\.\d+/.test(lastPart) && !nameWithoutGz.includes("-go");
5000
5189
  });
5001
- const base = parsed[0];
5002
- const baseProxies = base.proxies || [];
5003
- const seenNames = new Set(baseProxies.map((p) => p.name));
5004
- for (let i = 1; i < parsed.length; i++) {
5005
- const extraProxies = parsed[i].proxies || [];
5006
- for (const proxy of extraProxies) {
5007
- if (!seenNames.has(proxy.name)) {
5008
- baseProxies.push(proxy);
5009
- seenNames.add(proxy.name);
5010
- }
5011
- }
5012
- }
5013
- base.proxies = baseProxies;
5014
- const mergedContent = dump(base, YAML_DUMP_OPTS);
5015
- saveSubscriptionRawConfig(subName, mergedContent);
5016
- const meta = extractSubscriptionMeta(responses[0].response?.headers);
5017
- saveSubscriptionMeta(subName, meta);
5018
- const proxyGroups = base["proxy-groups"];
5019
- return {
5020
- proxies: baseProxies.length,
5021
- proxyGroups: proxyGroups ? proxyGroups.length : 0,
5022
- userInfo: meta.userInfo,
5023
- updateInterval: meta.updateInterval,
5024
- webPageUrl: meta.webPageUrl,
5025
- username: meta.username
5026
- };
5190
+ return standardAsset || matchingAssets[0];
5027
5191
  }
5028
- function prepareConfigForStart(mode, subName = "default") {
5029
- const rawContent = readSubscriptionRawConfig(subName);
5030
- if (!rawContent) {
5031
- throw new Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
5192
+ async function getLatestRelease(repo, mirror) {
5193
+ const url = withMirror(`https://api.github.com/repos/${repo}/releases`, mirror);
5194
+ const response = await HTTP_CLIENT2.get(url, { responseType: "json" });
5195
+ const releases = response.data;
5196
+ if (!Array.isArray(releases) || releases.length === 0) {
5197
+ throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7248\u672C\u4FE1\u606F");
5032
5198
  }
5033
- const buildResult = buildConfig(rawContent, mode);
5034
- if (buildResult.warnings.length > 0) {
5035
- for (const warning of buildResult.warnings) {
5036
- console.log(`${colors.yellow("\u81EA\u52A8\u4FEE\u590D:")} ${warning}`);
5199
+ const stableReleases = releases.filter(
5200
+ (r) => !r.prerelease && !r.tag_name.toLowerCase().includes("alpha") && !r.tag_name.toLowerCase().includes("beta") && !r.tag_name.toLowerCase().includes("prerelease")
5201
+ );
5202
+ return stableReleases.length > 0 ? stableReleases[0] : releases[0];
5203
+ }
5204
+ async function checkUpdate(mirror) {
5205
+ const currentVersion = getKernelVersion();
5206
+ const latest = await getLatestRelease(GITHUB_REPO, mirror);
5207
+ const latestVersion = latest.tag_name;
5208
+ let needsUpdate = false;
5209
+ const currentDisplay = currentVersion || "\u672A\u5B89\u88C5";
5210
+ if (!currentVersion) {
5211
+ needsUpdate = true;
5212
+ } else {
5213
+ try {
5214
+ needsUpdate = compareVersions(latestVersion.replace(/^v/, ""), currentVersion.replace(/^v/, "")) > 0;
5215
+ } catch {
5216
+ needsUpdate = latestVersion !== currentVersion;
5037
5217
  }
5038
- console.log("");
5039
5218
  }
5040
- writeMihomoConfig(buildResult.config);
5041
- writeDebugConfig(buildResult);
5042
- const proxies = buildResult.config.proxies;
5043
- const proxyGroups = buildResult.config["proxy-groups"];
5044
5219
  return {
5045
- proxies: proxies ? proxies.length : 0,
5046
- proxyGroups: proxyGroups ? proxyGroups.length : 0
5220
+ current: currentDisplay,
5221
+ latest: latestVersion,
5222
+ needsUpdate,
5223
+ assets: latest.assets,
5224
+ release: latest
5047
5225
  };
5048
5226
  }
5049
- function needsAutoUpdate(sub) {
5050
- if (!sub.updated_at) return true;
5051
- const lastUpdate = new Date(sub.updated_at).getTime();
5052
- if (Number.isNaN(lastUpdate)) return true;
5053
- const intervalHours = resolveUpdateInterval(sub.url, sub.update_interval);
5054
- const intervalMs = intervalHours * 60 * 60 * 1e3;
5055
- return Date.now() - lastUpdate > intervalMs;
5056
- }
5057
- async function tryUpdateOne(sub, signal) {
5058
- try {
5059
- let info;
5060
- if (isMultiUrl(sub.url)) {
5061
- info = await downloadMergedSubscription(splitUrls(sub.url), sub.name, signal);
5062
- } else {
5063
- info = await downloadSubscription(sub.url, sub.name, signal);
5227
+ function findBinaryInDir(dir, maxDepth = 4) {
5228
+ if (maxDepth <= 0) return null;
5229
+ const files = fs7.readdirSync(dir);
5230
+ for (const f of files) {
5231
+ const fullPath = path5.join(dir, f);
5232
+ const stat = fs7.statSync(fullPath);
5233
+ if (stat.isDirectory()) {
5234
+ const found = findBinaryInDir(fullPath, maxDepth - 1);
5235
+ if (found) return found;
5236
+ continue;
5064
5237
  }
5065
- return { name: sub.name, success: true, proxies: info.proxies, proxyGroups: info.proxyGroups };
5066
- } catch (e) {
5067
- return { name: sub.name, success: false, error: e.message };
5238
+ if (f === "mihomo") return fullPath;
5239
+ if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
5068
5240
  }
5241
+ return null;
5069
5242
  }
5070
- function printUpdateResult(r) {
5071
- if (r.success) {
5072
- console.log(`${colors.green("\u2713")} ${r.name}: ${colors.green("\u5DF2\u66F4\u65B0")} (${formatProxySummary(r)})`);
5073
- } else {
5074
- console.log(`${colors.red("\u2717")} ${r.name}: ${colors.red("\u5931\u8D25")} (${(r.error || "").split("\n")[0]})`);
5243
+ async function downloadKernel(progressCallback, mirror, releaseInfo) {
5244
+ ensureDirs();
5245
+ const latest = releaseInfo || await getLatestRelease(GITHUB_REPO, mirror);
5246
+ const arch = getArch();
5247
+ const platform = process.platform;
5248
+ const asset = findMatchingAsset(latest.assets, platform, arch);
5249
+ if (!asset) {
5250
+ const available = latest.assets.map((a) => a.name).join(", ");
5251
+ let hint = "";
5252
+ if (available) hint = `
5253
+ \u53EF\u7528\u7248\u672C: ${available}`;
5254
+ throw new Error(`\u672A\u627E\u5230\u5339\u914D\u7684\u5185\u6838\u6587\u4EF6
5255
+ \u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
5075
5256
  }
5076
- }
5077
- async function autoUpdateStaleSubscription(options = {}) {
5078
- const allSubs = getSubscriptionsWithCache();
5079
- const staleSubs = allSubs.filter(needsAutoUpdate);
5080
- if (staleSubs.length === 0) {
5081
- return { total: 0, updated: 0, failed: 0 };
5257
+ const downloadUrl = withMirror(asset.browser_download_url, mirror);
5258
+ const tempPath = path5.join(DIRS.kernel, asset.name);
5259
+ const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
5260
+ if (mirror && progressCallback) {
5261
+ 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");
5082
5262
  }
5083
- if (staleSubs.length === 1) {
5084
- const sub = staleSubs[0];
5085
- const interval = resolveUpdateInterval(sub.url, sub.update_interval);
5086
- console.log(`\u8BA2\u9605 "${sub.name}" \u8D85\u8FC7 ${interval} \u5C0F\u65F6\u672A\u66F4\u65B0\uFF0C\u6B63\u5728\u66F4\u65B0...`);
5087
- } else {
5088
- console.log(`\u68C0\u67E5\u5230 ${staleSubs.length} \u4E2A\u8BA2\u9605\u9700\u8981\u66F4\u65B0\uFF0C\u6B63\u5728\u5E76\u884C\u66F4\u65B0...`);
5263
+ if (progressCallback) {
5264
+ progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
5089
5265
  }
5090
- const timeoutMs = options.timeout ?? DEFAULT_AUTO_UPDATE_TIMEOUT;
5091
- const controller = new AbortController();
5092
- let results;
5093
- try {
5094
- results = await withTimeout(Promise.all(staleSubs.map((sub) => tryUpdateOne(sub, controller.signal))), timeoutMs);
5095
- } catch (e) {
5096
- if (e instanceof TimeoutError) {
5097
- controller.abort();
5098
- console.log(colors.yellow(`\u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 (${timeoutMs / 1e3}s)\uFF0C\u8DF3\u8FC7\u66F4\u65B0\uFF0C\u4F7F\u7528\u7F13\u5B58\u914D\u7F6E`));
5099
- return { total: staleSubs.length, updated: 0, failed: staleSubs.length };
5266
+ const curlResult = spawnSync5(
5267
+ "curl",
5268
+ ["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
5269
+ { stdio: "inherit" }
5270
+ );
5271
+ if (curlResult.status !== 0) {
5272
+ try {
5273
+ fs7.unlinkSync(tempPath);
5274
+ } catch {
5100
5275
  }
5101
- throw e;
5276
+ throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
5102
5277
  }
5103
- let updatedCount = 0;
5104
- for (const r of results) {
5105
- if (r.success) updatedCount++;
5106
- printUpdateResult(r);
5278
+ if (!fs7.existsSync(tempPath)) {
5279
+ throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
5107
5280
  }
5108
- return { total: staleSubs.length, updated: updatedCount, failed: staleSubs.length - updatedCount };
5109
- }
5110
- var API_BASE = `http://${BASE_CONFIG["external-controller"]}`;
5111
- async function testProxyDelay(proxyName, timeout, testUrl, client, apiBase = API_BASE) {
5112
- const encodedName = encodeURIComponent(proxyName);
5113
- const url = `${apiBase}/proxies/${encodedName}/delay?timeout=${timeout}&url=${encodeURIComponent(testUrl)}`;
5281
+ if (progressCallback) {
5282
+ progressCallback("\u89E3\u538B\u5185\u6838...");
5283
+ }
5284
+ const extractPath = DIRS.kernel;
5285
+ let extractedBinary = null;
5114
5286
  try {
5115
- const response = await client.get(url);
5116
- const data = JSON.parse(response.data);
5117
- if (data.delay && data.delay > 0) {
5118
- return { name: proxyName, delay: data.delay };
5287
+ if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
5288
+ const tarResult = spawnSync5("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
5289
+ if (tarResult.error) throw tarResult.error;
5290
+ if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
5291
+ } else if (tempPath.endsWith(".gz")) {
5292
+ const baseName = path5.basename(tempPath, ".gz");
5293
+ const outputPath = path5.join(extractPath, baseName);
5294
+ const gzipResult = spawnSync5("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
5295
+ if (gzipResult.error) throw gzipResult.error;
5296
+ if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
5297
+ fs7.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
5298
+ extractedBinary = outputPath;
5119
5299
  }
5120
- return { name: proxyName, delay: null, error: data.message || "no delay" };
5121
5300
  } catch (e) {
5122
- const err = e;
5123
- let errorMsg = "timeout";
5124
- if (err.response?.data?.message) {
5125
- errorMsg = String(err.response.data.message);
5126
- } else if (err.message) {
5127
- errorMsg = err.message;
5301
+ try {
5302
+ fs7.unlinkSync(tempPath);
5303
+ } catch {
5128
5304
  }
5129
- return { name: proxyName, delay: null, error: errorMsg };
5305
+ throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
5130
5306
  }
5131
- }
5132
- async function testSubscriptionProxies(subName, options = {}) {
5133
- const { timeout = DEFAULT_TEST_TIMEOUT, concurrency = DEFAULT_TEST_CONCURRENCY, testUrl = DEFAULT_TEST_URL, apiBase = API_BASE, onResult } = options;
5134
- const { proxies } = options.parsed || loadSubscriptionConfig(subName);
5135
- if (proxies.length === 0) {
5136
- return { total: 0, alive: 0, dead: 0, results: [] };
5307
+ const foundBinary = extractedBinary || findBinaryInDir(extractPath);
5308
+ if (!foundBinary) {
5309
+ try {
5310
+ fs7.unlinkSync(tempPath);
5311
+ } catch {
5312
+ }
5313
+ throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
5137
5314
  }
5138
- const client = createHttpClient({ timeout: timeout + 3e3 });
5139
- const results = new Array(proxies.length);
5140
- let completedCount = 0;
5141
- let nextIndex = 0;
5142
- async function runNext() {
5143
- while (nextIndex < proxies.length) {
5144
- const idx = nextIndex++;
5145
- const result = await testProxyDelay(proxies[idx].name, timeout, testUrl, client, apiBase);
5146
- results[idx] = result;
5147
- onResult?.(result, completedCount, proxies.length);
5148
- completedCount++;
5315
+ const targetPath = PATHS.mihomoBinary;
5316
+ if (foundBinary !== targetPath) {
5317
+ if (fs7.existsSync(targetPath)) {
5318
+ fs7.chmodSync(targetPath, 493);
5319
+ try {
5320
+ fs7.unlinkSync(targetPath);
5321
+ } catch {
5322
+ }
5149
5323
  }
5324
+ fs7.renameSync(foundBinary, targetPath);
5150
5325
  }
5151
- const workers = Array.from({ length: Math.min(concurrency, proxies.length) }, () => runNext());
5152
- await Promise.all(workers);
5153
- const alive = results.filter((r) => r.delay !== null).length;
5154
- return { total: results.length, alive, dead: results.length - alive, results };
5155
- }
5156
- function normalizeProxyNamesBeforeSave(parsed) {
5157
- const { proxies, proxyGroups } = parsed;
5158
- const renameMap = /* @__PURE__ */ new Map();
5159
- const usedNames = /* @__PURE__ */ new Set();
5160
- for (const proxy of proxies) {
5161
- const shortened = proxy.name.replace(/_github\.com\/[^_]+/, "");
5162
- if (shortened !== proxy.name && !usedNames.has(shortened)) {
5163
- renameMap.set(proxy.name, shortened);
5164
- usedNames.add(shortened);
5165
- } else {
5166
- usedNames.add(proxy.name);
5326
+ fs7.chmodSync(targetPath, 493);
5327
+ if (progressCallback) {
5328
+ progressCallback("\u6821\u9A8C\u5185\u6838...");
5329
+ }
5330
+ const check = spawnSync5(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
5331
+ const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
5332
+ if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
5333
+ try {
5334
+ fs7.unlinkSync(targetPath);
5335
+ } catch {
5167
5336
  }
5337
+ try {
5338
+ fs7.unlinkSync(tempPath);
5339
+ } catch {
5340
+ }
5341
+ throw new Error(`\u5185\u6838\u81EA\u68C0\u5931\u8D25\uFF08\u53EF\u80FD\u4E0B\u8F7D\u635F\u574F\u6216\u67B6\u6784\u4E0D\u5339\u914D\uFF09\uFF0C\u5DF2\u5220\u9664
5342
+ \u9000\u51FA\u7801: ${check.status}
5343
+ \u8F93\u51FA: ${checkOutput || "(\u7A7A)"}`);
5344
+ }
5345
+ try {
5346
+ fs7.unlinkSync(tempPath);
5347
+ } catch {
5348
+ }
5349
+ clearKernelVersionCache();
5350
+ return { version: latest.tag_name, path: targetPath };
5351
+ }
5352
+
5353
+ // src/commands/kernel.ts
5354
+ async function cmdKernel(args) {
5355
+ const mirrorInfo = parseMirrorArg(args);
5356
+ const effectiveMirror = mirrorInfo.mirror;
5357
+ if (effectiveMirror) {
5358
+ const mirrorDesc = mirrorInfo.type === "all" ? " (API\u548C\u4E0B\u8F7D\u5747\u4F7F\u7528\u955C\u50CF)" : " (\u4E0B\u8F7D\u65F6\u4F7F\u7528\u955C\u50CF)";
5359
+ console.log(`\u955C\u50CF: ${effectiveMirror}${mirrorDesc}`);
5168
5360
  }
5169
- if (renameMap.size === 0) return 0;
5170
- for (const proxy of proxies) {
5171
- const newName = renameMap.get(proxy.name);
5172
- if (newName) proxy.name = newName;
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)" : ""}`);
5173
5372
  }
5174
- for (const group of proxyGroups) {
5175
- if (Array.isArray(group.proxies)) {
5176
- group.proxies = group.proxies.map((name) => renameMap.get(name) || name);
5373
+ console.log("");
5374
+ console.log("\u68C0\u67E5\u5185\u6838\u66F4\u65B0...");
5375
+ try {
5376
+ const apiMirror = mirrorInfo.type === "all" ? effectiveMirror : null;
5377
+ const info = await checkUpdate(apiMirror);
5378
+ console.log(`\u5F53\u524D: ${info.current}`);
5379
+ console.log(`\u6700\u65B0: ${info.latest}`);
5380
+ if (!info.needsUpdate) {
5381
+ console.log("\u5DF2\u662F\u6700\u65B0\u7248\u672C");
5382
+ } else {
5383
+ console.log("\n\u6B63\u5728\u4E0B\u8F7D...");
5384
+ const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
5385
+ console.log(`
5386
+ \u5DF2\u66F4\u65B0\u5230 ${result.version}`);
5177
5387
  }
5178
- }
5179
- return renameMap.size;
5180
- }
5181
- function cleanDeadProxies(parsed, deadNames) {
5182
- const { proxies, proxyGroups } = parsed;
5183
- const originalCount = proxies.length;
5184
- parsed.proxies = proxies.filter((p) => !deadNames.has(p.name));
5185
- const removedProxies = originalCount - parsed.proxies.length;
5186
- let updatedGroups = 0;
5187
- const removedGroupNames = /* @__PURE__ */ new Set();
5188
- for (const group of proxyGroups) {
5189
- if (Array.isArray(group.proxies)) {
5190
- const before = group.proxies.length;
5191
- group.proxies = group.proxies.filter((name) => !deadNames.has(name));
5192
- if (group.proxies.length < before) {
5193
- updatedGroups++;
5388
+ } catch (e) {
5389
+ console.error(`
5390
+ \u66F4\u65B0\u5931\u8D25: ${e.message}`);
5391
+ const err = e;
5392
+ if (err.response?.data) {
5393
+ if (err.response.data.message) {
5394
+ console.error(`\u539F\u56E0: ${err.response.data.message}`);
5194
5395
  }
5195
- if (group.proxies.length === 0) {
5196
- removedGroupNames.add(group.name);
5396
+ if (err.response.data.documentation_url) {
5397
+ console.error(`\u6587\u6863: ${err.response.data.documentation_url}`);
5197
5398
  }
5198
5399
  }
5400
+ process.exit(1);
5199
5401
  }
5200
- if (removedGroupNames.size > 0) {
5201
- parsed.proxyGroups = proxyGroups.filter((g) => !removedGroupNames.has(g.name));
5202
- for (const group of parsed.proxyGroups) {
5203
- if (Array.isArray(group.proxies)) {
5204
- group.proxies = group.proxies.filter((name) => !removedGroupNames.has(name));
5205
- }
5206
- }
5207
- const rules = parsed.raw.rules;
5208
- if (Array.isArray(rules)) {
5209
- parsed.raw.rules = rules.filter((rule) => {
5210
- if (typeof rule !== "string") return true;
5211
- const parts = rule.split(",");
5212
- if (parts.length < 2) return true;
5213
- return !removedGroupNames.has(parts[parts.length - 1].trim());
5214
- });
5215
- }
5402
+ }
5403
+
5404
+ // src/commands/log.ts
5405
+ function cmdLog(args) {
5406
+ const logPath = getLogPath();
5407
+ if (hasFlag(args, "-o", "--open")) {
5408
+ openLogFile(logPath);
5409
+ return;
5216
5410
  }
5217
- return { removedProxies, updatedGroups, removedGroups: removedGroupNames.size };
5411
+ viewLogWithTail(logPath, { follow: true, lines: 50 });
5218
5412
  }
5219
- async function autoCleanSubscription(subName, options = {}) {
5220
- const parsed = loadSubscriptionConfig(subName);
5221
- const { onResult, onRetryRound, rounds = DEFAULT_CLEAN_ROUNDS, ...testOptions } = options;
5222
- const wrapOnResult = (round) => onResult ? (r, i, t) => onResult(r, i, t, round) : void 0;
5223
- const summary = await testSubscriptionProxies(subName, {
5224
- ...testOptions,
5225
- parsed,
5226
- onResult: wrapOnResult(1)
5227
- });
5228
- let removedProxies = 0;
5229
- let updatedGroups = 0;
5230
- let removedGroups = 0;
5231
- let skipped = false;
5232
- if (summary.dead > 0) {
5233
- if (summary.alive === 0 || summary.alive / summary.total < 0.01) {
5234
- skipped = true;
5413
+ function cmdLogs(args) {
5414
+ const targetName = getNonFlagArg(args, 1);
5415
+ const lines = parseIntArg(args, "-n", "--lines", 100);
5416
+ const openInViewer = hasFlag(args, "-o", "--open");
5417
+ if (targetName) {
5418
+ let logPath;
5419
+ if (targetName === "current" || targetName === "0") {
5420
+ logPath = getLogPath();
5235
5421
  } else {
5236
- const deadNames = new Set(summary.results.filter((r) => r.delay === null).map((r) => r.name));
5237
- const deadProxies = parsed.proxies.filter((p) => deadNames.has(p.name));
5238
- for (let retry = 0; retry < rounds - 1; retry++) {
5239
- const round = retry + 2;
5240
- const retryTargets = deadProxies.filter((p) => deadNames.has(p.name));
5241
- if (retryTargets.length === 0) break;
5242
- onRetryRound?.(round, retryTargets.length);
5243
- const retryParsed = { raw: {}, proxies: retryTargets, proxyGroups: [] };
5244
- const retrySummary = await testSubscriptionProxies(subName, {
5245
- ...testOptions,
5246
- parsed: retryParsed,
5247
- onResult: wrapOnResult(round)
5248
- });
5249
- for (const r of retrySummary.results) {
5250
- if (r.delay !== null) {
5251
- deadNames.delete(r.name);
5252
- }
5422
+ const parsedIdx = parseInt(targetName, 10);
5423
+ if (!Number.isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
5424
+ const archiveLogs = listLogs();
5425
+ const archive = archiveLogs.archives[parsedIdx - 1];
5426
+ if (!archive) {
5427
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
5428
+ console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5429
+ process.exit(1);
5253
5430
  }
5431
+ logPath = archive.path;
5432
+ } else {
5433
+ logPath = getLogPathByName(targetName);
5254
5434
  }
5255
- summary.dead = deadNames.size;
5256
- summary.alive = summary.total - summary.dead;
5257
- if (deadNames.size > 0) {
5258
- const cleanResult = cleanDeadProxies(parsed, deadNames);
5259
- removedProxies = cleanResult.removedProxies;
5260
- updatedGroups = cleanResult.updatedGroups;
5261
- removedGroups = cleanResult.removedGroups;
5262
- }
5263
5435
  }
5436
+ if (!logPath) {
5437
+ console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u65E5\u5FD7 "${targetName}"`);
5438
+ console.log('\u4F7F\u7528 "mihomo logs" \u67E5\u770B\u53EF\u7528\u65E5\u5FD7\u5217\u8868');
5439
+ process.exit(1);
5440
+ }
5441
+ if (openInViewer) {
5442
+ openLogFile(logPath);
5443
+ return;
5444
+ }
5445
+ viewLogWithTail(logPath, { follow: false, lines });
5446
+ return;
5264
5447
  }
5265
- if (!skipped && removedProxies > 0) {
5266
- saveSubscriptionConfig(subName, parsed);
5448
+ const logs = listLogs();
5449
+ const all = [];
5450
+ if (logs.current) all.push(logs.current);
5451
+ all.push(...logs.archives);
5452
+ if (all.length === 0) {
5453
+ console.log("\u6682\u65E0\u65E5\u5FD7");
5454
+ return;
5267
5455
  }
5268
- return { summary, removedProxies, updatedGroups, removedGroups, skipped };
5456
+ console.log("");
5457
+ console.log("\u65E5\u5FD7\u5217\u8868:");
5458
+ console.log("");
5459
+ let archiveCounter = 0;
5460
+ for (const log of all) {
5461
+ let num;
5462
+ if (log.isCurrent) {
5463
+ num = " 0";
5464
+ } else {
5465
+ archiveCounter++;
5466
+ num = archiveCounter < 10 ? ` ${archiveCounter}` : `${archiveCounter}`;
5467
+ }
5468
+ const time = formatDate(log.mtime);
5469
+ const size = formatBytes(log.size);
5470
+ const name = log.isCurrent ? "mihomo.log (\u5F53\u524D\u8FD0\u884C\u4E2D)" : log.name;
5471
+ console.log(` ${num}. ${name}`);
5472
+ console.log(` \u65F6\u95F4: ${time} \u5927\u5C0F: ${size}`);
5473
+ if (!log.isCurrent) {
5474
+ console.log(` \u67E5\u770B: mihomo logs ${archiveCounter} \u6216 mihomo logs ${archiveCounter} -o`);
5475
+ }
5476
+ console.log("");
5477
+ }
5478
+ console.log("\u7528\u6CD5:");
5479
+ console.log(" mihomo logs 0 # \u67E5\u770B\u5F53\u524D\u65E5\u5FD7 (\u6700\u540E 100 \u884C)");
5480
+ console.log(" mihomo logs 1 # \u67E5\u770B\u7B2C 1 \u4E2A\u5F52\u6863\u65E5\u5FD7\uFF08\u6700\u65B0\uFF09");
5481
+ console.log(" mihomo logs 1 -n 200 # \u67E5\u770B 200 \u884C");
5482
+ console.log(" mihomo logs 1 -o # \u7528\u7CFB\u7EDF\u9ED8\u8BA4\u7A0B\u5E8F\u6253\u5F00");
5483
+ console.log("");
5269
5484
  }
5270
5485
 
5486
+ // src/commands/overwrite.ts
5487
+ import path7 from "path";
5488
+
5271
5489
  // src/commands/status.ts
5272
5490
  function printStatus() {
5273
5491
  const status = getStatus();
5492
+ const daemon = getDaemonStatus();
5274
5493
  const info = getConfigInfo();
5275
5494
  const overwriteEnabled = isOverwriteEnabled();
5276
5495
  const overwriteFiles = listOverwriteFile().files;
5277
5496
  const activeSub = getActiveSubscription();
5497
+ const running = daemon.enabled ? isDaemonRunning(daemon) : status.running;
5498
+ const pid = daemon.enabled ? daemon.pid : status.pid;
5278
5499
  console.log("");
5279
5500
  let modeLabel = "";
5280
- if (info && status.running) {
5501
+ if (info && running) {
5281
5502
  modeLabel = colors.cyan(info.tun ? " (TUN)" : " (Mixed)");
5282
5503
  }
5283
- const statusText = status.running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
5504
+ const statusText = running ? colors.green("\u25CF \u8FD0\u884C\u4E2D") : colors.yellow("\u4E0D\u5728\u8FD0\u884C");
5284
5505
  console.log(`${colors.gray("\u72B6\u6001: ")}${statusText}${modeLabel}`);
5285
5506
  console.log(`${colors.gray("\u5185\u6838: ")}${status.kernelVersion || "\u672A\u5B89\u88C5"}`);
5286
- if (status.pid) {
5287
- console.log(`${colors.gray("PID: ")}${status.pid}`);
5288
- if (status.processInfo) {
5507
+ if (pid) {
5508
+ console.log(`${colors.gray("PID: ")}${pid}`);
5509
+ if (!daemon.enabled && status.processInfo) {
5289
5510
  console.log(`${colors.gray("\u5185\u5B58: ")}${status.processInfo.memory}`);
5290
5511
  }
5291
5512
  }
@@ -5316,6 +5537,9 @@ function printStatus() {
5316
5537
  } else {
5317
5538
  console.log(`${colors.gray("\u8986\u5199: ")}${colors.yellow("\u5DF2\u7981\u7528")}`);
5318
5539
  }
5540
+ if (daemon.enabled) {
5541
+ console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
5542
+ }
5319
5543
  console.log("");
5320
5544
  }
5321
5545
 
@@ -5328,6 +5552,11 @@ function handleStopResult(result) {
5328
5552
  }
5329
5553
  }
5330
5554
  async function cmdStop() {
5555
+ if (isDaemonEnabled()) {
5556
+ console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u4EE3\u7406\u7531 launchd \u6258\u7BA1"));
5557
+ console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
5558
+ return;
5559
+ }
5331
5560
  const pids = getAllMihomoPids();
5332
5561
  if (pids.length === 0) {
5333
5562
  console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
@@ -5340,8 +5569,8 @@ async function cmdStop() {
5340
5569
 
5341
5570
  // src/test-instance.ts
5342
5571
  import { spawn as spawn2 } from "child_process";
5343
- import fs7 from "fs";
5344
- import path5 from "path";
5572
+ import fs8 from "fs";
5573
+ import path6 from "path";
5345
5574
 
5346
5575
  // src/lifecycle.ts
5347
5576
  var cleanupFns = /* @__PURE__ */ new Set();
@@ -5362,20 +5591,20 @@ function runCleanup() {
5362
5591
  }
5363
5592
 
5364
5593
  // src/test-instance.ts
5365
- var TEST_DIR = path5.join(USER_DATA_DIR, "test");
5594
+ var TEST_DIR = path6.join(USER_DATA_DIR, "test");
5366
5595
  var TEST_DIRS = {
5367
- data: path5.join(TEST_DIR, "data"),
5368
- runtime: path5.join(TEST_DIR, "runtime")
5596
+ data: path6.join(TEST_DIR, "data"),
5597
+ runtime: path6.join(TEST_DIR, "runtime")
5369
5598
  };
5370
5599
  var TEST_PATHS = {
5371
- configFile: path5.join(TEST_DIRS.runtime, "config.yaml"),
5372
- pidFile: path5.join(TEST_DIRS.runtime, "pid"),
5373
- logFile: path5.join(TEST_DIR, "test.log")
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")
5374
5603
  };
5375
5604
  var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
5376
5605
  function ensureTestDirs() {
5377
5606
  for (const dir of Object.values(TEST_DIRS)) {
5378
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
5607
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
5379
5608
  }
5380
5609
  }
5381
5610
  function cleanupTestDir() {
@@ -5413,21 +5642,21 @@ function buildTestConfig(subName) {
5413
5642
  rules: ["MATCH,PROXY"]
5414
5643
  };
5415
5644
  const content = dump(config, { indent: 2, lineWidth: -1, schema: CORE_SCHEMA });
5416
- fs7.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
5645
+ fs8.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
5417
5646
  }
5418
5647
  async function startTestInstance() {
5419
5648
  const binary = PATHS.mihomoBinary;
5420
- if (!fs7.existsSync(binary)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
5649
+ if (!fs8.existsSync(binary)) throw new Error("\u672A\u627E\u5230 mihomo \u5185\u6838");
5421
5650
  stopTestInstance();
5422
- const logFd = fs7.openSync(TEST_PATHS.logFile, "a");
5651
+ const logFd = fs8.openSync(TEST_PATHS.logFile, "a");
5423
5652
  const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
5424
5653
  detached: true,
5425
5654
  stdio: ["ignore", logFd, logFd]
5426
5655
  });
5427
- fs7.closeSync(logFd);
5656
+ fs8.closeSync(logFd);
5428
5657
  child.unref();
5429
5658
  const pid = child.pid;
5430
- fs7.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
5659
+ fs8.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
5431
5660
  const client = createHttpClient({ timeout: 2e3 });
5432
5661
  let ready = false;
5433
5662
  for (let i = 0; i < 60; i++) {
@@ -5443,7 +5672,7 @@ async function startTestInstance() {
5443
5672
  if (!isProcessRunning(pid)) {
5444
5673
  let errorDetail = "";
5445
5674
  try {
5446
- errorDetail = fs7.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
5675
+ errorDetail = fs8.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
5447
5676
  } catch {
5448
5677
  }
5449
5678
  throw new Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
@@ -5456,7 +5685,7 @@ ${errorDetail}` : ""}`);
5456
5685
  function stopTestInstance() {
5457
5686
  let pid;
5458
5687
  try {
5459
- pid = parseInt(fs7.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
5688
+ pid = parseInt(fs8.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
5460
5689
  } catch {
5461
5690
  return;
5462
5691
  }
@@ -5468,7 +5697,7 @@ function stopTestInstance() {
5468
5697
  }
5469
5698
  }
5470
5699
  try {
5471
- fs7.unlinkSync(TEST_PATHS.pidFile);
5700
+ fs8.unlinkSync(TEST_PATHS.pidFile);
5472
5701
  } catch {
5473
5702
  }
5474
5703
  }
@@ -5744,7 +5973,7 @@ async function cmdSubscription(args) {
5744
5973
  }
5745
5974
  const status = getStatus();
5746
5975
  const configInfo = getConfigInfo();
5747
- const currentMode = configInfo?.tun ? "tun" : "mixed";
5976
+ const currentMode = isDaemonEnabled() ? "mixed" : configInfo?.tun ? "tun" : "mixed";
5748
5977
  const success = setDefaultSubscription(target.name);
5749
5978
  if (success) {
5750
5979
  console.log(`\u5DF2\u5207\u6362\u5230 "${target.name}"`);
@@ -5752,7 +5981,7 @@ async function cmdSubscription(args) {
5752
5981
  console.error(`\u9519\u8BEF: \u672A\u627E\u5230\u8BA2\u9605 "${name}"`);
5753
5982
  process.exit(1);
5754
5983
  }
5755
- if (status.running) {
5984
+ if (status.running || isDaemonEnabled()) {
5756
5985
  console.log("");
5757
5986
  await cmdStart(["start", currentMode]);
5758
5987
  return;
@@ -5878,12 +6107,19 @@ async function cmdSubscription(args) {
5878
6107
  }
5879
6108
 
5880
6109
  // src/commands/start.ts
6110
+ var DAEMON_RESTART_WAIT_MS = 500;
5881
6111
  async function cmdStart(args) {
5882
6112
  if (!hasKernel()) {
5883
6113
  console.error('\u9519\u8BEF: \u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
5884
6114
  process.exit(1);
5885
6115
  }
5886
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");
6121
+ process.exit(1);
6122
+ }
5887
6123
  const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
5888
6124
  const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
5889
6125
  const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
@@ -5897,16 +6133,27 @@ async function cmdStart(args) {
5897
6133
  if (!skipUpdate) {
5898
6134
  await autoUpdateStaleSubscription({ timeout: updateTimeout });
5899
6135
  }
5900
- const status = getStatus();
5901
- const hasProcess = status.running || status.allProcesses.length > 0;
5902
- if (hasProcess) {
5903
- const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
5904
- console.log(`\u505C\u6B62 ${count} \u4E2A\u8FDB\u7A0B...`);
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;
5905
6144
  }
5906
- handleStopResult(stop());
5907
- if (hasProcess) {
5908
- console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
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...`);
6151
+ }
6152
+ handleStopResult(stop());
6153
+ if (hasProcess) {
6154
+ console.log(`${colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B")}
5909
6155
  `);
6156
+ }
5910
6157
  }
5911
6158
  let configInfo;
5912
6159
  try {
@@ -5918,8 +6165,9 @@ async function cmdStart(args) {
5918
6165
  const modeLabel = targetMode === "tun" ? "TUN" : "Mixed";
5919
6166
  console.log([colors.cyan(modeLabel), sub.name, formatProxySummary(configInfo)].join(" \xB7 "));
5920
6167
  try {
5921
- const result = await start(targetMode);
5922
- console.log(`${colors.green("\u5DF2\u542F\u52A8")} (PID ${result.pid})`);
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})` : ""}`);
5923
6171
  } catch (e) {
5924
6172
  const msg = e.message;
5925
6173
  const lines = msg.split("\n");
@@ -5951,11 +6199,11 @@ async function cmdStart(args) {
5951
6199
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(cleanResult)}`);
5952
6200
  console.log("");
5953
6201
  console.log("\u91CD\u65B0\u52A0\u8F7D\u914D\u7F6E...");
5954
- handleStopResult(stop());
6202
+ if (!daemonEnabled) handleStopResult(stop());
5955
6203
  try {
5956
6204
  configInfo = prepareConfigForStart(targetMode, sub.name);
5957
- const result = await start(targetMode);
5958
- console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${result.pid}) \xB7 ${formatProxySummary(configInfo)}`);
6205
+ const pid = await launchOrRestart();
6206
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")}${pid ? ` (PID ${pid})` : ""} \xB7 ${formatProxySummary(configInfo)}`);
5959
6207
  } catch (e) {
5960
6208
  console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
5961
6209
  process.exit(1);
@@ -5975,8 +6223,8 @@ function printOverwriteList() {
5975
6223
  if (info.files.length === 0) {
5976
6224
  console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
5977
6225
  console.log("");
5978
- console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path6.join(info.dir, "overwrite.yaml")}`);
5979
- console.log(` \u6216 ${path6.join(info.dir, "overwrite.dns.yaml")}`);
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")}`);
5980
6228
  console.log("");
5981
6229
  } else {
5982
6230
  console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
@@ -5998,7 +6246,7 @@ async function cmdOverwrite(args) {
5998
6246
  const action = args?.[1];
5999
6247
  const status = getStatus();
6000
6248
  const configInfo = getConfigInfo();
6001
- const currentMode = configInfo?.tun ? "tun" : "mixed";
6249
+ const currentMode = isDaemonEnabled() ? "mixed" : configInfo?.tun ? "tun" : "mixed";
6002
6250
  if (action === "on" || action === "enable") {
6003
6251
  if (isOverwriteEnabled()) {
6004
6252
  console.log("\u8986\u5199\u914D\u7F6E\u5DF2\u662F\u542F\u7528\u72B6\u6001");
@@ -6008,7 +6256,7 @@ async function cmdOverwrite(args) {
6008
6256
  }
6009
6257
  setOverwriteEnabled(true);
6010
6258
  console.log("\u5DF2\u542F\u7528\u8986\u5199\u914D\u7F6E");
6011
- if (status.running) {
6259
+ if (status.running || isDaemonEnabled()) {
6012
6260
  console.log("");
6013
6261
  await cmdStart(["start", currentMode]);
6014
6262
  return;
@@ -6026,7 +6274,7 @@ async function cmdOverwrite(args) {
6026
6274
  }
6027
6275
  setOverwriteEnabled(false);
6028
6276
  console.log("\u5DF2\u7981\u7528\u8986\u5199\u914D\u7F6E");
6029
- if (status.running) {
6277
+ if (status.running || isDaemonEnabled()) {
6030
6278
  console.log("");
6031
6279
  await cmdStart(["start", currentMode]);
6032
6280
  return;
@@ -6040,7 +6288,7 @@ async function cmdOverwrite(args) {
6040
6288
  }
6041
6289
 
6042
6290
  // src/commands/reset.ts
6043
- import fs8 from "fs";
6291
+ import fs9 from "fs";
6044
6292
  import readline from "readline";
6045
6293
  var RESET_TARGETS = [
6046
6294
  {
@@ -6095,10 +6343,22 @@ var RESET_TARGETS = [
6095
6343
  label: "\u8986\u5199",
6096
6344
  paths: () => {
6097
6345
  const dir = USER_DATA_DIR;
6098
- if (!fs8.existsSync(dir)) return [];
6099
- return fs8.readdirSync(dir).filter((f) => f === "overwrite.yaml" || /^overwrite\..+\.ya?ml$/.test(f)).map((f) => `${dir}/${f}`);
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}`);
6100
6348
  },
6101
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"
6102
6362
  }
6103
6363
  ];
6104
6364
  function resolveResetTargets(names) {
@@ -6148,7 +6408,7 @@ async function cmdReset(args) {
6148
6408
  }
6149
6409
  targets = matched;
6150
6410
  } else {
6151
- targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites"].includes(t.id));
6411
+ targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
6152
6412
  }
6153
6413
  for (const t of targets) {
6154
6414
  if (t.checkEmpty?.()) {
@@ -6160,25 +6420,34 @@ async function cmdReset(args) {
6160
6420
  }
6161
6421
  const needsStop = targets.some((t) => t.needsStop);
6162
6422
  const warnRunning = targets.some((t) => t.warnIfRunning);
6423
+ const kernelTargeted = targets.some((t) => t.id === "kernel");
6424
+ const disablesDaemon = needsStop || kernelTargeted;
6163
6425
  const pids = needsStop || warnRunning ? getAllMihomoPids() : [];
6164
- if (needsStop && pids.length > 0) {
6165
- console.log(`\u505C\u6B62 ${pids.length} \u4E2A\u8FDB\u7A0B...`);
6166
- cleanupAll();
6167
- for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
6168
- if (getAllMihomoPids().length === 0) break;
6169
- await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
6170
- }
6171
- } else if (warnRunning && pids.length > 0) {
6426
+ if (warnRunning && pids.length > 0) {
6172
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`));
6173
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
+ }
6174
6432
  console.log(`\u5C06\u5220\u9664: ${targets.map((t) => t.label).join("\u3001")}`);
6175
6433
  if (!skipConfirm && !await confirmPrompt("\u786E\u8BA4?")) {
6176
6434
  console.log("\u5DF2\u53D6\u6D88");
6177
6435
  return;
6178
6436
  }
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));
6446
+ }
6447
+ }
6179
6448
  for (const t of targets) {
6180
6449
  for (const p of t.paths()) {
6181
- if (fs8.existsSync(p)) {
6450
+ if (fs9.existsSync(p)) {
6182
6451
  try {
6183
6452
  rmrf(p);
6184
6453
  } catch (e) {
@@ -6197,6 +6466,13 @@ async function cmdReset(args) {
6197
6466
 
6198
6467
  // src/commands/test.ts
6199
6468
  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
+ }
6200
6476
  const status = getStatus();
6201
6477
  if (!status.running) {
6202
6478
  console.error("\u9519\u8BEF: mihomo \u672A\u8FD0\u884C\uFF0C\u8BF7\u5148\u542F\u52A8 (mihomo start)");
@@ -6256,11 +6532,22 @@ async function cmdClean(args) {
6256
6532
  console.log(`${colors.green("\u5DF2\u6E05\u7406")}: ${formatCleanSummary(result)}`);
6257
6533
  console.log("");
6258
6534
  console.log("\u91CD\u542F mihomo \u4F7F\u66F4\u6539\u751F\u6548...");
6259
- const currentMode = getConfigInfo()?.tun ? "tun" : "mixed";
6260
- handleStopResult(stop());
6261
- const configInfo = prepareConfigForStart(currentMode, activeSub.name);
6262
- const startResult = await start(currentMode);
6263
- console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${startResult.pid}) \xB7 ${formatProxySummary(configInfo)}`);
6535
+ if (isDaemonEnabled()) {
6536
+ const configInfo = prepareConfigForStart("mixed", activeSub.name);
6537
+ try {
6538
+ restartDaemon();
6539
+ } catch (e) {
6540
+ console.error(`${colors.red("\u91CD\u542F\u5931\u8D25:")} ${e.message.split("\n")[0]}`);
6541
+ process.exit(1);
6542
+ }
6543
+ console.log(`${colors.green("\u5DF2\u91CD\u542F (\u4FDD\u6D3B)")} \xB7 ${formatProxySummary(configInfo)}`);
6544
+ } else {
6545
+ const currentMode = getConfigInfo()?.tun ? "tun" : "mixed";
6546
+ handleStopResult(stop());
6547
+ const configInfo = prepareConfigForStart(currentMode, activeSub.name);
6548
+ const startResult = await start(currentMode);
6549
+ console.log(`${colors.green("\u5DF2\u91CD\u542F")} (PID ${startResult.pid}) \xB7 ${formatProxySummary(configInfo)}`);
6550
+ }
6264
6551
  }
6265
6552
  }
6266
6553
 
@@ -6422,6 +6709,9 @@ async function main() {
6422
6709
  case "reset":
6423
6710
  await cmdReset(args);
6424
6711
  break;
6712
+ case "daemon":
6713
+ await cmdDaemon(args);
6714
+ break;
6425
6715
  case "on":
6426
6716
  await cmdOverwrite(["ow", "on"]);
6427
6717
  break;