mihomo-cli 3.6.0 → 3.8.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 +75 -0
- package/README.md +115 -17
- package/dist/index.js +1131 -176
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -3067,7 +3067,14 @@ var UI_URLS = {
|
|
|
3067
3067
|
dash: "https://metacubex.github.io/metacubexd",
|
|
3068
3068
|
yacd: "https://yacd.metacubex.one"
|
|
3069
3069
|
};
|
|
3070
|
-
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;
|
|
3071
3078
|
var CONTROLLER_PORT = 9090;
|
|
3072
3079
|
var CONTROLLER_ADDR = `127.0.0.1:${CONTROLLER_PORT}`;
|
|
3073
3080
|
var CONTROLLER_BASE_URL = `http://${CONTROLLER_ADDR}`;
|
|
@@ -3120,6 +3127,41 @@ var AUTO_CLEAN_THRESHOLD = 100;
|
|
|
3120
3127
|
var AUTO_CLEAN_THRESHOLD_GITHUB = 50;
|
|
3121
3128
|
var AUTO_CLEAN_COOLDOWN_HOURS = 12;
|
|
3122
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
|
+
|
|
3123
3165
|
// src/overwrite.ts
|
|
3124
3166
|
import fs3 from "fs";
|
|
3125
3167
|
import path3 from "path";
|
|
@@ -3140,7 +3182,11 @@ var DIRS = {
|
|
|
3140
3182
|
subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
|
|
3141
3183
|
logs: path.join(USER_DATA_DIR, "logs"),
|
|
3142
3184
|
data: path.join(USER_DATA_DIR, "data"),
|
|
3143
|
-
runtime: path.join(USER_DATA_DIR, "runtime")
|
|
3185
|
+
runtime: path.join(USER_DATA_DIR, "runtime"),
|
|
3186
|
+
// 隧道运行态。刻意独立于 runtime/:clearRuntime() 会在 stop() 成功路径 rmrf 整个
|
|
3187
|
+
// runtime 目录,隧道状态放那里会被 `mihomo stop` 连同 config.yaml 一起抹掉,
|
|
3188
|
+
// 于是「谁起的」标记丢失、手动起的隧道再也无法被识别
|
|
3189
|
+
tunnel: path.join(USER_DATA_DIR, "tunnel")
|
|
3144
3190
|
};
|
|
3145
3191
|
var PATHS = {
|
|
3146
3192
|
mihomoBinary: path.join(DIRS.kernel, "mihomo"),
|
|
@@ -3191,62 +3237,32 @@ function rmrf(dir) {
|
|
|
3191
3237
|
// src/settings.ts
|
|
3192
3238
|
import fs2 from "fs";
|
|
3193
3239
|
import path2 from "path";
|
|
3194
|
-
|
|
3195
|
-
// src/errors.ts
|
|
3196
|
-
var TimeoutError = class extends Error {
|
|
3197
|
-
constructor() {
|
|
3198
|
-
super("timeout");
|
|
3199
|
-
this.name = "TimeoutError";
|
|
3200
|
-
}
|
|
3201
|
-
};
|
|
3202
|
-
var CliError = class extends Error {
|
|
3203
|
-
hint;
|
|
3204
|
-
label;
|
|
3205
|
-
exitCode;
|
|
3206
|
-
constructor(message, options = {}) {
|
|
3207
|
-
super(message);
|
|
3208
|
-
this.name = "CliError";
|
|
3209
|
-
this.label = options.label ?? "\u9519\u8BEF";
|
|
3210
|
-
this.hint = options.hint === void 0 ? [] : Array.isArray(options.hint) ? options.hint : [options.hint];
|
|
3211
|
-
this.exitCode = options.exitCode ?? 1;
|
|
3212
|
-
}
|
|
3213
|
-
};
|
|
3214
|
-
function withTimeout(promise, ms) {
|
|
3215
|
-
return new Promise((resolve, reject) => {
|
|
3216
|
-
const timer = setTimeout(() => reject(new TimeoutError()), ms);
|
|
3217
|
-
promise.then(
|
|
3218
|
-
(v) => {
|
|
3219
|
-
clearTimeout(timer);
|
|
3220
|
-
resolve(v);
|
|
3221
|
-
},
|
|
3222
|
-
(e) => {
|
|
3223
|
-
clearTimeout(timer);
|
|
3224
|
-
reject(e);
|
|
3225
|
-
}
|
|
3226
|
-
);
|
|
3227
|
-
});
|
|
3228
|
-
}
|
|
3229
|
-
|
|
3230
|
-
// src/settings.ts
|
|
3231
3240
|
var settingsCache = null;
|
|
3232
3241
|
function readSettings() {
|
|
3233
3242
|
if (settingsCache !== null) return settingsCache;
|
|
3234
3243
|
ensureDirs();
|
|
3235
3244
|
if (fs2.existsSync(PATHS.settingsFile)) {
|
|
3245
|
+
let parsed;
|
|
3236
3246
|
try {
|
|
3237
|
-
|
|
3238
|
-
settingsCache = JSON.parse(content);
|
|
3239
|
-
return settingsCache;
|
|
3247
|
+
parsed = JSON.parse(fs2.readFileSync(PATHS.settingsFile, "utf8"));
|
|
3240
3248
|
} catch {
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
console.warn("\u8B66\u544A: settings.json \u683C\u5F0F\u635F\u574F\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u8BBE\u7F6E");
|
|
3246
|
-
}
|
|
3247
|
-
settingsCache = {};
|
|
3248
|
-
return settingsCache;
|
|
3249
|
+
return recoverCorruptedSettings();
|
|
3250
|
+
}
|
|
3251
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3252
|
+
return recoverCorruptedSettings();
|
|
3249
3253
|
}
|
|
3254
|
+
settingsCache = parsed;
|
|
3255
|
+
return settingsCache;
|
|
3256
|
+
}
|
|
3257
|
+
settingsCache = {};
|
|
3258
|
+
return settingsCache;
|
|
3259
|
+
}
|
|
3260
|
+
function recoverCorruptedSettings() {
|
|
3261
|
+
try {
|
|
3262
|
+
fs2.copyFileSync(PATHS.settingsFile, `${PATHS.settingsFile}.bak`);
|
|
3263
|
+
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`);
|
|
3264
|
+
} catch {
|
|
3265
|
+
console.warn("\u8B66\u544A: settings.json \u683C\u5F0F\u635F\u574F\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u8BBE\u7F6E");
|
|
3250
3266
|
}
|
|
3251
3267
|
settingsCache = {};
|
|
3252
3268
|
return settingsCache;
|
|
@@ -3265,11 +3281,7 @@ function writeSettings(settings) {
|
|
|
3265
3281
|
function invalidateSettingsCache() {
|
|
3266
3282
|
settingsCache = null;
|
|
3267
3283
|
}
|
|
3268
|
-
function
|
|
3269
|
-
if (!url) return url;
|
|
3270
|
-
if (url.includes(",")) {
|
|
3271
|
-
return url.split(",").map((u) => maskUrl(u.trim())).join(", ");
|
|
3272
|
-
}
|
|
3284
|
+
function maskSingleUrl(url) {
|
|
3273
3285
|
try {
|
|
3274
3286
|
const parsed = new URL(url);
|
|
3275
3287
|
const tokenKeys = ["token", "key", "secret", "pass", "password", "auth", "access_token", "api_key"];
|
|
@@ -3289,6 +3301,26 @@ function maskUrl(url) {
|
|
|
3289
3301
|
return url;
|
|
3290
3302
|
}
|
|
3291
3303
|
}
|
|
3304
|
+
function looksLikeMultiUrl(url) {
|
|
3305
|
+
if (!url.includes(",")) return false;
|
|
3306
|
+
const parts = url.split(",").map((u) => u.trim()).filter(Boolean);
|
|
3307
|
+
if (parts.length < 2) return false;
|
|
3308
|
+
return parts.every((p) => {
|
|
3309
|
+
try {
|
|
3310
|
+
const u = new URL(p);
|
|
3311
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
3312
|
+
} catch {
|
|
3313
|
+
return false;
|
|
3314
|
+
}
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
function maskUrl(url) {
|
|
3318
|
+
if (!url) return url;
|
|
3319
|
+
if (looksLikeMultiUrl(url)) {
|
|
3320
|
+
return url.split(",").map((u) => maskSingleUrl(u.trim())).join(", ");
|
|
3321
|
+
}
|
|
3322
|
+
return maskSingleUrl(url);
|
|
3323
|
+
}
|
|
3292
3324
|
function readSubscriptionCache() {
|
|
3293
3325
|
ensureDirs();
|
|
3294
3326
|
if (fs2.existsSync(PATHS.subscriptionsCacheFile)) {
|
|
@@ -3318,7 +3350,14 @@ function saveSubscriptionCache(subName, data) {
|
|
|
3318
3350
|
}
|
|
3319
3351
|
function getSubscriptions() {
|
|
3320
3352
|
const settings = readSettings();
|
|
3321
|
-
|
|
3353
|
+
const subs = settings.subscriptions;
|
|
3354
|
+
if (!Array.isArray(subs)) {
|
|
3355
|
+
if (subs !== void 0) {
|
|
3356
|
+
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");
|
|
3357
|
+
}
|
|
3358
|
+
return [];
|
|
3359
|
+
}
|
|
3360
|
+
return subs.filter((s) => s != null && typeof s === "object" && typeof s.name === "string" && typeof s.url === "string");
|
|
3322
3361
|
}
|
|
3323
3362
|
function getSubscriptionsWithCache() {
|
|
3324
3363
|
const subs = getSubscriptions();
|
|
@@ -3337,7 +3376,7 @@ function validateSubscriptionName(name) {
|
|
|
3337
3376
|
function addSubscription(url, name = "default") {
|
|
3338
3377
|
validateSubscriptionName(name);
|
|
3339
3378
|
const settings = readSettings();
|
|
3340
|
-
const subs = [...
|
|
3379
|
+
const subs = [...getSubscriptions()];
|
|
3341
3380
|
if (subs.some((s) => s.name === name)) {
|
|
3342
3381
|
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`);
|
|
3343
3382
|
}
|
|
@@ -3350,7 +3389,7 @@ function addSubscription(url, name = "default") {
|
|
|
3350
3389
|
}
|
|
3351
3390
|
function removeSubscription(name) {
|
|
3352
3391
|
const settings = readSettings();
|
|
3353
|
-
const subs = [...
|
|
3392
|
+
const subs = [...getSubscriptions()];
|
|
3354
3393
|
const idx = subs.findIndex((s) => s.name === name);
|
|
3355
3394
|
if (idx < 0) return null;
|
|
3356
3395
|
subs.splice(idx, 1);
|
|
@@ -3374,7 +3413,7 @@ function removeSubscription(name) {
|
|
|
3374
3413
|
}
|
|
3375
3414
|
function setDefaultSubscription(name) {
|
|
3376
3415
|
const settings = readSettings();
|
|
3377
|
-
const subs =
|
|
3416
|
+
const subs = getSubscriptions();
|
|
3378
3417
|
const idx = subs.findIndex((s) => s.name === name);
|
|
3379
3418
|
if (idx < 0) return false;
|
|
3380
3419
|
if (settings.active_subscription === name) return true;
|
|
@@ -3459,6 +3498,18 @@ function deepMergeWithOverrides(target, override) {
|
|
|
3459
3498
|
const { key, forceOverwrite, arrayPrepend, arrayAppend, arrayMergeByName } = parseOverrideKey(rawKey);
|
|
3460
3499
|
const existingValue = result[key];
|
|
3461
3500
|
if (arrayMergeByName) {
|
|
3501
|
+
if (existingValue !== void 0 && !Array.isArray(existingValue)) {
|
|
3502
|
+
throw new CliError(
|
|
3503
|
+
`\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`}`,
|
|
3504
|
+
{
|
|
3505
|
+
label: "\u8986\u5199\u914D\u7F6E\u9519\u8BEF",
|
|
3506
|
+
hint: [
|
|
3507
|
+
`~${key} \u7528\u4E8E\u6309 name \u5C31\u5730\u5408\u5E76\u6570\u7EC4\u5143\u7D20\uFF08\u5982 ~proxy-groups\uFF09\u3002`,
|
|
3508
|
+
`\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`
|
|
3509
|
+
]
|
|
3510
|
+
}
|
|
3511
|
+
);
|
|
3512
|
+
}
|
|
3462
3513
|
const existingArr = Array.isArray(existingValue) ? existingValue : [];
|
|
3463
3514
|
const overrideArr = Array.isArray(value) ? value : [value];
|
|
3464
3515
|
const merged = [...existingArr];
|
|
@@ -3475,6 +3526,15 @@ function deepMergeWithOverrides(target, override) {
|
|
|
3475
3526
|
continue;
|
|
3476
3527
|
}
|
|
3477
3528
|
if (arrayPrepend || arrayAppend) {
|
|
3529
|
+
if (existingValue !== void 0 && !Array.isArray(existingValue)) {
|
|
3530
|
+
throw new CliError(
|
|
3531
|
+
`\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`}`,
|
|
3532
|
+
{
|
|
3533
|
+
label: "\u8986\u5199\u914D\u7F6E\u9519\u8BEF",
|
|
3534
|
+
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`]
|
|
3535
|
+
}
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3478
3538
|
const existingArr = Array.isArray(existingValue) ? existingValue : [];
|
|
3479
3539
|
const overrideArr = Array.isArray(value) ? value : [value];
|
|
3480
3540
|
if (arrayPrepend) {
|
|
@@ -3537,7 +3597,18 @@ function summarizeMatch(match) {
|
|
|
3537
3597
|
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
3538
3598
|
}
|
|
3539
3599
|
function splitUrlsLocal(url) {
|
|
3540
|
-
|
|
3600
|
+
const isValidHttp = (u) => {
|
|
3601
|
+
try {
|
|
3602
|
+
const parsed = new URL(u.trim());
|
|
3603
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
3604
|
+
} catch {
|
|
3605
|
+
return false;
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
if (!url.includes(",")) return [url.trim()];
|
|
3609
|
+
const parts = url.split(",").map((u) => u.trim()).filter(Boolean);
|
|
3610
|
+
if (parts.length > 1 && parts.every(isValidHttp)) return parts;
|
|
3611
|
+
return [url.trim()];
|
|
3541
3612
|
}
|
|
3542
3613
|
function hostMatchesDomain(host, domain) {
|
|
3543
3614
|
const h = host.toLowerCase();
|
|
@@ -3548,7 +3619,9 @@ function matchesScope(match, scope) {
|
|
|
3548
3619
|
if (!match) return true;
|
|
3549
3620
|
if (match.subscription) {
|
|
3550
3621
|
const names = Array.isArray(match.subscription) ? match.subscription : [match.subscription];
|
|
3551
|
-
if (!scope?.subName
|
|
3622
|
+
if (!scope?.subName) return false;
|
|
3623
|
+
const subName = scope.subName.toLowerCase();
|
|
3624
|
+
if (!names.some((n) => n.toLowerCase() === subName)) return false;
|
|
3552
3625
|
}
|
|
3553
3626
|
if (match["url-domain"]) {
|
|
3554
3627
|
const domains = Array.isArray(match["url-domain"]) ? match["url-domain"] : [match["url-domain"]];
|
|
@@ -3672,23 +3745,63 @@ function hasFlag(args, short, long) {
|
|
|
3672
3745
|
}
|
|
3673
3746
|
function parseIntArg(args, short, long, defaultValue) {
|
|
3674
3747
|
if (!args) return defaultValue;
|
|
3748
|
+
const parse = (raw, flag) => {
|
|
3749
|
+
if (!/^\d+$/.test(raw.trim())) {
|
|
3750
|
+
throw new CliError(`\u9009\u9879 ${flag} \u9700\u8981\u6B63\u6574\u6570\uFF0C\u6536\u5230 "${raw}"`, { hint: [`\u4F8B\u5982: ${flag} ${defaultValue}`] });
|
|
3751
|
+
}
|
|
3752
|
+
const val = Number(raw);
|
|
3753
|
+
if (!Number.isSafeInteger(val) || val < 1) {
|
|
3754
|
+
throw new CliError(`\u9009\u9879 ${flag} \u9700\u8981 >= 1 \u7684\u6574\u6570\uFF0C\u6536\u5230 "${raw}"`, { hint: [`\u4F8B\u5982: ${flag} ${defaultValue}`] });
|
|
3755
|
+
}
|
|
3756
|
+
return val;
|
|
3757
|
+
};
|
|
3675
3758
|
for (let i = 0; i < args.length; i++) {
|
|
3676
3759
|
if (args[i] === short || args[i] === long) {
|
|
3677
3760
|
if (i + 1 < args.length) {
|
|
3678
|
-
|
|
3679
|
-
return Number.isNaN(val) ? defaultValue : val;
|
|
3761
|
+
return parse(args[i + 1], args[i]);
|
|
3680
3762
|
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3763
|
+
throw new CliError(`\u9009\u9879 ${args[i]} \u7F3A\u5C11\u503C`, { hint: [`\u4F8B\u5982: ${args[i]} ${defaultValue}`] });
|
|
3764
|
+
}
|
|
3765
|
+
if (args[i].startsWith(`${long}=`)) {
|
|
3766
|
+
return parse(args[i].slice(long.length + 1), long);
|
|
3684
3767
|
}
|
|
3685
3768
|
}
|
|
3686
3769
|
return defaultValue;
|
|
3687
3770
|
}
|
|
3688
|
-
|
|
3771
|
+
function parseStringArg(args, long, short) {
|
|
3772
|
+
if (!args) return null;
|
|
3773
|
+
for (let i = 0; i < args.length; i++) {
|
|
3774
|
+
if (args[i] === long || short !== void 0 && args[i] === short) {
|
|
3775
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
3776
|
+
return args[i + 1];
|
|
3777
|
+
}
|
|
3778
|
+
throw new CliError(`\u9009\u9879 ${args[i]} \u7F3A\u5C11\u503C`, { hint: [`\u4F8B\u5982: ${long} <\u503C>`] });
|
|
3779
|
+
}
|
|
3780
|
+
if (args[i].startsWith(`${long}=`)) {
|
|
3781
|
+
const value = args[i].slice(long.length + 1);
|
|
3782
|
+
if (!value) throw new CliError(`\u9009\u9879 ${long} \u7F3A\u5C11\u503C`, { hint: [`\u4F8B\u5982: ${long}=<\u503C>`] });
|
|
3783
|
+
return value;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
3789
|
+
"-t",
|
|
3790
|
+
"--timeout",
|
|
3791
|
+
"-j",
|
|
3792
|
+
"--concurrency",
|
|
3793
|
+
"-r",
|
|
3794
|
+
"--rounds",
|
|
3795
|
+
"-n",
|
|
3796
|
+
"--lines",
|
|
3797
|
+
"-u",
|
|
3798
|
+
"--update-timeout",
|
|
3799
|
+
"--host",
|
|
3800
|
+
"--port"
|
|
3801
|
+
]);
|
|
3689
3802
|
function extractStartOptions(args) {
|
|
3690
3803
|
if (!args) return [];
|
|
3691
|
-
const BOOL_FLAGS = /* @__PURE__ */ new Set(["-s", "--no-update", "--no-clean"]);
|
|
3804
|
+
const BOOL_FLAGS = /* @__PURE__ */ new Set(["-s", "--no-update", "--no-clean", "--no-tunnel"]);
|
|
3692
3805
|
const out = [];
|
|
3693
3806
|
for (let i = 0; i < args.length; i++) {
|
|
3694
3807
|
const a = args[i];
|
|
@@ -3829,7 +3942,7 @@ function excludeOverwriteProxiesFromIncludeAll(config, overwriteFiles) {
|
|
|
3829
3942
|
if (injectedNames.length === 0) return;
|
|
3830
3943
|
const groups = config["proxy-groups"];
|
|
3831
3944
|
if (!groups) return;
|
|
3832
|
-
const excludePattern = injectedNames.map((n) => escapeRegExp(n)).join("|")
|
|
3945
|
+
const excludePattern = `^(?:${injectedNames.map((n) => escapeRegExp(n)).join("|")})$`;
|
|
3833
3946
|
for (const group of groups) {
|
|
3834
3947
|
if (!group["include-all"] && !group["include-all-proxies"]) continue;
|
|
3835
3948
|
const existing = group["exclude-filter"];
|
|
@@ -3864,7 +3977,58 @@ function getRuleTarget(rule) {
|
|
|
3864
3977
|
}
|
|
3865
3978
|
return last;
|
|
3866
3979
|
}
|
|
3980
|
+
function assertConfigShape(config) {
|
|
3981
|
+
const listSections = [
|
|
3982
|
+
{ key: "proxies", label: "\u8282\u70B9", needsName: true },
|
|
3983
|
+
{ key: "proxy-groups", label: "\u4EE3\u7406\u7EC4", needsName: true },
|
|
3984
|
+
{ key: "rules", label: "\u89C4\u5219", needsName: false }
|
|
3985
|
+
];
|
|
3986
|
+
for (const { key, label, needsName } of listSections) {
|
|
3987
|
+
const value = config[key];
|
|
3988
|
+
if (value === void 0 || value === null) continue;
|
|
3989
|
+
if (!Array.isArray(value)) {
|
|
3990
|
+
throw new CliError(`${key} \u5FC5\u987B\u662F\u5217\u8868\uFF0C\u5F53\u524D\u4E3A ${typeof value === "object" ? "\u6620\u5C04" : typeof value}`, {
|
|
3991
|
+
label: "\u914D\u7F6E\u9519\u8BEF",
|
|
3992
|
+
hint: [
|
|
3993
|
+
`${label}\u6BB5\uFF08${key}\uFF09\u9700\u5199\u6210 YAML \u5217\u8868\uFF0C\u6BCF\u9879\u4EE5 "- " \u5F00\u5934\u3002`,
|
|
3994
|
+
`\u4F8B\u5982: ${key}:`,
|
|
3995
|
+
key === "rules" ? " - MATCH,DIRECT" : " - {name: xxx, ...}"
|
|
3996
|
+
]
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
for (let i = 0; i < value.length; i++) {
|
|
4000
|
+
const item = value[i];
|
|
4001
|
+
if (item === null || item === void 0) {
|
|
4002
|
+
throw new CliError(`${key}[${i}] \u4E3A\u7A7A`, {
|
|
4003
|
+
label: "\u914D\u7F6E\u9519\u8BEF",
|
|
4004
|
+
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`]
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
if (needsName) {
|
|
4008
|
+
if (typeof item !== "object" || Array.isArray(item)) {
|
|
4009
|
+
throw new CliError(`${key}[${i}] \u5FC5\u987B\u662F\u6620\u5C04`, {
|
|
4010
|
+
label: "\u914D\u7F6E\u9519\u8BEF",
|
|
4011
|
+
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`]
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
const name = item.name;
|
|
4015
|
+
if (typeof name !== "string" || name === "") {
|
|
4016
|
+
throw new CliError(`${key}[${i}] \u7F3A\u5C11\u6709\u6548\u7684 name`, {
|
|
4017
|
+
label: "\u914D\u7F6E\u9519\u8BEF",
|
|
4018
|
+
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`]
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
} else if (typeof item !== "string") {
|
|
4022
|
+
throw new CliError(`${key}[${i}] \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`, {
|
|
4023
|
+
label: "\u914D\u7F6E\u9519\u8BEF",
|
|
4024
|
+
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`]
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
3867
4030
|
function validateConfig(config) {
|
|
4031
|
+
assertConfigShape(config);
|
|
3868
4032
|
const warnings = [];
|
|
3869
4033
|
const proxies = config.proxies || [];
|
|
3870
4034
|
const groups = config["proxy-groups"] || [];
|
|
@@ -4102,12 +4266,13 @@ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}`, "", "\u547D\u4EE4\u522B\
|
|
|
4102
4266
|
" mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605",
|
|
4103
4267
|
" mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)",
|
|
4104
4268
|
" mihomo daemon on # \u5F00\u542F\u4FDD\u6D3B\uFF08\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF09",
|
|
4269
|
+
" mihomo tunnel add work --host m4 --port 1080 # \u52A0 ssh \u96A7\u9053\u51FA\u53E3",
|
|
4105
4270
|
" mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)",
|
|
4106
4271
|
" mihomo ui # \u6253\u5F00 Web UI",
|
|
4107
4272
|
"",
|
|
4108
4273
|
`${colors.cyan("\u5FEB\u6377\u547D\u4EE4:")}`,
|
|
4109
4274
|
" tun = start tun use = sub use on/off = ow on/off open = dir open",
|
|
4110
|
-
" up = start down = stop upd/upgrade = update",
|
|
4275
|
+
" up = start down = stop upd/upgrade = update ssh = tunnel",
|
|
4111
4276
|
"",
|
|
4112
4277
|
`${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}`,
|
|
4113
4278
|
" mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)",
|
|
@@ -4187,7 +4352,7 @@ function isProcessRunning(pid) {
|
|
|
4187
4352
|
function isProcessCommandMatching(pid, needle) {
|
|
4188
4353
|
if (!pid) return false;
|
|
4189
4354
|
try {
|
|
4190
|
-
const result = spawnSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
|
|
4355
|
+
const result = spawnSync2("ps", ["-ww", "-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
|
|
4191
4356
|
return (result.stdout || "").includes(needle);
|
|
4192
4357
|
} catch {
|
|
4193
4358
|
return false;
|
|
@@ -4697,6 +4862,17 @@ function viewLogWithTail(logPath, options) {
|
|
|
4697
4862
|
|
|
4698
4863
|
// src/daemon.ts
|
|
4699
4864
|
var SERVICE_TARGET = `system/${LAUNCH_DAEMON_LABEL}`;
|
|
4865
|
+
function assertDaemonLabelSafe() {
|
|
4866
|
+
if (RAW_DAEMON_LABEL_INPUT !== void 0 && !isValidDaemonLabel(RAW_DAEMON_LABEL_INPUT)) {
|
|
4867
|
+
throw new CliError(`MIHOMO_CLI_DAEMON_LABEL \u65E0\u6548: "${RAW_DAEMON_LABEL_INPUT}"`, {
|
|
4868
|
+
label: "\u914D\u7F6E\u9519\u8BEF",
|
|
4869
|
+
hint: [
|
|
4870
|
+
'\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\uFF0C\u4E14\u4E0D\u80FD\u542B ".."\u3002',
|
|
4871
|
+
"\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"
|
|
4872
|
+
]
|
|
4873
|
+
});
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4700
4876
|
var HOT_RELOAD_TIMEOUT_MS = 5e3;
|
|
4701
4877
|
var DAEMON_BOOT_WAIT_MS = 500;
|
|
4702
4878
|
var LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
|
|
@@ -4778,6 +4954,7 @@ function isDaemonRunning(status) {
|
|
|
4778
4954
|
return status.loaded && status.pid !== null;
|
|
4779
4955
|
}
|
|
4780
4956
|
function enableDaemon() {
|
|
4957
|
+
assertDaemonLabelSafe();
|
|
4781
4958
|
if (!fs6.existsSync(PATHS.mihomoBinary)) {
|
|
4782
4959
|
throw new CliError("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
|
|
4783
4960
|
}
|
|
@@ -4818,6 +4995,7 @@ function enableDaemon() {
|
|
|
4818
4995
|
}
|
|
4819
4996
|
}
|
|
4820
4997
|
function disableDaemon() {
|
|
4998
|
+
assertDaemonLabelSafe();
|
|
4821
4999
|
if (!isDaemonEnabled()) return;
|
|
4822
5000
|
const target = shellQuote(SERVICE_TARGET);
|
|
4823
5001
|
const plistDest = shellQuote(PATHS.launchDaemonPlist);
|
|
@@ -4862,7 +5040,7 @@ async function tryHotReload() {
|
|
|
4862
5040
|
}
|
|
4863
5041
|
async function restartDaemon() {
|
|
4864
5042
|
if (!fs6.existsSync(PATHS.launchDaemonPlist)) {
|
|
4865
|
-
throw new
|
|
5043
|
+
throw new CliError("\u4FDD\u6D3B\u672A\u542F\u7528\uFF0C\u65E0\u6CD5\u91CD\u542F", { hint: "\u542F\u7528\u4FDD\u6D3B: mihomo daemon on" });
|
|
4866
5044
|
}
|
|
4867
5045
|
if (!logOversized() && await tryHotReload()) return;
|
|
4868
5046
|
const target = shellQuote(SERVICE_TARGET);
|
|
@@ -4889,6 +5067,7 @@ async function restartDaemon() {
|
|
|
4889
5067
|
|
|
4890
5068
|
// src/http.ts
|
|
4891
5069
|
var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
|
|
5070
|
+
var MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
4892
5071
|
function createHttpClient(options = {}) {
|
|
4893
5072
|
const { timeout = 6e4, secret } = options;
|
|
4894
5073
|
const authHeaders = secret ? { Authorization: `Bearer ${secret}` } : {};
|
|
@@ -4906,7 +5085,8 @@ function createHttpClient(options = {}) {
|
|
|
4906
5085
|
const error = new Error(`HTTP ${response.status}`);
|
|
4907
5086
|
error.response = { status: response.status };
|
|
4908
5087
|
try {
|
|
4909
|
-
|
|
5088
|
+
const text2 = await readBodyWithLimit(response, controller, MAX_ERROR_BODY_BYTES);
|
|
5089
|
+
error.response.data = JSON.parse(text2);
|
|
4910
5090
|
} catch {
|
|
4911
5091
|
}
|
|
4912
5092
|
throw error;
|
|
@@ -4915,7 +5095,7 @@ function createHttpClient(options = {}) {
|
|
|
4915
5095
|
if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
|
|
4916
5096
|
throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
|
|
4917
5097
|
}
|
|
4918
|
-
const text = await readBodyWithLimit(response, controller);
|
|
5098
|
+
const text = await readBodyWithLimit(response, controller, MAX_RESPONSE_BYTES);
|
|
4919
5099
|
const data = config?.responseType === "json" ? JSON.parse(text) : text;
|
|
4920
5100
|
return { data, headers: response.headers, status: response.status };
|
|
4921
5101
|
} finally {
|
|
@@ -4924,7 +5104,7 @@ function createHttpClient(options = {}) {
|
|
|
4924
5104
|
}
|
|
4925
5105
|
};
|
|
4926
5106
|
}
|
|
4927
|
-
async function readBodyWithLimit(response, controller) {
|
|
5107
|
+
async function readBodyWithLimit(response, controller, limit) {
|
|
4928
5108
|
if (!response.body) return response.text();
|
|
4929
5109
|
const reader = response.body.getReader();
|
|
4930
5110
|
const chunks = [];
|
|
@@ -4935,9 +5115,9 @@ async function readBodyWithLimit(response, controller) {
|
|
|
4935
5115
|
if (done) break;
|
|
4936
5116
|
if (value) {
|
|
4937
5117
|
total += value.byteLength;
|
|
4938
|
-
if (total >
|
|
5118
|
+
if (total > limit) {
|
|
4939
5119
|
controller.abort();
|
|
4940
|
-
throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(
|
|
5120
|
+
throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(limit)}\uFF09`);
|
|
4941
5121
|
}
|
|
4942
5122
|
chunks.push(value);
|
|
4943
5123
|
}
|
|
@@ -4964,7 +5144,9 @@ function resolveUpdateInterval(url, cachedInterval) {
|
|
|
4964
5144
|
}
|
|
4965
5145
|
var HTTP_CLIENT = createHttpClient({ timeout: 6e4 });
|
|
4966
5146
|
function isMultiUrl(url) {
|
|
4967
|
-
|
|
5147
|
+
if (!url.includes(",")) return false;
|
|
5148
|
+
const parts = url.split(",").map((u) => u.trim()).filter(Boolean);
|
|
5149
|
+
return parts.length > 1 && parts.every(isValidHttpUrl);
|
|
4968
5150
|
}
|
|
4969
5151
|
function isValidHttpUrl(url) {
|
|
4970
5152
|
try {
|
|
@@ -4975,6 +5157,7 @@ function isValidHttpUrl(url) {
|
|
|
4975
5157
|
}
|
|
4976
5158
|
}
|
|
4977
5159
|
function splitUrls(url) {
|
|
5160
|
+
if (!isMultiUrl(url)) return [url.trim()];
|
|
4978
5161
|
return url.split(",").map((u) => u.trim()).filter(Boolean);
|
|
4979
5162
|
}
|
|
4980
5163
|
function loadSubscriptionConfig(subName) {
|
|
@@ -5097,6 +5280,23 @@ function pickSingleSubscription(subs, pattern) {
|
|
|
5097
5280
|
function resolveSubscription(subs, pattern) {
|
|
5098
5281
|
return pickSingleSubscription(findSubscriptionFuzzy(subs, pattern), pattern);
|
|
5099
5282
|
}
|
|
5283
|
+
function assertLooksLikeSubscription(parsed, maskedUrl) {
|
|
5284
|
+
const hasProxies = Array.isArray(parsed.proxies) && parsed.proxies.length > 0;
|
|
5285
|
+
const hasGroups = Array.isArray(parsed["proxy-groups"]) && parsed["proxy-groups"].length > 0;
|
|
5286
|
+
const providers = parsed["proxy-providers"];
|
|
5287
|
+
const hasProviders = providers != null && typeof providers === "object" && Object.keys(providers).length > 0;
|
|
5288
|
+
if (hasProxies || hasGroups || hasProviders) return;
|
|
5289
|
+
const serverMsg = ["error", "message", "msg", "info"].map((k) => parsed[k]).find((v) => typeof v === "string" && v.length > 0);
|
|
5290
|
+
throw new CliError("\u8BA2\u9605\u5185\u5BB9\u4E0D\u542B\u4EFB\u4F55\u8282\u70B9\u6765\u6E90\uFF08proxies / proxy-groups / proxy-providers \u5747\u4E3A\u7A7A\uFF09", {
|
|
5291
|
+
label: "\u8BA2\u9605\u65E0\u6548",
|
|
5292
|
+
hint: [
|
|
5293
|
+
...serverMsg ? [`\u670D\u52A1\u7AEF\u8FD4\u56DE: ${serverMsg}`] : [],
|
|
5294
|
+
`URL: ${maskedUrl}`,
|
|
5295
|
+
"\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",
|
|
5296
|
+
"\u78C1\u76D8\u4E0A\u539F\u6709\u7684\u8BA2\u9605\u914D\u7F6E\u672A\u88AB\u8986\u76D6\u3002"
|
|
5297
|
+
]
|
|
5298
|
+
});
|
|
5299
|
+
}
|
|
5100
5300
|
async function downloadSubscription(url, subName = "default", signal, persist = true) {
|
|
5101
5301
|
let response;
|
|
5102
5302
|
try {
|
|
@@ -5118,6 +5318,7 @@ async function downloadSubscription(url, subName = "default", signal, persist =
|
|
|
5118
5318
|
}
|
|
5119
5319
|
const parsed = parseYamlOrJson(content, "\u8BA2\u9605\u5185\u5BB9");
|
|
5120
5320
|
if (!parsed) throw new Error("\u8BA2\u9605\u5185\u5BB9\u4E3A\u7A7A");
|
|
5321
|
+
assertLooksLikeSubscription(parsed, maskUrl(url));
|
|
5121
5322
|
if (persist) {
|
|
5122
5323
|
saveSubscriptionRawConfig(subName, content);
|
|
5123
5324
|
}
|
|
@@ -5150,12 +5351,13 @@ async function downloadMergedSubscription(urls, subName, signal, persist = true)
|
|
|
5150
5351
|
}
|
|
5151
5352
|
})
|
|
5152
5353
|
);
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5354
|
+
const failures = responses.filter((r) => r.error);
|
|
5355
|
+
if (failures.length > 0) {
|
|
5356
|
+
const isAbort = (e) => e.name === "AbortError" || /abort/i.test(e.message);
|
|
5357
|
+
const real = failures.find((r) => !isAbort(r.error)) ?? failures[0];
|
|
5358
|
+
const maskedUrl = maskUrl(real.url);
|
|
5359
|
+
throw new Error(`\u5408\u5E76\u8BA2\u9605\u7B2C ${real.index + 1} \u4E2A URL \u83B7\u53D6\u5931\u8D25: ${real.error.message}
|
|
5157
5360
|
URL: ${maskedUrl}`);
|
|
5158
|
-
}
|
|
5159
5361
|
}
|
|
5160
5362
|
const parsed = responses.map((r, i) => {
|
|
5161
5363
|
const content = r.response?.data;
|
|
@@ -5176,6 +5378,7 @@ async function downloadMergedSubscription(urls, subName, signal, persist = true)
|
|
|
5176
5378
|
}
|
|
5177
5379
|
base.proxies = baseProxies;
|
|
5178
5380
|
const mergedContent = dumpYaml(base);
|
|
5381
|
+
assertLooksLikeSubscription(base, urls.map((u) => maskUrl(u)).join(", "));
|
|
5179
5382
|
if (persist) {
|
|
5180
5383
|
saveSubscriptionRawConfig(subName, mergedContent);
|
|
5181
5384
|
}
|
|
@@ -5465,6 +5668,9 @@ async function autoCleanSubscription(subName, options = {}) {
|
|
|
5465
5668
|
return { summary, removedProxies, updatedGroups, removedGroups, skipped };
|
|
5466
5669
|
}
|
|
5467
5670
|
|
|
5671
|
+
// src/commands/shared.ts
|
|
5672
|
+
import readline from "readline";
|
|
5673
|
+
|
|
5468
5674
|
// src/runtime.ts
|
|
5469
5675
|
function getRuntimeMode() {
|
|
5470
5676
|
if (isDaemonEnabled()) return "mixed";
|
|
@@ -5563,8 +5769,373 @@ function formatTestSummary(summary) {
|
|
|
5563
5769
|
return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
|
|
5564
5770
|
}
|
|
5565
5771
|
|
|
5772
|
+
// src/tunnel.ts
|
|
5773
|
+
import { spawn as spawn2 } from "child_process";
|
|
5774
|
+
import fs7 from "fs";
|
|
5775
|
+
import net from "net";
|
|
5776
|
+
import path6 from "path";
|
|
5777
|
+
var PORT_PROBE_TIMEOUT_MS = 300;
|
|
5778
|
+
var STOP_WAIT_ATTEMPTS = 20;
|
|
5779
|
+
var STOP_WAIT_INTERVAL = 100;
|
|
5780
|
+
var START_WAIT_ATTEMPTS = 40;
|
|
5781
|
+
var START_WAIT_INTERVAL = 500;
|
|
5782
|
+
function getReservedPorts() {
|
|
5783
|
+
const reserved = /* @__PURE__ */ new Map();
|
|
5784
|
+
const mixedPort = Number(BASE_CONFIG["mixed-port"]);
|
|
5785
|
+
if (Number.isInteger(mixedPort)) reserved.set(mixedPort, "mihomo \u6DF7\u5408\u4EE3\u7406\u7AEF\u53E3");
|
|
5786
|
+
reserved.set(CONTROLLER_PORT, "mihomo \u63A7\u5236\u5668\u7AEF\u53E3");
|
|
5787
|
+
const testPort = Number(TEST_CONFIG["mixed-port"]);
|
|
5788
|
+
if (Number.isInteger(testPort)) reserved.set(testPort, "\u6D4B\u901F\u5B9E\u4F8B\u4EE3\u7406\u7AEF\u53E3");
|
|
5789
|
+
const testController = Number(TEST_CONTROLLER_ADDR.split(":")[1]);
|
|
5790
|
+
if (Number.isInteger(testController)) reserved.set(testController, "\u6D4B\u901F\u5B9E\u4F8B\u63A7\u5236\u5668\u7AEF\u53E3");
|
|
5791
|
+
return reserved;
|
|
5792
|
+
}
|
|
5793
|
+
function validateTunnelName(name) {
|
|
5794
|
+
if (!name || !SAFE_NAME_RE.test(name)) {
|
|
5795
|
+
throw new CliError(`\u96A7\u9053\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`);
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
var SAFE_HOST_RE = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
|
|
5799
|
+
function validateTunnelHost(host) {
|
|
5800
|
+
if (!host) {
|
|
5801
|
+
throw new CliError("\u7F3A\u5C11 --host", { hint: ["\u4F8B\u5982: mihomo tunnel add work --host m4 --port 1080"] });
|
|
5802
|
+
}
|
|
5803
|
+
if (host.startsWith("-")) {
|
|
5804
|
+
throw new CliError(`\u4E3B\u673A\u540D\u65E0\u6548: "${host}"`, {
|
|
5805
|
+
label: "\u53C2\u6570\u9519\u8BEF",
|
|
5806
|
+
hint: ['\u4E3B\u673A\u540D\u4E0D\u80FD\u4EE5 "-" \u5F00\u5934\u2014\u2014\u5B83\u4F1A\u88AB ssh \u5F53\u4F5C\u9009\u9879\u89E3\u6790\uFF08\u5982 -oProxyCommand=... \u53EF\u6267\u884C\u4EFB\u610F\u547D\u4EE4\uFF09\u3002']
|
|
5807
|
+
});
|
|
5808
|
+
}
|
|
5809
|
+
if (!SAFE_HOST_RE.test(host)) {
|
|
5810
|
+
throw new CliError(`\u4E3B\u673A\u540D\u65E0\u6548: "${host}"\uFF0C\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u548C @`, {
|
|
5811
|
+
label: "\u53C2\u6570\u9519\u8BEF",
|
|
5812
|
+
hint: ["\u53EF\u7528 ssh \u522B\u540D\uFF08~/.ssh/config \u91CC\u7684 Host\uFF09\u6216 user@hostname\u3002"]
|
|
5813
|
+
});
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
function validateTunnelPort(port, exclude) {
|
|
5817
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5818
|
+
throw new CliError(`\u7AEF\u53E3\u65E0\u6548: ${port}\uFF0C\u9700\u4E3A 1-65535 \u7684\u6574\u6570`);
|
|
5819
|
+
}
|
|
5820
|
+
const reservedLabel = getReservedPorts().get(port);
|
|
5821
|
+
if (reservedLabel) {
|
|
5822
|
+
throw new CliError(`\u7AEF\u53E3 ${port} \u5DF2\u88AB ${reservedLabel} \u5360\u7528`, {
|
|
5823
|
+
label: "\u7AEF\u53E3\u51B2\u7A81",
|
|
5824
|
+
hint: ["\u8BF7\u6362\u4E00\u4E2A\u7AEF\u53E3\uFF0C\u4F8B\u5982 1080\u3002"]
|
|
5825
|
+
});
|
|
5826
|
+
}
|
|
5827
|
+
const conflict = getTunnels().find((t) => t.port === port && t.name !== exclude);
|
|
5828
|
+
if (conflict) {
|
|
5829
|
+
throw new CliError(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u96A7\u9053 "${conflict.name}" \u4F7F\u7528`, { label: "\u7AEF\u53E3\u51B2\u7A81" });
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
function getTunnels() {
|
|
5833
|
+
const settings = readSettings();
|
|
5834
|
+
const tunnels = settings.tunnels;
|
|
5835
|
+
if (!Array.isArray(tunnels)) {
|
|
5836
|
+
if (tunnels !== void 0) {
|
|
5837
|
+
console.warn("\u8B66\u544A: settings.json \u7684 tunnels \u4E0D\u662F\u5217\u8868\uFF0C\u5DF2\u5FFD\u7565\uFF08\u53EF\u7528 mihomo tunnel add \u91CD\u65B0\u6DFB\u52A0\uFF09");
|
|
5838
|
+
}
|
|
5839
|
+
return [];
|
|
5840
|
+
}
|
|
5841
|
+
return tunnels.filter((t) => t != null && typeof t === "object" && typeof t.name === "string" && typeof t.host === "string" && Number.isInteger(t.port));
|
|
5842
|
+
}
|
|
5843
|
+
function findTunnel(name) {
|
|
5844
|
+
return getTunnels().find((t) => t.name === name);
|
|
5845
|
+
}
|
|
5846
|
+
function addTunnel(config) {
|
|
5847
|
+
validateTunnelName(config.name);
|
|
5848
|
+
validateTunnelHost(config.host);
|
|
5849
|
+
validateTunnelPort(config.port);
|
|
5850
|
+
const tunnels = [...getTunnels()];
|
|
5851
|
+
if (tunnels.some((t) => t.name === config.name)) {
|
|
5852
|
+
throw new CliError(`\u96A7\u9053 "${config.name}" \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E2A\u540D\u79F0\uFF0C\u6216\u5148\u5220\u9664\uFF08mihomo tunnel rm ${config.name}\uFF09`);
|
|
5853
|
+
}
|
|
5854
|
+
tunnels.push(config);
|
|
5855
|
+
writeSettings({ tunnels });
|
|
5856
|
+
}
|
|
5857
|
+
function removeTunnel(name) {
|
|
5858
|
+
const tunnels = [...getTunnels()];
|
|
5859
|
+
const idx = tunnels.findIndex((t) => t.name === name);
|
|
5860
|
+
if (idx < 0) return false;
|
|
5861
|
+
tunnels.splice(idx, 1);
|
|
5862
|
+
writeSettings({ tunnels });
|
|
5863
|
+
clearTunnelRuntime(name);
|
|
5864
|
+
return true;
|
|
5865
|
+
}
|
|
5866
|
+
function getTunnelRuntimePath(name) {
|
|
5867
|
+
validateTunnelName(name);
|
|
5868
|
+
return path6.join(DIRS.tunnel, `${name}.json`);
|
|
5869
|
+
}
|
|
5870
|
+
function readTunnelRuntime(name) {
|
|
5871
|
+
try {
|
|
5872
|
+
const raw = fs7.readFileSync(getTunnelRuntimePath(name), "utf8");
|
|
5873
|
+
const parsed = JSON.parse(raw);
|
|
5874
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
5875
|
+
const r = parsed;
|
|
5876
|
+
if (!Number.isInteger(r.pid) || r.pid <= 0) return null;
|
|
5877
|
+
return {
|
|
5878
|
+
pid: r.pid,
|
|
5879
|
+
started_by: r.started_by === "manual" ? "manual" : "auto",
|
|
5880
|
+
started_at: typeof r.started_at === "string" ? r.started_at : "",
|
|
5881
|
+
port: Number.isInteger(r.port) ? r.port : 0
|
|
5882
|
+
};
|
|
5883
|
+
} catch {
|
|
5884
|
+
return null;
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
function writeTunnelRuntime(name, runtime) {
|
|
5888
|
+
ensureDirs();
|
|
5889
|
+
atomicWriteFileSync(getTunnelRuntimePath(name), JSON.stringify(runtime, null, 2), { mode: 384 });
|
|
5890
|
+
}
|
|
5891
|
+
function clearTunnelRuntime(name) {
|
|
5892
|
+
try {
|
|
5893
|
+
fs7.rmSync(getTunnelRuntimePath(name), { force: true });
|
|
5894
|
+
} catch {
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
function buildSshArgs(tunnel) {
|
|
5898
|
+
return [
|
|
5899
|
+
"-D",
|
|
5900
|
+
`127.0.0.1:${tunnel.port}`,
|
|
5901
|
+
"-N",
|
|
5902
|
+
"-o",
|
|
5903
|
+
"ExitOnForwardFailure=yes",
|
|
5904
|
+
"-o",
|
|
5905
|
+
"BatchMode=yes",
|
|
5906
|
+
"-o",
|
|
5907
|
+
"ServerAliveInterval=30",
|
|
5908
|
+
"-o",
|
|
5909
|
+
"ServerAliveCountMax=3",
|
|
5910
|
+
"-o",
|
|
5911
|
+
"ConnectTimeout=15",
|
|
5912
|
+
tunnel.host
|
|
5913
|
+
];
|
|
5914
|
+
}
|
|
5915
|
+
function commandNeedle(port) {
|
|
5916
|
+
return `-D 127.0.0.1:${port}`;
|
|
5917
|
+
}
|
|
5918
|
+
function getTunnelLogPath(name) {
|
|
5919
|
+
validateTunnelName(name);
|
|
5920
|
+
return path6.join(DIRS.logs, `tunnel-${name}.log`);
|
|
5921
|
+
}
|
|
5922
|
+
function isTunnelProcessAlive(pid, port) {
|
|
5923
|
+
return isProcessRunning(pid) && isProcessCommandMatching(pid, commandNeedle(port));
|
|
5924
|
+
}
|
|
5925
|
+
function isPortListening(port, timeoutMs = PORT_PROBE_TIMEOUT_MS) {
|
|
5926
|
+
return new Promise((resolve) => {
|
|
5927
|
+
const socket = new net.Socket();
|
|
5928
|
+
let settled = false;
|
|
5929
|
+
const finish = (result) => {
|
|
5930
|
+
if (settled) return;
|
|
5931
|
+
settled = true;
|
|
5932
|
+
socket.destroy();
|
|
5933
|
+
resolve(result);
|
|
5934
|
+
};
|
|
5935
|
+
socket.setTimeout(timeoutMs);
|
|
5936
|
+
socket.once("connect", () => finish(true));
|
|
5937
|
+
socket.once("timeout", () => finish(false));
|
|
5938
|
+
socket.once("error", () => finish(false));
|
|
5939
|
+
socket.connect(port, "127.0.0.1");
|
|
5940
|
+
});
|
|
5941
|
+
}
|
|
5942
|
+
async function getTunnelStatus(config) {
|
|
5943
|
+
const runtime = readTunnelRuntime(config.name);
|
|
5944
|
+
const alive = runtime !== null && isTunnelProcessAlive(runtime.pid, runtime.port || config.port);
|
|
5945
|
+
if (!alive) {
|
|
5946
|
+
return { config, state: "stopped", pid: null, started_by: null, started_at: null };
|
|
5947
|
+
}
|
|
5948
|
+
const listening = await isPortListening(config.port);
|
|
5949
|
+
return {
|
|
5950
|
+
config,
|
|
5951
|
+
state: listening ? "running" : "dead-port",
|
|
5952
|
+
pid: runtime.pid,
|
|
5953
|
+
started_by: runtime.started_by,
|
|
5954
|
+
started_at: runtime.started_at || null
|
|
5955
|
+
};
|
|
5956
|
+
}
|
|
5957
|
+
async function getAllTunnelStatus() {
|
|
5958
|
+
return Promise.all(getTunnels().map(getTunnelStatus));
|
|
5959
|
+
}
|
|
5960
|
+
async function startTunnel(name, options) {
|
|
5961
|
+
const config = findTunnel(name);
|
|
5962
|
+
if (!config) {
|
|
5963
|
+
throw new CliError(`\u672A\u627E\u5230\u96A7\u9053 "${name}"`, { hint: ["\u67E5\u770B\u5168\u90E8\u96A7\u9053: mihomo tunnel"] });
|
|
5964
|
+
}
|
|
5965
|
+
const existing = readTunnelRuntime(name);
|
|
5966
|
+
if (existing && isTunnelProcessAlive(existing.pid, existing.port || config.port)) {
|
|
5967
|
+
if (options.startedBy === "manual" && existing.started_by === "auto") {
|
|
5968
|
+
writeTunnelRuntime(name, { ...existing, started_by: "manual" });
|
|
5969
|
+
}
|
|
5970
|
+
return { alreadyRunning: true, pid: existing.pid };
|
|
5971
|
+
}
|
|
5972
|
+
if (existing) clearTunnelRuntime(name);
|
|
5973
|
+
if (await isPortListening(config.port)) {
|
|
5974
|
+
throw new CliError(`\u7AEF\u53E3 ${config.port} \u5DF2\u88AB\u5360\u7528`, {
|
|
5975
|
+
label: "\u65E0\u6CD5\u542F\u52A8\u96A7\u9053",
|
|
5976
|
+
hint: [`\u53EF\u80FD\u5DF2\u6709\u96A7\u9053\u6216\u5176\u4ED6\u7A0B\u5E8F\u5728\u76D1\u542C 127.0.0.1:${config.port}\u3002`, `\u67E5\u770B\u5360\u7528: lsof -nP -iTCP:${config.port} -sTCP:LISTEN`]
|
|
5977
|
+
});
|
|
5978
|
+
}
|
|
5979
|
+
ensureDirs();
|
|
5980
|
+
const logPath = getTunnelLogPath(name);
|
|
5981
|
+
const logFd = fs7.openSync(logPath, "w");
|
|
5982
|
+
const child = spawn2("ssh", buildSshArgs(config), {
|
|
5983
|
+
detached: true,
|
|
5984
|
+
stdio: ["ignore", logFd, logFd]
|
|
5985
|
+
});
|
|
5986
|
+
child.on("error", () => {
|
|
5987
|
+
});
|
|
5988
|
+
fs7.closeSync(logFd);
|
|
5989
|
+
child.unref();
|
|
5990
|
+
const pid = child.pid;
|
|
5991
|
+
if (!pid) {
|
|
5992
|
+
throw new CliError("\u65E0\u6CD5\u521B\u5EFA ssh \u8FDB\u7A0B", { label: "\u542F\u52A8\u96A7\u9053\u5931\u8D25", hint: ["\u8BF7\u786E\u8BA4\u5DF2\u5B89\u88C5 ssh \u5BA2\u6237\u7AEF\u3002"] });
|
|
5993
|
+
}
|
|
5994
|
+
writeTunnelRuntime(name, {
|
|
5995
|
+
pid,
|
|
5996
|
+
started_by: options.startedBy,
|
|
5997
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5998
|
+
port: config.port
|
|
5999
|
+
});
|
|
6000
|
+
for (let i = 0; i < START_WAIT_ATTEMPTS; i++) {
|
|
6001
|
+
if (!isProcessRunning(pid)) break;
|
|
6002
|
+
if (await isPortListening(config.port)) {
|
|
6003
|
+
return { alreadyRunning: false, pid };
|
|
6004
|
+
}
|
|
6005
|
+
await new Promise((resolve) => setTimeout(resolve, START_WAIT_INTERVAL));
|
|
6006
|
+
}
|
|
6007
|
+
clearTunnelRuntime(name);
|
|
6008
|
+
if (isProcessRunning(pid)) {
|
|
6009
|
+
try {
|
|
6010
|
+
process.kill(pid, "SIGKILL");
|
|
6011
|
+
} catch {
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
throw new CliError("ssh \u96A7\u9053\u672A\u80FD\u5EFA\u7ACB", {
|
|
6015
|
+
label: "\u542F\u52A8\u96A7\u9053\u5931\u8D25",
|
|
6016
|
+
hint: [...readLogTail(logPath), `\u5B8C\u6574\u65E5\u5FD7: ${logPath}`]
|
|
6017
|
+
});
|
|
6018
|
+
}
|
|
6019
|
+
function readLogTail(logPath, maxLines = 5) {
|
|
6020
|
+
try {
|
|
6021
|
+
const content = fs7.readFileSync(logPath, "utf8").trim();
|
|
6022
|
+
if (!content) return [];
|
|
6023
|
+
return content.split("\n").slice(-maxLines).map((line) => ` ${line.trim()}`);
|
|
6024
|
+
} catch {
|
|
6025
|
+
return [];
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
function stopTunnel(name) {
|
|
6029
|
+
const config = findTunnel(name);
|
|
6030
|
+
const runtime = readTunnelRuntime(name);
|
|
6031
|
+
if (!runtime) {
|
|
6032
|
+
clearTunnelRuntime(name);
|
|
6033
|
+
return { notRunning: true, pid: null };
|
|
6034
|
+
}
|
|
6035
|
+
const port = runtime.port || config?.port || 0;
|
|
6036
|
+
const { pid } = runtime;
|
|
6037
|
+
if (!isTunnelProcessAlive(pid, port)) {
|
|
6038
|
+
clearTunnelRuntime(name);
|
|
6039
|
+
return { notRunning: true, pid: null };
|
|
6040
|
+
}
|
|
6041
|
+
try {
|
|
6042
|
+
process.kill(pid, "SIGTERM");
|
|
6043
|
+
} catch {
|
|
6044
|
+
}
|
|
6045
|
+
for (let i = 0; i < STOP_WAIT_ATTEMPTS; i++) {
|
|
6046
|
+
if (!isProcessRunning(pid)) break;
|
|
6047
|
+
sleepSync(STOP_WAIT_INTERVAL);
|
|
6048
|
+
}
|
|
6049
|
+
if (isProcessRunning(pid) && isProcessCommandMatching(pid, commandNeedle(port))) {
|
|
6050
|
+
try {
|
|
6051
|
+
process.kill(pid, "SIGKILL");
|
|
6052
|
+
} catch {
|
|
6053
|
+
}
|
|
6054
|
+
for (let i = 0; i < STOP_WAIT_ATTEMPTS; i++) {
|
|
6055
|
+
if (!isProcessRunning(pid)) break;
|
|
6056
|
+
sleepSync(STOP_WAIT_INTERVAL);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
clearTunnelRuntime(name);
|
|
6060
|
+
return { notRunning: false, pid };
|
|
6061
|
+
}
|
|
6062
|
+
async function startAutoTunnels() {
|
|
6063
|
+
const outcomes = [];
|
|
6064
|
+
for (const tunnel of getTunnels()) {
|
|
6065
|
+
if (!tunnel.auto) continue;
|
|
6066
|
+
try {
|
|
6067
|
+
const result = await startTunnel(tunnel.name, { startedBy: "auto" });
|
|
6068
|
+
outcomes.push({ name: tunnel.name, ok: true, alreadyRunning: result.alreadyRunning });
|
|
6069
|
+
} catch (e) {
|
|
6070
|
+
outcomes.push({
|
|
6071
|
+
name: tunnel.name,
|
|
6072
|
+
ok: false,
|
|
6073
|
+
error: e instanceof CliError ? e : new CliError(e.message)
|
|
6074
|
+
});
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
return outcomes;
|
|
6078
|
+
}
|
|
6079
|
+
function stopAutoTunnels() {
|
|
6080
|
+
const stopped = [];
|
|
6081
|
+
for (const tunnel of getTunnels()) {
|
|
6082
|
+
const runtime = readTunnelRuntime(tunnel.name);
|
|
6083
|
+
if (runtime?.started_by !== "auto") continue;
|
|
6084
|
+
const result = stopTunnel(tunnel.name);
|
|
6085
|
+
if (!result.notRunning) stopped.push(tunnel.name);
|
|
6086
|
+
}
|
|
6087
|
+
return stopped;
|
|
6088
|
+
}
|
|
6089
|
+
function stopAllTunnels() {
|
|
6090
|
+
const stopped = [];
|
|
6091
|
+
for (const tunnel of getTunnels()) {
|
|
6092
|
+
const result = stopTunnel(tunnel.name);
|
|
6093
|
+
if (!result.notRunning) stopped.push(tunnel.name);
|
|
6094
|
+
}
|
|
6095
|
+
return stopped;
|
|
6096
|
+
}
|
|
6097
|
+
function getTunnelOverwritePath(name) {
|
|
6098
|
+
validateTunnelName(name);
|
|
6099
|
+
return path6.join(USER_DATA_DIR, `overwrite.tunnel-${name}.yaml`);
|
|
6100
|
+
}
|
|
6101
|
+
function renderTunnelOverwrite(tunnel) {
|
|
6102
|
+
const proxyName = `Tunnel-${tunnel.name}-Host`;
|
|
6103
|
+
const groupName = `Tunnel-${tunnel.name}`;
|
|
6104
|
+
return `# mihomo-cli \u96A7\u9053\u8986\u5199\uFF08tunnel: ${tunnel.name}\uFF09
|
|
6105
|
+
# \u672C\u6587\u4EF6\u7531 mihomo-cli \u9996\u6B21\u521B\u5EFA\uFF0C\u4E4B\u540E\u5B8C\u5168\u7531\u4F60\u7EF4\u62A4\u2014\u2014CLI \u4E0D\u4F1A\u518D\u6539\u5199\u6216\u5220\u9664\u5B83\u3002
|
|
6106
|
+
#
|
|
6107
|
+
# ~ \u662F\u300C\u6309 name \u5C31\u5730\u5408\u5E76\u300D\u8BED\u4E49\uFF1A\u4E0E\u5176\u4ED6\u8986\u5199\u6587\u4EF6\u4E2D\u7684\u540C\u540D\u8282\u70B9/\u5206\u7EC4\u5408\u5E76\uFF0C
|
|
6108
|
+
# \u4E0D\u4F9D\u8D56\u6587\u4EF6\u52A0\u8F7D\u987A\u5E8F\uFF08+proxies \u5219\u4F9D\u8D56\u5B57\u6BCD\u5E8F\uFF0C\u4F1A\u88AB\u540E\u6765\u7684\u6587\u4EF6\u538B\u8FC7\u53BB\uFF09\u3002
|
|
6109
|
+
|
|
6110
|
+
~proxies:
|
|
6111
|
+
- name: ${proxyName}
|
|
6112
|
+
type: socks5
|
|
6113
|
+
server: 127.0.0.1
|
|
6114
|
+
port: ${tunnel.port}
|
|
6115
|
+
|
|
6116
|
+
~proxy-groups:
|
|
6117
|
+
- name: ${groupName}
|
|
6118
|
+
type: select
|
|
6119
|
+
proxies:
|
|
6120
|
+
- ${proxyName}
|
|
6121
|
+
- DIRECT
|
|
6122
|
+
|
|
6123
|
+
# \u53D6\u6D88\u6CE8\u91CA\u5E76\u586B\u5165\u9700\u8981\u8D70\u96A7\u9053\u7684\u5185\u7F51\u57DF\u540D/\u7F51\u6BB5\uFF08CLI \u65E0\u4ECE\u77E5\u9053\u4F60\u7684\u5185\u7F51\u5730\u5740\uFF09\uFF1A
|
|
6124
|
+
# +rules:
|
|
6125
|
+
# - DOMAIN-SUFFIX,example.internal,${groupName}
|
|
6126
|
+
# - IP-CIDR,10.0.0.0/8,${groupName}
|
|
6127
|
+
`;
|
|
6128
|
+
}
|
|
6129
|
+
function ensureTunnelOverwriteFile(tunnel) {
|
|
6130
|
+
const filePath = getTunnelOverwritePath(tunnel.name);
|
|
6131
|
+
if (fs7.existsSync(filePath)) return false;
|
|
6132
|
+
ensureDirs();
|
|
6133
|
+
atomicWriteFileSync(filePath, renderTunnelOverwrite(tunnel), { mode: 384 });
|
|
6134
|
+
return true;
|
|
6135
|
+
}
|
|
6136
|
+
|
|
5566
6137
|
// src/commands/status.ts
|
|
5567
|
-
function printStatus() {
|
|
6138
|
+
async function printStatus() {
|
|
5568
6139
|
const status = getStatus();
|
|
5569
6140
|
const state = getRunningState();
|
|
5570
6141
|
const info = getConfigInfo();
|
|
@@ -5631,6 +6202,17 @@ function printStatus() {
|
|
|
5631
6202
|
if (isDaemonEnabled()) {
|
|
5632
6203
|
console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
|
|
5633
6204
|
}
|
|
6205
|
+
const tunnels = getTunnels();
|
|
6206
|
+
if (tunnels.length > 0) {
|
|
6207
|
+
const statuses = await getAllTunnelStatus();
|
|
6208
|
+
const parts = statuses.map((s) => {
|
|
6209
|
+
const label = `${s.config.name}:${s.config.port}`;
|
|
6210
|
+
if (s.state === "running") return colors.green(label);
|
|
6211
|
+
if (s.state === "dead-port") return colors.yellow(`${label} \u5047\u6D3B`);
|
|
6212
|
+
return colors.gray(label);
|
|
6213
|
+
});
|
|
6214
|
+
console.log(`${colors.gray("\u96A7\u9053: ")}${parts.join(", ")}`);
|
|
6215
|
+
}
|
|
5634
6216
|
console.log("");
|
|
5635
6217
|
}
|
|
5636
6218
|
|
|
@@ -5640,23 +6222,51 @@ function handleStopResult(result) {
|
|
|
5640
6222
|
throw new CliError(result.remaining.join(", "), { label: "\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62", hint: "\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo" });
|
|
5641
6223
|
}
|
|
5642
6224
|
}
|
|
5643
|
-
|
|
6225
|
+
function stopAutoTunnelsWithLog() {
|
|
6226
|
+
const stopped = stopAutoTunnels();
|
|
6227
|
+
if (stopped.length > 0) {
|
|
6228
|
+
console.log(`${colors.green("\u5DF2\u505C\u6B62\u96A7\u9053")}: ${stopped.join(", ")}`);
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
async function cmdStop(args) {
|
|
5644
6232
|
if (isDaemonEnabled()) {
|
|
5645
6233
|
console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u4EE3\u7406\u7531 launchd \u6258\u7BA1"));
|
|
5646
6234
|
console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
|
|
5647
6235
|
return;
|
|
5648
6236
|
}
|
|
6237
|
+
const skipTunnel = hasFlag(args, "--no-tunnel");
|
|
5649
6238
|
const pids = getMihomoPids();
|
|
5650
6239
|
if (pids.length === 0) {
|
|
5651
6240
|
console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
|
|
6241
|
+
if (!skipTunnel) stopAutoTunnelsWithLog();
|
|
5652
6242
|
return;
|
|
5653
6243
|
}
|
|
5654
6244
|
console.log(`\u505C\u6B62 ${pids.length} \u4E2A\u8FDB\u7A0B...`);
|
|
5655
6245
|
handleStopResult(stop());
|
|
5656
6246
|
console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
|
|
6247
|
+
if (!skipTunnel) stopAutoTunnelsWithLog();
|
|
5657
6248
|
}
|
|
5658
6249
|
|
|
5659
6250
|
// src/commands/start.ts
|
|
6251
|
+
async function startAutoTunnelsWithWarning() {
|
|
6252
|
+
const outcomes = await startAutoTunnels();
|
|
6253
|
+
if (outcomes.length === 0) return;
|
|
6254
|
+
const started = outcomes.filter((o) => o.ok && !o.alreadyRunning);
|
|
6255
|
+
if (started.length > 0) {
|
|
6256
|
+
console.log(`${colors.green("\u5DF2\u542F\u52A8\u96A7\u9053")}: ${started.map((o) => o.name).join(", ")}`);
|
|
6257
|
+
}
|
|
6258
|
+
for (const failed of outcomes.filter((o) => !o.ok)) {
|
|
6259
|
+
console.log("");
|
|
6260
|
+
console.log(colors.yellow(`\u8B66\u544A: \u96A7\u9053 "${failed.name}" \u542F\u52A8\u5931\u8D25`));
|
|
6261
|
+
console.log(colors.gray(` ${failed.error?.message ?? "\u672A\u77E5\u9519\u8BEF"}`));
|
|
6262
|
+
for (const line of failed.error?.hint ?? []) {
|
|
6263
|
+
if (line.trim()) console.log(colors.gray(line.startsWith(" ") ? line : ` ${line}`));
|
|
6264
|
+
}
|
|
6265
|
+
console.log(colors.gray(" \u5185\u7F51\u5206\u6D41\u89C4\u5219\u5C06\u4E0D\u53EF\u7528\uFF0C\u5176\u4F59\u6D41\u91CF\u6B63\u5E38"));
|
|
6266
|
+
console.log(colors.gray(` \u6392\u67E5: mihomo tunnel status ${failed.name}`));
|
|
6267
|
+
console.log("");
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
5660
6270
|
async function cmdStart(args) {
|
|
5661
6271
|
const modeToken = args[1] && !args[1].startsWith("-") ? args[1].toLowerCase() : void 0;
|
|
5662
6272
|
if (modeToken !== void 0 && modeToken !== "tun" && modeToken !== "mixed") {
|
|
@@ -5675,6 +6285,7 @@ async function cmdStart(args) {
|
|
|
5675
6285
|
const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
|
|
5676
6286
|
const skipUpdate = hasFlag(args, "-s", "--no-update");
|
|
5677
6287
|
const skipClean = hasFlag(args, "--no-clean");
|
|
6288
|
+
const skipTunnel = hasFlag(args, "--no-tunnel");
|
|
5678
6289
|
const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
|
|
5679
6290
|
const sub = requireActiveSubscription("\u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
|
|
5680
6291
|
if (!skipUpdate) {
|
|
@@ -5716,6 +6327,9 @@ async function cmdStart(args) {
|
|
|
5716
6327
|
const lines = e.message.split("\n");
|
|
5717
6328
|
throw new CliError(lines[0], { label: "\u542F\u52A8\u5931\u8D25", hint: lines.slice(1) });
|
|
5718
6329
|
}
|
|
6330
|
+
if (!skipTunnel) {
|
|
6331
|
+
await startAutoTunnelsWithWarning();
|
|
6332
|
+
}
|
|
5719
6333
|
const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
|
|
5720
6334
|
if (!skipClean && configInfo.proxies > cleanThreshold) {
|
|
5721
6335
|
const cache = readSubscriptionCache();
|
|
@@ -5755,7 +6369,7 @@ async function cmdStart(args) {
|
|
|
5755
6369
|
saveSubscriptionCache(sub.name, { last_auto_clean_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5756
6370
|
}
|
|
5757
6371
|
}
|
|
5758
|
-
printStatus();
|
|
6372
|
+
await printStatus();
|
|
5759
6373
|
}
|
|
5760
6374
|
|
|
5761
6375
|
// src/commands/shared.ts
|
|
@@ -5768,6 +6382,17 @@ async function dispatchSubcommand(args, table, options) {
|
|
|
5768
6382
|
}
|
|
5769
6383
|
return options.fallback(args);
|
|
5770
6384
|
}
|
|
6385
|
+
async function confirmPrompt(question) {
|
|
6386
|
+
if (!process.stdin.isTTY) return false;
|
|
6387
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6388
|
+
const answer = await new Promise((resolve) => {
|
|
6389
|
+
rl.question(`${question} (y/N) `, (a) => {
|
|
6390
|
+
rl.close();
|
|
6391
|
+
resolve(a);
|
|
6392
|
+
});
|
|
6393
|
+
});
|
|
6394
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
6395
|
+
}
|
|
5771
6396
|
function requireRunning() {
|
|
5772
6397
|
const state = getRunningState();
|
|
5773
6398
|
if (!state.running) {
|
|
@@ -5927,15 +6552,32 @@ function printDirectoryInfo() {
|
|
|
5927
6552
|
console.log(" MIHOMO_CLI_DIR: \u81EA\u5B9A\u4E49\u6839\u76EE\u5F55\u4F4D\u7F6E");
|
|
5928
6553
|
console.log("");
|
|
5929
6554
|
}
|
|
5930
|
-
var SUBCOMMANDS2 = [
|
|
5931
|
-
|
|
5932
|
-
|
|
6555
|
+
var SUBCOMMANDS2 = [
|
|
6556
|
+
{ name: "open", handler: openDirectory },
|
|
6557
|
+
{ name: "list", handler: printDirectoryInfo }
|
|
6558
|
+
];
|
|
6559
|
+
async function cmdDirectory(args) {
|
|
6560
|
+
await dispatchSubcommand(args, SUBCOMMANDS2, {
|
|
6561
|
+
fallback: printDirectoryInfo,
|
|
6562
|
+
onUnknown: (action) => {
|
|
6563
|
+
const names = SUBCOMMANDS2.flatMap((c) => [c.name, ...c.aliases ?? []]);
|
|
6564
|
+
const suggestion = suggestSimilar(action, names);
|
|
6565
|
+
throw new CliError(`\u672A\u77E5\u7684\u76EE\u5F55\u5B50\u547D\u4EE4: ${action}`, {
|
|
6566
|
+
hint: [
|
|
6567
|
+
...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [],
|
|
6568
|
+
"",
|
|
6569
|
+
"\u53EF\u7528\u5B50\u547D\u4EE4: open, list",
|
|
6570
|
+
"\u6253\u5F00\u6307\u5B9A\u76EE\u5F55: mihomo dir open <target>"
|
|
6571
|
+
]
|
|
6572
|
+
});
|
|
6573
|
+
}
|
|
6574
|
+
});
|
|
5933
6575
|
}
|
|
5934
6576
|
|
|
5935
6577
|
// src/kernel.ts
|
|
5936
6578
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
5937
|
-
import
|
|
5938
|
-
import
|
|
6579
|
+
import fs8 from "fs";
|
|
6580
|
+
import path7 from "path";
|
|
5939
6581
|
|
|
5940
6582
|
// node_modules/compare-versions/lib/esm/utils.js
|
|
5941
6583
|
var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
|
|
@@ -6061,10 +6703,10 @@ async function checkUpdate(mirror) {
|
|
|
6061
6703
|
}
|
|
6062
6704
|
function findBinaryInDir(dir, maxDepth = 4) {
|
|
6063
6705
|
if (maxDepth <= 0) return null;
|
|
6064
|
-
const files =
|
|
6706
|
+
const files = fs8.readdirSync(dir);
|
|
6065
6707
|
for (const f of files) {
|
|
6066
|
-
const fullPath =
|
|
6067
|
-
const stat =
|
|
6708
|
+
const fullPath = path7.join(dir, f);
|
|
6709
|
+
const stat = fs8.statSync(fullPath);
|
|
6068
6710
|
if (stat.isDirectory()) {
|
|
6069
6711
|
const found = findBinaryInDir(fullPath, maxDepth - 1);
|
|
6070
6712
|
if (found) return found;
|
|
@@ -6090,7 +6732,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6090
6732
|
\u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
|
|
6091
6733
|
}
|
|
6092
6734
|
const downloadUrl = withMirror(asset.browser_download_url, mirror);
|
|
6093
|
-
const tempPath =
|
|
6735
|
+
const tempPath = path7.join(DIRS.kernel, path7.basename(asset.name));
|
|
6094
6736
|
const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
|
|
6095
6737
|
if (mirror && progressCallback) {
|
|
6096
6738
|
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");
|
|
@@ -6111,12 +6753,12 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6111
6753
|
}
|
|
6112
6754
|
if (curlResult.status !== 0) {
|
|
6113
6755
|
try {
|
|
6114
|
-
|
|
6756
|
+
fs8.unlinkSync(tempPath);
|
|
6115
6757
|
} catch {
|
|
6116
6758
|
}
|
|
6117
6759
|
throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
|
|
6118
6760
|
}
|
|
6119
|
-
if (!
|
|
6761
|
+
if (!fs8.existsSync(tempPath)) {
|
|
6120
6762
|
throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
|
|
6121
6763
|
}
|
|
6122
6764
|
if (progressCallback) {
|
|
@@ -6139,17 +6781,17 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6139
6781
|
if (tarResult.error) throw tarResult.error;
|
|
6140
6782
|
if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
|
|
6141
6783
|
} else if (tempPath.endsWith(".gz")) {
|
|
6142
|
-
const baseName =
|
|
6143
|
-
const outputPath =
|
|
6784
|
+
const baseName = path7.basename(tempPath, ".gz");
|
|
6785
|
+
const outputPath = path7.join(extractPath, baseName);
|
|
6144
6786
|
const gzipResult = spawnSync4("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
|
|
6145
6787
|
if (gzipResult.error) throw gzipResult.error;
|
|
6146
6788
|
if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
|
|
6147
|
-
|
|
6789
|
+
fs8.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
|
|
6148
6790
|
extractedBinary = outputPath;
|
|
6149
6791
|
}
|
|
6150
6792
|
} catch (e) {
|
|
6151
6793
|
try {
|
|
6152
|
-
|
|
6794
|
+
fs8.unlinkSync(tempPath);
|
|
6153
6795
|
} catch {
|
|
6154
6796
|
}
|
|
6155
6797
|
throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
|
|
@@ -6157,23 +6799,23 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6157
6799
|
const foundBinary = extractedBinary || findBinaryInDir(extractPath);
|
|
6158
6800
|
if (!foundBinary) {
|
|
6159
6801
|
try {
|
|
6160
|
-
|
|
6802
|
+
fs8.unlinkSync(tempPath);
|
|
6161
6803
|
} catch {
|
|
6162
6804
|
}
|
|
6163
6805
|
throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
|
|
6164
6806
|
}
|
|
6165
6807
|
const targetPath = PATHS.mihomoBinary;
|
|
6166
6808
|
if (foundBinary !== targetPath) {
|
|
6167
|
-
if (
|
|
6168
|
-
|
|
6809
|
+
if (fs8.existsSync(targetPath)) {
|
|
6810
|
+
fs8.chmodSync(targetPath, 493);
|
|
6169
6811
|
try {
|
|
6170
|
-
|
|
6812
|
+
fs8.unlinkSync(targetPath);
|
|
6171
6813
|
} catch {
|
|
6172
6814
|
}
|
|
6173
6815
|
}
|
|
6174
|
-
|
|
6816
|
+
fs8.renameSync(foundBinary, targetPath);
|
|
6175
6817
|
}
|
|
6176
|
-
|
|
6818
|
+
fs8.chmodSync(targetPath, 493);
|
|
6177
6819
|
if (progressCallback) {
|
|
6178
6820
|
progressCallback("\u6821\u9A8C\u5185\u6838...");
|
|
6179
6821
|
}
|
|
@@ -6181,11 +6823,11 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6181
6823
|
const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
|
|
6182
6824
|
if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
|
|
6183
6825
|
try {
|
|
6184
|
-
|
|
6826
|
+
fs8.unlinkSync(targetPath);
|
|
6185
6827
|
} catch {
|
|
6186
6828
|
}
|
|
6187
6829
|
try {
|
|
6188
|
-
|
|
6830
|
+
fs8.unlinkSync(tempPath);
|
|
6189
6831
|
} catch {
|
|
6190
6832
|
}
|
|
6191
6833
|
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
|
|
@@ -6193,7 +6835,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6193
6835
|
\u8F93\u51FA: ${checkOutput || "(\u7A7A)"}`);
|
|
6194
6836
|
}
|
|
6195
6837
|
try {
|
|
6196
|
-
|
|
6838
|
+
fs8.unlinkSync(tempPath);
|
|
6197
6839
|
} catch {
|
|
6198
6840
|
}
|
|
6199
6841
|
clearKernelVersionCache();
|
|
@@ -6328,7 +6970,7 @@ function cmdLogs(args) {
|
|
|
6328
6970
|
}
|
|
6329
6971
|
|
|
6330
6972
|
// src/commands/overwrite.ts
|
|
6331
|
-
import
|
|
6973
|
+
import path8 from "path";
|
|
6332
6974
|
function printOverwriteList() {
|
|
6333
6975
|
const info = listOverwriteFile();
|
|
6334
6976
|
const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
|
|
@@ -6338,8 +6980,8 @@ function printOverwriteList() {
|
|
|
6338
6980
|
if (info.files.length === 0) {
|
|
6339
6981
|
console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
|
|
6340
6982
|
console.log("");
|
|
6341
|
-
console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${
|
|
6342
|
-
console.log(` \u6216 ${
|
|
6983
|
+
console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path8.join(info.dir, "overwrite.yaml")}`);
|
|
6984
|
+
console.log(` \u6216 ${path8.join(info.dir, "overwrite.dns.yaml")}`);
|
|
6343
6985
|
console.log("");
|
|
6344
6986
|
} else {
|
|
6345
6987
|
console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
|
|
@@ -6375,20 +7017,35 @@ async function setOverwrite(enabled, args) {
|
|
|
6375
7017
|
}
|
|
6376
7018
|
var SUBCOMMANDS3 = [
|
|
6377
7019
|
{ name: "on", aliases: ["enable"], handler: (args) => setOverwrite(true, args) },
|
|
6378
|
-
{ name: "off", aliases: ["disable"], handler: (args) => setOverwrite(false, args) }
|
|
7020
|
+
{ name: "off", aliases: ["disable"], handler: (args) => setOverwrite(false, args) },
|
|
7021
|
+
// list 显式注册:onUnknown 生效后,未注册的子命令会报错,不能再靠 fallback 兜住 `ow list`
|
|
7022
|
+
{
|
|
7023
|
+
name: "list",
|
|
7024
|
+
handler: () => {
|
|
7025
|
+
console.log("");
|
|
7026
|
+
printOverwriteList();
|
|
7027
|
+
}
|
|
7028
|
+
}
|
|
6379
7029
|
];
|
|
6380
7030
|
async function cmdOverwrite(args) {
|
|
6381
7031
|
await dispatchSubcommand(args, SUBCOMMANDS3, {
|
|
7032
|
+
// 无子命令 → 列表;未知子命令 → 报错(与 sub/daemon 同构,避免 `ow onn` 静默当成 list)
|
|
6382
7033
|
fallback: () => {
|
|
6383
7034
|
console.log("");
|
|
6384
7035
|
printOverwriteList();
|
|
7036
|
+
},
|
|
7037
|
+
onUnknown: (action) => {
|
|
7038
|
+
const names = SUBCOMMANDS3.flatMap((c) => [c.name, ...c.aliases ?? []]);
|
|
7039
|
+
const suggestion = suggestSimilar(action, names);
|
|
7040
|
+
throw new CliError(`\u672A\u77E5\u7684\u8986\u5199\u5B50\u547D\u4EE4: ${action}`, {
|
|
7041
|
+
hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "", "\u53EF\u7528\u5B50\u547D\u4EE4: on, off, list"]
|
|
7042
|
+
});
|
|
6385
7043
|
}
|
|
6386
7044
|
});
|
|
6387
7045
|
}
|
|
6388
7046
|
|
|
6389
7047
|
// src/commands/reset.ts
|
|
6390
|
-
import
|
|
6391
|
-
import readline from "readline";
|
|
7048
|
+
import fs9 from "fs";
|
|
6392
7049
|
var RESET_TARGETS = [
|
|
6393
7050
|
{
|
|
6394
7051
|
id: "subs",
|
|
@@ -6421,11 +7078,29 @@ var RESET_TARGETS = [
|
|
|
6421
7078
|
paths: () => [DIRS.runtime],
|
|
6422
7079
|
needsStop: true
|
|
6423
7080
|
},
|
|
7081
|
+
{
|
|
7082
|
+
// 隧道要在删 pid 文件之前先停进程(onBefore):文件一删就再也找不到那些 ssh 进程,
|
|
7083
|
+
// 它们会继续占着端口跑下去,且 CLI 无任何路径能停掉。
|
|
7084
|
+
// **必须排在 settings 之前**:onAfter 会 writeSettings 重建 settings.json,
|
|
7085
|
+
// 排在 settings 之后会让 `reset --full` 留下一个 {}(同 subs 的处理,见执行顺序注释)
|
|
7086
|
+
id: "tunnel",
|
|
7087
|
+
aliases: ["tunnel", "tunnels", "ssh"],
|
|
7088
|
+
label: "\u96A7\u9053",
|
|
7089
|
+
paths: () => [DIRS.tunnel],
|
|
7090
|
+
needsStop: false,
|
|
7091
|
+
onBefore: () => stopAllTunnels(),
|
|
7092
|
+
// 同步清空 settings 里的隧道列表:只删运行态会留下「列表在但状态没了」的半重置状态。
|
|
7093
|
+
// 覆写文件不动——那是用户维护的资产,由 overwrites target 负责
|
|
7094
|
+
onAfter: () => writeSettings({ tunnels: void 0 })
|
|
7095
|
+
},
|
|
6424
7096
|
{
|
|
6425
7097
|
id: "settings",
|
|
6426
7098
|
aliases: ["setting", "settings", "config"],
|
|
6427
7099
|
label: "\u8BBE\u7F6E",
|
|
6428
|
-
|
|
7100
|
+
// 同时删 .bak:readSettings 遇格式损坏会备份原文件(settings.ts),里面含
|
|
7101
|
+
// controller_secret 与订阅 URL 的 token。只删主文件会让 "已重置: 设置" 名不副实,
|
|
7102
|
+
// 凭据仍明文留在数据目录(cache.json.bak 在 subscriptions/ 内,随整目录删除,无需单列)
|
|
7103
|
+
paths: () => [PATHS.settingsFile, `${PATHS.settingsFile}.bak`],
|
|
6429
7104
|
needsStop: false
|
|
6430
7105
|
},
|
|
6431
7106
|
{
|
|
@@ -6445,8 +7120,8 @@ var RESET_TARGETS = [
|
|
|
6445
7120
|
label: "\u8986\u5199",
|
|
6446
7121
|
paths: () => {
|
|
6447
7122
|
const dir = USER_DATA_DIR;
|
|
6448
|
-
if (!
|
|
6449
|
-
return
|
|
7123
|
+
if (!fs9.existsSync(dir)) return [];
|
|
7124
|
+
return fs9.readdirSync(dir).filter(isOverwriteFilename).map((f) => `${dir}/${f}`);
|
|
6450
7125
|
},
|
|
6451
7126
|
needsStop: false
|
|
6452
7127
|
},
|
|
@@ -6475,18 +7150,9 @@ function resolveResetTargets(names) {
|
|
|
6475
7150
|
unmatched.push(name);
|
|
6476
7151
|
}
|
|
6477
7152
|
}
|
|
7153
|
+
matched.sort((a, b) => RESET_TARGETS.indexOf(a) - RESET_TARGETS.indexOf(b));
|
|
6478
7154
|
return { matched, unmatched };
|
|
6479
7155
|
}
|
|
6480
|
-
async function confirmPrompt(question) {
|
|
6481
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6482
|
-
const answer = await new Promise((resolve) => {
|
|
6483
|
-
rl.question(`${question} (y/N) `, (a) => {
|
|
6484
|
-
rl.close();
|
|
6485
|
-
resolve(a);
|
|
6486
|
-
});
|
|
6487
|
-
});
|
|
6488
|
-
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
6489
|
-
}
|
|
6490
7156
|
async function cmdReset(args) {
|
|
6491
7157
|
const flags = (args || []).filter((a) => a.startsWith("-"));
|
|
6492
7158
|
const names = (args || []).slice(1).filter((a) => !a.startsWith("-"));
|
|
@@ -6518,7 +7184,7 @@ async function cmdReset(args) {
|
|
|
6518
7184
|
}
|
|
6519
7185
|
targets = matched;
|
|
6520
7186
|
} else {
|
|
6521
|
-
targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
|
|
7187
|
+
targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon", "tunnel"].includes(t.id));
|
|
6522
7188
|
}
|
|
6523
7189
|
for (const t of targets) {
|
|
6524
7190
|
if (t.checkEmpty?.()) {
|
|
@@ -6541,29 +7207,46 @@ async function cmdReset(args) {
|
|
|
6541
7207
|
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"));
|
|
6542
7208
|
}
|
|
6543
7209
|
console.log(`\u5C06\u5220\u9664: ${targets.map((t) => t.label).join("\u3001")}`);
|
|
6544
|
-
if (!skipConfirm
|
|
6545
|
-
|
|
6546
|
-
|
|
7210
|
+
if (!skipConfirm) {
|
|
7211
|
+
if (!process.stdin.isTTY) {
|
|
7212
|
+
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"] });
|
|
7213
|
+
}
|
|
7214
|
+
if (!await confirmPrompt("\u786E\u8BA4?")) {
|
|
7215
|
+
console.log("\u5DF2\u53D6\u6D88");
|
|
7216
|
+
return;
|
|
7217
|
+
}
|
|
6547
7218
|
}
|
|
6548
7219
|
if (disablesDaemon && isDaemonEnabled()) {
|
|
6549
7220
|
try {
|
|
6550
7221
|
disableDaemon();
|
|
6551
7222
|
} catch (e) {
|
|
6552
|
-
|
|
6553
|
-
|
|
7223
|
+
if (e instanceof CliError) throw e;
|
|
7224
|
+
throw new CliError(e.message.split("\n")[0], { label: "\u4FDD\u6D3B\u5173\u95ED\u5DF2\u53D6\u6D88\uFF0C\u91CD\u7F6E\u4E2D\u6B62" });
|
|
6554
7225
|
}
|
|
6555
7226
|
}
|
|
6556
7227
|
if (needsStop && getMihomoPids().length > 0) {
|
|
6557
7228
|
console.log("\u505C\u6B62\u8FDB\u7A0B...");
|
|
6558
|
-
cleanupAll();
|
|
7229
|
+
const cleanup = cleanupAll();
|
|
6559
7230
|
for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
|
|
6560
7231
|
if (getMihomoPids().length === 0) break;
|
|
6561
7232
|
await new Promise((r) => setTimeout(r, PROCESS_WAIT_INTERVAL));
|
|
6562
7233
|
}
|
|
7234
|
+
const remaining = getMihomoPids();
|
|
7235
|
+
if (remaining.length > 0) {
|
|
7236
|
+
throw new CliError(remaining.join(", "), {
|
|
7237
|
+
label: "\u8FDB\u7A0B\u672A\u80FD\u505C\u6B62\uFF0C\u91CD\u7F6E\u4E2D\u6B62",
|
|
7238
|
+
hint: [
|
|
7239
|
+
`\u672A\u7EC8\u6B62\u7684\u8FDB\u7A0B: ${remaining.join(", ")}${cleanup.failed > 0 ? `\uFF08${cleanup.failed} \u4E2A\u7EC8\u6B62\u5931\u8D25\uFF09` : ""}`,
|
|
7240
|
+
"\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo",
|
|
7241
|
+
"\u5426\u5219\u6B8B\u7559\u8FDB\u7A0B\u4F1A\u7EE7\u7EED\u4F7F\u7528\u5373\u5C06\u5220\u9664\u7684\u914D\u7F6E\u3002"
|
|
7242
|
+
]
|
|
7243
|
+
});
|
|
7244
|
+
}
|
|
6563
7245
|
}
|
|
6564
7246
|
for (const t of targets) {
|
|
7247
|
+
t.onBefore?.();
|
|
6565
7248
|
for (const p of t.paths()) {
|
|
6566
|
-
if (
|
|
7249
|
+
if (fs9.existsSync(p)) {
|
|
6567
7250
|
try {
|
|
6568
7251
|
rmrf(p);
|
|
6569
7252
|
} catch (e) {
|
|
@@ -6581,10 +7264,11 @@ async function cmdReset(args) {
|
|
|
6581
7264
|
}
|
|
6582
7265
|
|
|
6583
7266
|
// src/test-instance.ts
|
|
6584
|
-
import { spawn as
|
|
6585
|
-
import
|
|
6586
|
-
import
|
|
7267
|
+
import { spawn as spawn3 } from "child_process";
|
|
7268
|
+
import fs10 from "fs";
|
|
7269
|
+
import path9 from "path";
|
|
6587
7270
|
function isProxyValid(proxy) {
|
|
7271
|
+
if (proxy === null || typeof proxy !== "object") return false;
|
|
6588
7272
|
if (!proxy.name || !proxy.server || !proxy.port) return false;
|
|
6589
7273
|
if (!proxy.type) return false;
|
|
6590
7274
|
if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
|
|
@@ -6593,20 +7277,20 @@ function isProxyValid(proxy) {
|
|
|
6593
7277
|
}
|
|
6594
7278
|
return true;
|
|
6595
7279
|
}
|
|
6596
|
-
var TEST_DIR =
|
|
7280
|
+
var TEST_DIR = path9.join(USER_DATA_DIR, "test");
|
|
6597
7281
|
var TEST_DIRS = {
|
|
6598
|
-
data:
|
|
6599
|
-
runtime:
|
|
7282
|
+
data: path9.join(TEST_DIR, "data"),
|
|
7283
|
+
runtime: path9.join(TEST_DIR, "runtime")
|
|
6600
7284
|
};
|
|
6601
7285
|
var TEST_PATHS = {
|
|
6602
|
-
configFile:
|
|
6603
|
-
pidFile:
|
|
6604
|
-
logFile:
|
|
7286
|
+
configFile: path9.join(TEST_DIRS.runtime, "config.yaml"),
|
|
7287
|
+
pidFile: path9.join(TEST_DIRS.runtime, "pid"),
|
|
7288
|
+
logFile: path9.join(TEST_DIR, "test.log")
|
|
6605
7289
|
};
|
|
6606
7290
|
var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
|
|
6607
7291
|
function ensureTestDirs() {
|
|
6608
7292
|
for (const dir of Object.values(TEST_DIRS)) {
|
|
6609
|
-
|
|
7293
|
+
fs10.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
6610
7294
|
}
|
|
6611
7295
|
}
|
|
6612
7296
|
function cleanupTestDir() {
|
|
@@ -6644,24 +7328,25 @@ function buildTestConfig(subName) {
|
|
|
6644
7328
|
rules: ["MATCH,PROXY"]
|
|
6645
7329
|
};
|
|
6646
7330
|
const content = dumpYaml(config);
|
|
6647
|
-
|
|
7331
|
+
fs10.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
|
|
6648
7332
|
}
|
|
6649
7333
|
async function startTestInstance() {
|
|
6650
7334
|
const binary = PATHS.mihomoBinary;
|
|
6651
|
-
if (!
|
|
7335
|
+
if (!fs10.existsSync(binary)) throw new CliError('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
|
|
6652
7336
|
stopTestInstance();
|
|
6653
|
-
const logFd =
|
|
6654
|
-
const child =
|
|
7337
|
+
const logFd = fs10.openSync(TEST_PATHS.logFile, "a");
|
|
7338
|
+
const child = spawn3(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
|
|
6655
7339
|
detached: true,
|
|
6656
7340
|
stdio: ["ignore", logFd, logFd]
|
|
6657
7341
|
});
|
|
6658
7342
|
child.on("error", () => {
|
|
6659
7343
|
});
|
|
6660
|
-
|
|
7344
|
+
fs10.closeSync(logFd);
|
|
6661
7345
|
child.unref();
|
|
6662
7346
|
const pid = child.pid;
|
|
6663
7347
|
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");
|
|
6664
|
-
|
|
7348
|
+
spawnedTestPid = pid;
|
|
7349
|
+
fs10.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
|
|
6665
7350
|
const client = createHttpClient({ timeout: 2e3 });
|
|
6666
7351
|
let ready = false;
|
|
6667
7352
|
for (let i = 0; i < 60; i++) {
|
|
@@ -6677,7 +7362,7 @@ async function startTestInstance() {
|
|
|
6677
7362
|
if (!isProcessRunning(pid)) {
|
|
6678
7363
|
let errorDetail = "";
|
|
6679
7364
|
try {
|
|
6680
|
-
errorDetail =
|
|
7365
|
+
errorDetail = fs10.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
|
|
6681
7366
|
} catch {
|
|
6682
7367
|
}
|
|
6683
7368
|
throw new CliError(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
|
|
@@ -6687,13 +7372,16 @@ ${errorDetail}` : ""}`);
|
|
|
6687
7372
|
throw new CliError("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
|
|
6688
7373
|
}
|
|
6689
7374
|
}
|
|
7375
|
+
var spawnedTestPid = null;
|
|
6690
7376
|
function stopTestInstance() {
|
|
6691
|
-
let pid;
|
|
7377
|
+
let pid = null;
|
|
6692
7378
|
try {
|
|
6693
|
-
|
|
7379
|
+
const fromFile = parseInt(fs10.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
|
|
7380
|
+
if (fromFile > 0) pid = fromFile;
|
|
6694
7381
|
} catch {
|
|
6695
|
-
return;
|
|
6696
7382
|
}
|
|
7383
|
+
if (pid === null) pid = spawnedTestPid;
|
|
7384
|
+
if (pid === null) return;
|
|
6697
7385
|
if (pid > 0 && isProcessRunning(pid) && isProcessCommandMatching(pid, TEST_PATHS.configFile)) {
|
|
6698
7386
|
process.kill(pid, "SIGKILL");
|
|
6699
7387
|
for (let i = 0; i < 20; i++) {
|
|
@@ -6701,8 +7389,9 @@ function stopTestInstance() {
|
|
|
6701
7389
|
sleepSync(100);
|
|
6702
7390
|
}
|
|
6703
7391
|
}
|
|
7392
|
+
spawnedTestPid = null;
|
|
6704
7393
|
try {
|
|
6705
|
-
|
|
7394
|
+
fs10.unlinkSync(TEST_PATHS.pidFile);
|
|
6706
7395
|
} catch {
|
|
6707
7396
|
}
|
|
6708
7397
|
}
|
|
@@ -6824,11 +7513,12 @@ async function subAdd(args) {
|
|
|
6824
7513
|
console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
|
|
6825
7514
|
addSubscription(normalizedUrl, name);
|
|
6826
7515
|
try {
|
|
6827
|
-
setDefaultSubscription(name);
|
|
6828
7516
|
const info = await downloadMergedSubscription(urls, name);
|
|
7517
|
+
setDefaultSubscription(name);
|
|
6829
7518
|
console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
|
|
6830
7519
|
} catch (e) {
|
|
6831
7520
|
removeSubscription(name);
|
|
7521
|
+
if (e instanceof CliError) throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25", hint: e.hint });
|
|
6832
7522
|
throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25" });
|
|
6833
7523
|
}
|
|
6834
7524
|
} else {
|
|
@@ -6838,13 +7528,14 @@ async function subAdd(args) {
|
|
|
6838
7528
|
console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
|
|
6839
7529
|
addSubscription(url, name);
|
|
6840
7530
|
try {
|
|
6841
|
-
setDefaultSubscription(name);
|
|
6842
7531
|
const info = await downloadSubscription(url, name);
|
|
7532
|
+
setDefaultSubscription(name);
|
|
6843
7533
|
const repoUrl = githubRepoUrl(url);
|
|
6844
7534
|
if (repoUrl) saveSubscriptionCache(name, { web_page_url: repoUrl });
|
|
6845
7535
|
console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)})`);
|
|
6846
7536
|
} catch (e) {
|
|
6847
7537
|
removeSubscription(name);
|
|
7538
|
+
if (e instanceof CliError) throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25", hint: e.hint });
|
|
6848
7539
|
throw new CliError(e.message, { label: "\u6DFB\u52A0\u5931\u8D25" });
|
|
6849
7540
|
}
|
|
6850
7541
|
}
|
|
@@ -6941,8 +7632,8 @@ async function subWeb(args) {
|
|
|
6941
7632
|
console.log("\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740");
|
|
6942
7633
|
}
|
|
6943
7634
|
}
|
|
6944
|
-
function subRemove(args) {
|
|
6945
|
-
const name = args
|
|
7635
|
+
async function subRemove(args) {
|
|
7636
|
+
const name = getNonFlagArg(args, 2);
|
|
6946
7637
|
const subs = getSubscriptions();
|
|
6947
7638
|
if (!name) {
|
|
6948
7639
|
throw new CliError("\u8BF7\u6307\u5B9A\u8981\u5220\u9664\u7684\u8BA2\u9605\u540D\u79F0", {
|
|
@@ -6950,6 +7641,21 @@ function subRemove(args) {
|
|
|
6950
7641
|
});
|
|
6951
7642
|
}
|
|
6952
7643
|
const target = resolveSubscription(subs, name);
|
|
7644
|
+
const isExact = target.name === name;
|
|
7645
|
+
const skipConfirm = hasFlag(args, "-y", "--yes");
|
|
7646
|
+
if (!isExact && !skipConfirm) {
|
|
7647
|
+
if (!process.stdin.isTTY) {
|
|
7648
|
+
throw new CliError(`\u6A21\u7CCA\u5339\u914D\u5230 "${target.name}"\uFF0C\u975E\u4EA4\u4E92\u73AF\u5883\u9700\u786E\u8BA4`, {
|
|
7649
|
+
label: "\u5DF2\u53D6\u6D88",
|
|
7650
|
+
hint: [`\u8BF7\u7528\u5B8C\u6574\u540D\u79F0: mihomo sub remove ${target.name}`, `\u6216\u8DF3\u8FC7\u786E\u8BA4: mihomo sub remove ${name} -y`]
|
|
7651
|
+
});
|
|
7652
|
+
}
|
|
7653
|
+
console.log(`\u5C06\u5220\u9664\u8BA2\u9605 "${target.name}" (\u6A21\u7CCA\u5339\u914D "${name}")`);
|
|
7654
|
+
if (!await confirmPrompt("\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\uFF0C\u786E\u8BA4?")) {
|
|
7655
|
+
console.log("\u5DF2\u53D6\u6D88");
|
|
7656
|
+
return;
|
|
7657
|
+
}
|
|
7658
|
+
}
|
|
6953
7659
|
const switchedTo = removeSubscription(target.name);
|
|
6954
7660
|
console.log(`\u5DF2\u5220\u9664\u8BA2\u9605 "${target.name}"`);
|
|
6955
7661
|
if (switchedTo) {
|
|
@@ -7031,10 +7737,10 @@ async function cmdSubscription(args) {
|
|
|
7031
7737
|
|
|
7032
7738
|
// src/commands/test.ts
|
|
7033
7739
|
async function cmdTest(args) {
|
|
7034
|
-
requireRunning();
|
|
7035
|
-
const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
|
|
7036
7740
|
const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
|
|
7037
7741
|
const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
|
|
7742
|
+
requireRunning();
|
|
7743
|
+
const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
|
|
7038
7744
|
console.log(`\u6D4B\u8BD5 "${activeSub.name}" \u8282\u70B9\u8FDE\u901A\u6027...`);
|
|
7039
7745
|
console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
|
|
7040
7746
|
console.log("");
|
|
@@ -7048,11 +7754,11 @@ async function cmdTest(args) {
|
|
|
7048
7754
|
console.log(formatTestSummary(summary));
|
|
7049
7755
|
}
|
|
7050
7756
|
async function cmdClean(args) {
|
|
7051
|
-
requireRunning();
|
|
7052
|
-
const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
|
|
7053
7757
|
const timeout = parseIntArg(args, "-t", "--timeout", DEFAULT_TEST_TIMEOUT);
|
|
7054
7758
|
const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
|
|
7055
7759
|
const rounds = parseIntArg(args, "-r", "--rounds", DEFAULT_CLEAN_ROUNDS);
|
|
7760
|
+
requireRunning();
|
|
7761
|
+
const activeSub = requireActiveSubscription("\u6CA1\u6709\u6D3B\u8DC3\u8BA2\u9605");
|
|
7056
7762
|
console.log(`\u6E05\u7406 "${activeSub.name}" \u5931\u8D25\u8282\u70B9...`);
|
|
7057
7763
|
console.log(`\u8D85\u65F6: ${timeout}ms \u5E76\u53D1: ${concurrency}`);
|
|
7058
7764
|
console.log("");
|
|
@@ -7095,6 +7801,224 @@ async function cmdClean(args) {
|
|
|
7095
7801
|
}
|
|
7096
7802
|
}
|
|
7097
7803
|
|
|
7804
|
+
// src/commands/tunnel.ts
|
|
7805
|
+
function formatState(status) {
|
|
7806
|
+
switch (status.state) {
|
|
7807
|
+
case "running":
|
|
7808
|
+
return colors.green(`\u8FD0\u884C\u4E2D (PID ${status.pid})`);
|
|
7809
|
+
case "dead-port":
|
|
7810
|
+
return colors.yellow(`\u5047\u6D3B (PID ${status.pid}\uFF0C\u7AEF\u53E3 ${status.config.port} \u4E0D\u901A)`);
|
|
7811
|
+
default:
|
|
7812
|
+
return colors.yellow("\u672A\u8FD0\u884C");
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7815
|
+
function getTunnelNameArg(args) {
|
|
7816
|
+
return getNonFlagArg(args, 2);
|
|
7817
|
+
}
|
|
7818
|
+
function requireTunnelNameArg(args, usage) {
|
|
7819
|
+
const name = getTunnelNameArg(args);
|
|
7820
|
+
if (!name) {
|
|
7821
|
+
const tunnels = getTunnels();
|
|
7822
|
+
throw new CliError("\u8BF7\u6307\u5B9A\u96A7\u9053\u540D\u79F0", {
|
|
7823
|
+
hint: [usage, ...tunnels.length > 0 ? ["", "\u53EF\u7528\u96A7\u9053:", ...tunnels.map((t) => ` ${t.name}`)] : []]
|
|
7824
|
+
});
|
|
7825
|
+
}
|
|
7826
|
+
return name;
|
|
7827
|
+
}
|
|
7828
|
+
async function printTunnelList() {
|
|
7829
|
+
const tunnels = getTunnels();
|
|
7830
|
+
console.log("");
|
|
7831
|
+
if (tunnels.length === 0) {
|
|
7832
|
+
console.log("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053");
|
|
7833
|
+
console.log("");
|
|
7834
|
+
console.log("\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>");
|
|
7835
|
+
console.log(colors.gray(" \u4F8B\u5982: mihomo tunnel add work --host m4 --port 1080"));
|
|
7836
|
+
console.log(colors.gray(" \u96A7\u9053\u628A\u5185\u7F51\u51FA\u53E3\u66B4\u9732\u4E3A\u672C\u5730 SOCKS5\uFF0C\u914D\u5408\u8986\u5199\u6587\u4EF6\u5206\u6D41\u5185\u7F51\u57DF\u540D"));
|
|
7837
|
+
console.log("");
|
|
7838
|
+
return;
|
|
7839
|
+
}
|
|
7840
|
+
const statuses = await getAllTunnelStatus();
|
|
7841
|
+
console.log(colors.cyan("\u96A7\u9053\u5217\u8868:"));
|
|
7842
|
+
console.log("");
|
|
7843
|
+
for (const status of statuses) {
|
|
7844
|
+
const { config } = status;
|
|
7845
|
+
const autoLabel = config.auto ? colors.gray(" [auto]") : "";
|
|
7846
|
+
console.log(` ${colors.bold(config.name)}${autoLabel}`);
|
|
7847
|
+
console.log(` ${colors.gray("\u51FA\u53E3: ")}${config.host} \u2192 127.0.0.1:${config.port}`);
|
|
7848
|
+
console.log(` ${colors.gray("\u72B6\u6001: ")}${formatState(status)}`);
|
|
7849
|
+
if (status.started_by) {
|
|
7850
|
+
console.log(` ${colors.gray("\u6765\u6E90: ")}${status.started_by === "auto" ? "\u968F start \u62C9\u8D77" : "\u624B\u52A8\u542F\u52A8"}`);
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7853
|
+
console.log("");
|
|
7854
|
+
console.log("\u542F\u52A8: mihomo tunnel up [\u540D\u5B57] \u505C\u6B62: mihomo tunnel down [\u540D\u5B57]");
|
|
7855
|
+
console.log("\u72B6\u6001: mihomo tunnel status \u5220\u9664: mihomo tunnel rm <\u540D\u5B57>");
|
|
7856
|
+
console.log("");
|
|
7857
|
+
}
|
|
7858
|
+
async function tunnelAdd(args) {
|
|
7859
|
+
const name = requireTunnelNameArg(args, "\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3> [--no-auto]");
|
|
7860
|
+
const host = parseStringArg(args, "--host");
|
|
7861
|
+
if (!host) {
|
|
7862
|
+
throw new CliError("\u7F3A\u5C11 --host", {
|
|
7863
|
+
hint: ["\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>", "", "\u4E3B\u673A\u53EF\u7528 ~/.ssh/config \u91CC\u7684\u522B\u540D\uFF0C\u4F8B\u5982 --host m4"]
|
|
7864
|
+
});
|
|
7865
|
+
}
|
|
7866
|
+
const portRaw = parseStringArg(args, "--port");
|
|
7867
|
+
if (!portRaw) {
|
|
7868
|
+
throw new CliError("\u7F3A\u5C11 --port", { hint: ["\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"] });
|
|
7869
|
+
}
|
|
7870
|
+
if (!/^\d+$/.test(portRaw.trim())) {
|
|
7871
|
+
throw new CliError(`\u7AEF\u53E3\u65E0\u6548: "${portRaw}"\uFF0C\u9700\u4E3A 1-65535 \u7684\u6574\u6570`);
|
|
7872
|
+
}
|
|
7873
|
+
const port = Number(portRaw.trim());
|
|
7874
|
+
const auto = !hasFlag(args, "--no-auto");
|
|
7875
|
+
const config = { name, host, port, auto };
|
|
7876
|
+
addTunnel(config);
|
|
7877
|
+
const created = ensureTunnelOverwriteFile(config);
|
|
7878
|
+
const overwritePath = getTunnelOverwritePath(name);
|
|
7879
|
+
console.log(`${colors.green("\u5DF2\u6DFB\u52A0\u96A7\u9053")} ${name} \xB7 ${host} \u2192 127.0.0.1:${port}${auto ? " \xB7 auto" : ""}`);
|
|
7880
|
+
console.log("");
|
|
7881
|
+
if (created) {
|
|
7882
|
+
console.log(`\u5DF2\u751F\u6210\u8986\u5199\u6A21\u677F: ${overwritePath}`);
|
|
7883
|
+
console.log(colors.gray(" \u6A21\u677F\u53EA\u5EFA\u597D socks5 \u8282\u70B9\u4E0E\u5206\u7EC4\uFF0C\u5206\u6D41\u89C4\u5219\u9700\u4F60\u586B\u5199\uFF08CLI \u65E0\u4ECE\u77E5\u9053\u4F60\u7684\u5185\u7F51\u57DF\u540D\uFF09"));
|
|
7884
|
+
console.log(colors.gray(" \u7F16\u8F91\u540E\u6267\u884C mihomo start \u751F\u6548"));
|
|
7885
|
+
} else {
|
|
7886
|
+
console.log(`\u8986\u5199\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u672A\u6539\u52A8: ${overwritePath}`);
|
|
7887
|
+
}
|
|
7888
|
+
console.log("");
|
|
7889
|
+
console.log(`\u542F\u52A8\u96A7\u9053: mihomo tunnel up ${name}`);
|
|
7890
|
+
if (created) {
|
|
7891
|
+
await restartToApply(args);
|
|
7892
|
+
}
|
|
7893
|
+
}
|
|
7894
|
+
async function tunnelUp(args) {
|
|
7895
|
+
const name = getTunnelNameArg(args);
|
|
7896
|
+
const targets = name ? [resolveTunnel(name)] : getTunnels();
|
|
7897
|
+
if (targets.length === 0) {
|
|
7898
|
+
throw new CliError("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053", { hint: ["\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"] });
|
|
7899
|
+
}
|
|
7900
|
+
for (const config of targets) {
|
|
7901
|
+
if (ensureTunnelOverwriteFile(config)) {
|
|
7902
|
+
console.log(colors.gray(`\u5DF2\u8865\u5EFA\u8986\u5199\u6A21\u677F: ${getTunnelOverwritePath(config.name)}`));
|
|
7903
|
+
}
|
|
7904
|
+
const result = await startTunnel(config.name, { startedBy: "manual" });
|
|
7905
|
+
if (result.alreadyRunning) {
|
|
7906
|
+
console.log(`${colors.gray("\u5DF2\u5728\u8FD0\u884C")} ${config.name} (PID ${result.pid})`);
|
|
7907
|
+
} else {
|
|
7908
|
+
console.log(`${colors.green("\u5DF2\u542F\u52A8\u96A7\u9053")} ${config.name} \xB7 ${config.host} \u2192 127.0.0.1:${config.port} (PID ${result.pid})`);
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
function tunnelDown(args) {
|
|
7913
|
+
const name = getTunnelNameArg(args);
|
|
7914
|
+
const targets = name ? [resolveTunnel(name)] : getTunnels();
|
|
7915
|
+
if (targets.length === 0) {
|
|
7916
|
+
throw new CliError("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053");
|
|
7917
|
+
}
|
|
7918
|
+
let stopped = 0;
|
|
7919
|
+
for (const config of targets) {
|
|
7920
|
+
const result = stopTunnel(config.name);
|
|
7921
|
+
if (result.notRunning) {
|
|
7922
|
+
console.log(`${colors.yellow("\u4E0D\u5728\u8FD0\u884C")} ${config.name}`);
|
|
7923
|
+
} else {
|
|
7924
|
+
console.log(`${colors.green("\u5DF2\u505C\u6B62\u96A7\u9053")} ${config.name} (PID ${result.pid})`);
|
|
7925
|
+
stopped++;
|
|
7926
|
+
}
|
|
7927
|
+
}
|
|
7928
|
+
if (stopped > 0) {
|
|
7929
|
+
console.log("");
|
|
7930
|
+
console.log(colors.gray("\u6CE8\u610F: \u8986\u5199\u6587\u4EF6\u4ECD\u5728\uFF0C\u914D\u7F6E\u91CC\u7684\u96A7\u9053\u8282\u70B9\u73B0\u5728\u6307\u5411\u672A\u76D1\u542C\u7684\u7AEF\u53E3"));
|
|
7931
|
+
}
|
|
7932
|
+
}
|
|
7933
|
+
async function tunnelStatus(args) {
|
|
7934
|
+
const name = getTunnelNameArg(args);
|
|
7935
|
+
if (!name) {
|
|
7936
|
+
await printTunnelList();
|
|
7937
|
+
return;
|
|
7938
|
+
}
|
|
7939
|
+
const config = resolveTunnel(name);
|
|
7940
|
+
const status = await getTunnelStatus(config);
|
|
7941
|
+
console.log("");
|
|
7942
|
+
console.log(`${colors.gray("\u96A7\u9053: ")}${config.name}${config.auto ? colors.gray(" [auto]") : ""}`);
|
|
7943
|
+
console.log(`${colors.gray("\u51FA\u53E3: ")}${config.host} \u2192 127.0.0.1:${config.port}`);
|
|
7944
|
+
console.log(`${colors.gray("\u72B6\u6001: ")}${formatState(status)}`);
|
|
7945
|
+
if (status.started_by) {
|
|
7946
|
+
console.log(`${colors.gray("\u6765\u6E90: ")}${status.started_by === "auto" ? "\u968F start \u62C9\u8D77" : "\u624B\u52A8\u542F\u52A8"}`);
|
|
7947
|
+
}
|
|
7948
|
+
console.log("");
|
|
7949
|
+
if (status.state === "dead-port") {
|
|
7950
|
+
console.log(colors.yellow("ssh \u8FDB\u7A0B\u8FD8\u5728\uFF0C\u4F46\u7AEF\u53E3\u6CA1\u6709\u76D1\u542C\u2014\u2014\u5185\u7F51\u5206\u6D41\u5F53\u524D\u4E0D\u53EF\u7528"));
|
|
7951
|
+
console.log(`\u91CD\u542F\u96A7\u9053: mihomo tunnel down ${config.name} && mihomo tunnel up ${config.name}`);
|
|
7952
|
+
console.log(colors.gray(` \u65E5\u5FD7: ${getTunnelLogPath(config.name)}`));
|
|
7953
|
+
console.log("");
|
|
7954
|
+
} else if (status.state === "stopped") {
|
|
7955
|
+
console.log(`\u542F\u52A8\u96A7\u9053: mihomo tunnel up ${config.name}`);
|
|
7956
|
+
console.log("");
|
|
7957
|
+
}
|
|
7958
|
+
}
|
|
7959
|
+
async function tunnelRemove(args) {
|
|
7960
|
+
const name = requireTunnelNameArg(args, "\u7528\u6CD5: mihomo tunnel rm <\u540D\u5B57> [-y]");
|
|
7961
|
+
const config = resolveTunnel(name);
|
|
7962
|
+
const skipConfirm = hasFlag(args, "-y", "--yes");
|
|
7963
|
+
if (!skipConfirm) {
|
|
7964
|
+
if (!process.stdin.isTTY) {
|
|
7965
|
+
throw new CliError("\u5220\u9664\u96A7\u9053\u9700\u8981\u786E\u8BA4", {
|
|
7966
|
+
label: "\u5DF2\u53D6\u6D88",
|
|
7967
|
+
hint: [`\u8DF3\u8FC7\u786E\u8BA4: mihomo tunnel rm ${config.name} -y`]
|
|
7968
|
+
});
|
|
7969
|
+
}
|
|
7970
|
+
const confirmed = await confirmPrompt(`\u786E\u8BA4\u5220\u9664\u96A7\u9053 "${config.name}"?`);
|
|
7971
|
+
if (!confirmed) {
|
|
7972
|
+
console.log("\u5DF2\u53D6\u6D88");
|
|
7973
|
+
return;
|
|
7974
|
+
}
|
|
7975
|
+
}
|
|
7976
|
+
stopTunnel(config.name);
|
|
7977
|
+
removeTunnel(config.name);
|
|
7978
|
+
console.log(`${colors.green("\u5DF2\u5220\u9664\u96A7\u9053")} ${config.name}`);
|
|
7979
|
+
console.log("");
|
|
7980
|
+
console.log(`\u8986\u5199\u6587\u4EF6\u672A\u5220\u9664: ${getTunnelOverwritePath(config.name)}`);
|
|
7981
|
+
console.log(colors.gray(" \u5B83\u4ECD\u4F1A\u5411\u914D\u7F6E\u6CE8\u5165\u6307\u5411\u8BE5\u7AEF\u53E3\u7684\u8282\u70B9\uFF0C\u5982\u4E0D\u518D\u9700\u8981\u8BF7\u81EA\u884C\u5220\u9664"));
|
|
7982
|
+
}
|
|
7983
|
+
function resolveTunnel(name) {
|
|
7984
|
+
const config = findTunnel(name);
|
|
7985
|
+
if (!config) {
|
|
7986
|
+
const tunnels = getTunnels();
|
|
7987
|
+
const suggestion = suggestSimilar(
|
|
7988
|
+
name,
|
|
7989
|
+
tunnels.map((t) => t.name)
|
|
7990
|
+
);
|
|
7991
|
+
throw new CliError(`\u672A\u627E\u5230\u96A7\u9053 "${name}"`, {
|
|
7992
|
+
hint: [
|
|
7993
|
+
...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [],
|
|
7994
|
+
...tunnels.length > 0 ? ["", "\u53EF\u7528\u96A7\u9053:", ...tunnels.map((t) => ` ${t.name}`)] : ["", "\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"]
|
|
7995
|
+
]
|
|
7996
|
+
});
|
|
7997
|
+
}
|
|
7998
|
+
return config;
|
|
7999
|
+
}
|
|
8000
|
+
var SUBCOMMANDS5 = [
|
|
8001
|
+
{ name: "list", handler: printTunnelList },
|
|
8002
|
+
{ name: "add", handler: tunnelAdd },
|
|
8003
|
+
{ name: "up", aliases: ["start"], handler: tunnelUp },
|
|
8004
|
+
{ name: "down", aliases: ["stop"], handler: tunnelDown },
|
|
8005
|
+
{ name: "status", handler: tunnelStatus },
|
|
8006
|
+
{ name: "remove", aliases: ["rm", "delete"], handler: tunnelRemove }
|
|
8007
|
+
];
|
|
8008
|
+
async function cmdTunnel(args) {
|
|
8009
|
+
await dispatchSubcommand(args, SUBCOMMANDS5, {
|
|
8010
|
+
// 无 action → 列表;未知 action → 报错(不静默回落,否则 `tunnel upp` 会看似成功)
|
|
8011
|
+
fallback: printTunnelList,
|
|
8012
|
+
onUnknown: (action) => {
|
|
8013
|
+
const names = SUBCOMMANDS5.flatMap((c) => [c.name, ...c.aliases ?? []]);
|
|
8014
|
+
const suggestion = suggestSimilar(action, names);
|
|
8015
|
+
throw new CliError(`\u672A\u77E5\u7684 tunnel \u5B50\u547D\u4EE4: ${action}`, {
|
|
8016
|
+
hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "", "\u53EF\u7528\u5B50\u547D\u4EE4: list, add, up, down, status, rm"]
|
|
8017
|
+
});
|
|
8018
|
+
}
|
|
8019
|
+
});
|
|
8020
|
+
}
|
|
8021
|
+
|
|
7098
8022
|
// src/commands/ui.ts
|
|
7099
8023
|
function cmdUI(args) {
|
|
7100
8024
|
const uiName = args[1] || "zash";
|
|
@@ -7119,7 +8043,7 @@ function cmdUI(args) {
|
|
|
7119
8043
|
}
|
|
7120
8044
|
|
|
7121
8045
|
// src/commands/update.ts
|
|
7122
|
-
import { execFile, spawn as
|
|
8046
|
+
import { execFile, spawn as spawn4 } from "child_process";
|
|
7123
8047
|
import { promisify } from "util";
|
|
7124
8048
|
var execFileAsync = promisify(execFile);
|
|
7125
8049
|
var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
@@ -7150,7 +8074,7 @@ async function cmdUpdate() {
|
|
|
7150
8074
|
console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
|
|
7151
8075
|
console.log("");
|
|
7152
8076
|
await new Promise((resolve, reject) => {
|
|
7153
|
-
const npm =
|
|
8077
|
+
const npm = spawn4("npm", ["install", "-g", PKG_NAME], { stdio: "inherit" });
|
|
7154
8078
|
npm.on("close", (code) => {
|
|
7155
8079
|
if (code === 0) {
|
|
7156
8080
|
resolve();
|
|
@@ -7236,18 +8160,18 @@ var COMMANDS = [
|
|
|
7236
8160
|
// === 订阅 ===
|
|
7237
8161
|
{
|
|
7238
8162
|
name: "subscription",
|
|
7239
|
-
aliases: ["sub", "subscriptions"],
|
|
8163
|
+
aliases: ["sub", "subs", "subscriptions"],
|
|
7240
8164
|
handler: cmdSubscription,
|
|
7241
8165
|
group: "subscription",
|
|
7242
8166
|
usage: [
|
|
7243
|
-
"subscription \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub\uFF09",
|
|
8167
|
+
"subscription \u5217\u51FA\u6240\u6709\u8BA2\u9605\uFF08\u522B\u540D sub/subs\uFF09",
|
|
7244
8168
|
"subscription use <name> \u5207\u6362\u5F53\u524D\u8BA2\u9605",
|
|
7245
8169
|
"subscription add <url> [name] \u6DFB\u52A0\u8BA2\u9605",
|
|
7246
8170
|
"subscription update [name] \u66F4\u65B0\u8BA2\u9605\uFF08\u65E0\u53C2\u66F4\u65B0\u6240\u6709\uFF09",
|
|
7247
|
-
"subscription remove <name> \u5220\u9664\u8BA2\u9605",
|
|
8171
|
+
"subscription remove <name> \u5220\u9664\u8BA2\u9605\uFF08\u6A21\u7CCA\u5339\u914D\u9700\u786E\u8BA4\uFF0C-y \u8DF3\u8FC7\uFF09",
|
|
7248
8172
|
"subscription web [name] \u6253\u5F00\u8BA2\u9605\u9875\u9762",
|
|
7249
8173
|
"subscription test [name] \u6D4B\u8BD5\u8282\u70B9\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u65E0\u9700\u8FD0\u884C\uFF09",
|
|
7250
|
-
"subscription clean [name] \u6D4B\u901F\u6E05\u7406\uFF08\u72EC\u7ACB\u5B9E\u4F8B\uFF0C\u4E0D\u52A8\u4E3B\u5B9E\u4F8B\uFF09"
|
|
8174
|
+
"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]"
|
|
7251
8175
|
]
|
|
7252
8176
|
},
|
|
7253
8177
|
{
|
|
@@ -7284,7 +8208,7 @@ var COMMANDS = [
|
|
|
7284
8208
|
name: "on",
|
|
7285
8209
|
aliases: [],
|
|
7286
8210
|
handler: cmdOverwrite,
|
|
7287
|
-
rewrite: () => ["ow", "on"],
|
|
8211
|
+
rewrite: (args) => ["ow", "on", ...args.slice(1)],
|
|
7288
8212
|
group: "config",
|
|
7289
8213
|
usage: []
|
|
7290
8214
|
},
|
|
@@ -7292,7 +8216,7 @@ var COMMANDS = [
|
|
|
7292
8216
|
name: "off",
|
|
7293
8217
|
aliases: [],
|
|
7294
8218
|
handler: cmdOverwrite,
|
|
7295
|
-
rewrite: () => ["ow", "off"],
|
|
8219
|
+
rewrite: (args) => ["ow", "off", ...args.slice(1)],
|
|
7296
8220
|
group: "config",
|
|
7297
8221
|
usage: []
|
|
7298
8222
|
},
|
|
@@ -7326,6 +8250,20 @@ var COMMANDS = [
|
|
|
7326
8250
|
group: "system",
|
|
7327
8251
|
usage: ["daemon on|off \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF08\u4EC5 Mixed\uFF0C\u9700\u7BA1\u7406\u5458\u5BC6\u7801\uFF09", "daemon status \u67E5\u770B\u4FDD\u6D3B\u72B6\u6001"]
|
|
7328
8252
|
},
|
|
8253
|
+
{
|
|
8254
|
+
// 别名不能用 tun——已被 TUN 模式快捷命令占用(注册表重复 token 会在模块加载期直接抛错)
|
|
8255
|
+
name: "tunnel",
|
|
8256
|
+
aliases: ["ssh", "tunnels"],
|
|
8257
|
+
handler: cmdTunnel,
|
|
8258
|
+
group: "system",
|
|
8259
|
+
usage: [
|
|
8260
|
+
"tunnel \u5217\u51FA ssh \u96A7\u9053\uFF08\u522B\u540D ssh\uFF09",
|
|
8261
|
+
"tunnel add <\u540D\u5B57> --host <\u4E3B\u673A> --port <\u7AEF\u53E3> [--no-auto]",
|
|
8262
|
+
"tunnel up|down [\u540D\u5B57] \u542F\u52A8/\u505C\u6B62\u96A7\u9053\uFF08\u65E0\u53C2\u5373\u5168\u90E8\uFF09",
|
|
8263
|
+
"tunnel status [\u540D\u5B57] \u67E5\u770B\u96A7\u9053\u72B6\u6001\uFF08\u771F\u5B9E\u63A2\u6D4B\u7AEF\u53E3\uFF09",
|
|
8264
|
+
"tunnel rm <\u540D\u5B57> [-y] \u5220\u9664\u96A7\u9053\uFF08\u4E0D\u5220\u8986\u5199\u6587\u4EF6\uFF09"
|
|
8265
|
+
]
|
|
8266
|
+
},
|
|
7329
8267
|
{
|
|
7330
8268
|
name: "update",
|
|
7331
8269
|
aliases: ["upd", "upgrade"],
|
|
@@ -7411,12 +8349,27 @@ function clearProxyEnv() {
|
|
|
7411
8349
|
delete process.env.all_proxy;
|
|
7412
8350
|
delete process.env.ALL_PROXY;
|
|
7413
8351
|
}
|
|
8352
|
+
var PLATFORM_FREE_COMMANDS = /* @__PURE__ */ new Set(["help", "version"]);
|
|
8353
|
+
function assertSupportedPlatform(commandName) {
|
|
8354
|
+
if (process.platform === "darwin") return;
|
|
8355
|
+
if (PLATFORM_FREE_COMMANDS.has(commandName)) return;
|
|
8356
|
+
if (process.env.MIHOMO_CLI_ALLOW_ANY_PLATFORM === "1") return;
|
|
8357
|
+
throw new CliError(`mihomo-cli \u76EE\u524D\u4EC5\u652F\u6301 macOS\uFF08\u5F53\u524D\u5E73\u53F0: ${process.platform}\uFF09`, {
|
|
8358
|
+
label: "\u5E73\u53F0\u4E0D\u652F\u6301",
|
|
8359
|
+
hint: [
|
|
8360
|
+
"\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",
|
|
8361
|
+
"Windows / Linux \u9002\u914D\u4ECD\u5728\u8FDB\u884C\u4E2D\u3002",
|
|
8362
|
+
"\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"
|
|
8363
|
+
]
|
|
8364
|
+
});
|
|
8365
|
+
}
|
|
7414
8366
|
async function main() {
|
|
7415
8367
|
clearProxyEnv();
|
|
7416
|
-
ensureDirs();
|
|
7417
8368
|
const args = process.argv.slice(2);
|
|
7418
8369
|
if (args.length === 0) {
|
|
7419
|
-
|
|
8370
|
+
assertSupportedPlatform("status");
|
|
8371
|
+
ensureDirs();
|
|
8372
|
+
await printStatus();
|
|
7420
8373
|
printShortHelp();
|
|
7421
8374
|
return;
|
|
7422
8375
|
}
|
|
@@ -7428,6 +8381,8 @@ async function main() {
|
|
|
7428
8381
|
hint: [suggestion.length > 0 ? `\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?` : '\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9']
|
|
7429
8382
|
});
|
|
7430
8383
|
}
|
|
8384
|
+
assertSupportedPlatform(command.name);
|
|
8385
|
+
ensureDirs();
|
|
7431
8386
|
await command.handler(command.rewrite ? command.rewrite(args) : args);
|
|
7432
8387
|
}
|
|
7433
8388
|
main().catch((e) => {
|