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